Merge "[Extensions] Fixed surface abandoned crash when pause/resume too quickly on Samsung Z Fold2" into androidx-main
diff --git a/OWNERS b/OWNERS
index 0393b15..7e8dbef 100644
--- a/OWNERS
+++ b/OWNERS
@@ -4,6 +4,7 @@
 aurimas@google.com
 ccraik@google.com
 clarabayarri@google.com
+danysantiago@google.com
 emberrose@google.com
 fsladkey@google.com
 ilake@google.com
@@ -16,10 +17,8 @@
 natnaelbelay@google.com
 owengray@google.com
 romainguy@android.com
-saff@google.com
 sergeyv@google.com
 siyamed@google.com
-sjgilbert@google.com
 sumir@google.com
 tiem@google.com
 yboyar@google.com
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/androidTest/java/androidx/activity/compose/ActivityResultRegistryTest.kt b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/ActivityResultRegistryTest.kt
index 68e1f87..cbbe2c8 100644
--- a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/ActivityResultRegistryTest.kt
+++ b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/ActivityResultRegistryTest.kt
@@ -59,8 +59,8 @@
     val composeTestRule = createComposeRule()
 
     var launchCount = 0
-    val registryOwner = ActivityResultRegistryOwner {
-        object : ActivityResultRegistry() {
+    val registryOwner = object : ActivityResultRegistryOwner {
+        override val activityResultRegistry = object : ActivityResultRegistry() {
             override fun <I : Any?, O : Any?> onLaunch(
                 requestCode: Int,
                 contract: ActivityResultContract<I, O>,
@@ -161,8 +161,8 @@
 
         activityScenario.recreate()
 
-        val restoredOwner = ActivityResultRegistryOwner {
-            object : ActivityResultRegistry() {
+        val restoredOwner = object : ActivityResultRegistryOwner {
+            override val activityResultRegistry = object : ActivityResultRegistry() {
                 override fun <I : Any?, O : Any?> onLaunch(
                     requestCode: Int,
                     contract: ActivityResultContract<I, O>,
@@ -210,7 +210,9 @@
                 code = requestCode
             }
         }
-        val owner = ActivityResultRegistryOwner { registry }
+        val owner = object : ActivityResultRegistryOwner {
+            override val activityResultRegistry = registry
+        }
         var recompose by mutableStateOf(false)
         val launchChannel = Channel<Boolean>()
         val launchFlow = launchChannel.receiveAsFlow()
@@ -261,7 +263,9 @@
                 launchCount++
             }
         }
-        val owner = ActivityResultRegistryOwner { registry }
+        val owner = object : ActivityResultRegistryOwner {
+            override val activityResultRegistry = registry
+        }
         composeTestRule.setContent {
             var recompose by remember { mutableStateOf(false) }
             CompositionLocalProvider(
@@ -307,7 +311,9 @@
                 launchCount++
             }
         }
-        val owner = ActivityResultRegistryOwner { registry }
+        val owner = object : ActivityResultRegistryOwner {
+            override val activityResultRegistry = registry
+        }
         val contract = ActivityResultContracts.StartActivityForResult()
         composeTestRule.setContent {
             var recompose by remember { mutableStateOf(false) }
diff --git a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt
index a17c127..def0983 100644
--- a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt
+++ b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt
@@ -29,6 +29,7 @@
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
 import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
@@ -113,22 +114,21 @@
      */
     @Test
     fun testBackHandlerLifecycle() {
-        var inteceptedBack = false
+        var interceptedBack = false
         val lifecycleOwner = TestLifecycleOwner()
 
         composeTestRule.setContent {
             val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
-            val dispatcherOwner = object : OnBackPressedDispatcherOwner {
-                override fun getLifecycle() = lifecycleOwner.lifecycle
-
-                override fun getOnBackPressedDispatcher() = dispatcher
+            val dispatcherOwner =
+                object : OnBackPressedDispatcherOwner, LifecycleOwner by lifecycleOwner {
+                    override val onBackPressedDispatcher = dispatcher
             }
             dispatcher.addCallback(lifecycleOwner) { }
             CompositionLocalProvider(
                 LocalOnBackPressedDispatcherOwner provides dispatcherOwner,
                 LocalLifecycleOwner provides lifecycleOwner
             ) {
-                BackHandler { inteceptedBack = true }
+                BackHandler { interceptedBack = true }
             }
             Button(onClick = { dispatcher.onBackPressed() }) {
                 Text(text = "Press Back")
@@ -140,7 +140,7 @@
 
         composeTestRule.onNodeWithText("Press Back").performClick()
         composeTestRule.runOnIdle {
-            assertThat(inteceptedBack).isEqualTo(true)
+            assertThat(interceptedBack).isEqualTo(true)
         }
     }
 }
diff --git a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackPressedDispatcherOwnerTest.kt b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackPressedDispatcherOwnerTest.kt
index 1dc4598..458eafa 100644
--- a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackPressedDispatcherOwnerTest.kt
+++ b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackPressedDispatcherOwnerTest.kt
@@ -24,8 +24,8 @@
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleRegistry
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
@@ -55,14 +55,8 @@
     @Test
     fun testGetBackPressedDispatcherProviders() {
         val testDispatcherOwner: OnBackPressedDispatcherOwner =
-            object : OnBackPressedDispatcherOwner {
-                override fun getLifecycle(): Lifecycle {
-                    return LifecycleRegistry(this)
-                }
-
-                override fun getOnBackPressedDispatcher(): OnBackPressedDispatcher {
-                    return OnBackPressedDispatcher()
-                }
+            object : OnBackPressedDispatcherOwner, LifecycleOwner by TestLifecycleOwner() {
+                override val onBackPressedDispatcher = OnBackPressedDispatcher()
             }
 
         var innerDispatcherOwner: OnBackPressedDispatcherOwner? = null
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-ktx/api/current.ignore b/activity/activity-ktx/api/current.ignore
new file mode 100644
index 0000000..d23680b
--- /dev/null
+++ b/activity/activity-ktx/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedClass: androidx.activity.OnBackPressedDispatcherKt:
+    Removed class androidx.activity.OnBackPressedDispatcherKt
diff --git a/activity/activity-ktx/api/current.txt b/activity/activity-ktx/api/current.txt
index eb507d0..ba9fdc3 100644
--- a/activity/activity-ktx/api/current.txt
+++ b/activity/activity-ktx/api/current.txt
@@ -6,10 +6,6 @@
     method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM> viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
   }
 
-  public final class OnBackPressedDispatcherKt {
-    method public static androidx.activity.OnBackPressedCallback addCallback(androidx.activity.OnBackPressedDispatcher, optional androidx.lifecycle.LifecycleOwner? owner, optional boolean enabled, kotlin.jvm.functions.Function1<? super androidx.activity.OnBackPressedCallback,kotlin.Unit> onBackPressed);
-  }
-
   public final class PipHintTrackerKt {
   }
 
diff --git a/activity/activity-ktx/api/public_plus_experimental_current.txt b/activity/activity-ktx/api/public_plus_experimental_current.txt
index 29f1554..cece293 100644
--- a/activity/activity-ktx/api/public_plus_experimental_current.txt
+++ b/activity/activity-ktx/api/public_plus_experimental_current.txt
@@ -6,10 +6,6 @@
     method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM> viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
   }
 
-  public final class OnBackPressedDispatcherKt {
-    method public static androidx.activity.OnBackPressedCallback addCallback(androidx.activity.OnBackPressedDispatcher, optional androidx.lifecycle.LifecycleOwner? owner, optional boolean enabled, kotlin.jvm.functions.Function1<? super androidx.activity.OnBackPressedCallback,kotlin.Unit> onBackPressed);
-  }
-
   public final class PipHintTrackerKt {
     method @RequiresApi(android.os.Build.VERSION_CODES.O) @kotlinx.coroutines.ExperimentalCoroutinesApi public static suspend Object? trackPipAnimationHintView(android.app.Activity, android.view.View view, kotlin.coroutines.Continuation<? super kotlin.Unit>);
   }
diff --git a/activity/activity-ktx/api/restricted_current.ignore b/activity/activity-ktx/api/restricted_current.ignore
new file mode 100644
index 0000000..d23680b
--- /dev/null
+++ b/activity/activity-ktx/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedClass: androidx.activity.OnBackPressedDispatcherKt:
+    Removed class androidx.activity.OnBackPressedDispatcherKt
diff --git a/activity/activity-ktx/api/restricted_current.txt b/activity/activity-ktx/api/restricted_current.txt
index eb507d0..ba9fdc3 100644
--- a/activity/activity-ktx/api/restricted_current.txt
+++ b/activity/activity-ktx/api/restricted_current.txt
@@ -6,10 +6,6 @@
     method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM> viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
   }
 
-  public final class OnBackPressedDispatcherKt {
-    method public static androidx.activity.OnBackPressedCallback addCallback(androidx.activity.OnBackPressedDispatcher, optional androidx.lifecycle.LifecycleOwner? owner, optional boolean enabled, kotlin.jvm.functions.Function1<? super androidx.activity.OnBackPressedCallback,kotlin.Unit> onBackPressed);
-  }
-
   public final class PipHintTrackerKt {
   }
 
diff --git a/activity/activity-ktx/src/androidTest/java/androidx/activity/ActivityViewModelLazyTest.kt b/activity/activity-ktx/src/androidTest/java/androidx/activity/ActivityViewModelLazyTest.kt
index bfcd4e1..c3f7793 100644
--- a/activity/activity-ktx/src/androidTest/java/androidx/activity/ActivityViewModelLazyTest.kt
+++ b/activity/activity-ktx/src/androidTest/java/androidx/activity/ActivityViewModelLazyTest.kt
@@ -62,15 +62,15 @@
             super.onCreate(savedInstanceState)
         }
 
-        override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
-            return SavedStateViewModelFactory()
-        }
+        override val defaultViewModelProviderFactory
+            get() = SavedStateViewModelFactory()
 
-        override fun getDefaultViewModelCreationExtras(): CreationExtras {
-            val extras = MutableCreationExtras(super.getDefaultViewModelCreationExtras())
-            extras[DEFAULT_ARGS_KEY] = bundleOf("test" to "value")
-            return extras
-        }
+        override val defaultViewModelCreationExtras: CreationExtras
+            get() {
+                val extras = MutableCreationExtras(super.defaultViewModelCreationExtras)
+                extras[DEFAULT_ARGS_KEY] = bundleOf("test" to "value")
+                return extras
+            }
     }
 
     class TestViewModel : ViewModel()
diff --git a/activity/activity-ktx/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt b/activity/activity-ktx/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
deleted file mode 100644
index 0a08e5f..0000000
--- a/activity/activity-ktx/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
+++ /dev/null
@@ -1,162 +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.activity
-
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.testing.TestLifecycleOwner
-import androidx.test.annotation.UiThreadTest
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertWithMessage
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class OnBackPressedDispatcherTest {
-
-    private lateinit var dispatcher: OnBackPressedDispatcher
-
-    @Before
-    fun setup() {
-        dispatcher = OnBackPressedDispatcher()
-    }
-
-    @UiThreadTest
-    @Test
-    fun testRegisterCallback() {
-        var count = 0
-        val callback = dispatcher.addCallback {
-            count++
-        }
-        assertWithMessage("Callback should be enabled by default")
-            .that(callback.isEnabled)
-            .isTrue()
-        assertWithMessage("Dispatcher should have an enabled callback")
-            .that(dispatcher.hasEnabledCallbacks())
-            .isTrue()
-
-        dispatcher.onBackPressed()
-
-        assertWithMessage("Count should be incremented after onBackPressed")
-            .that(count)
-            .isEqualTo(1)
-    }
-
-    @UiThreadTest
-    @Test
-    fun testRegisterDisabledCallback() {
-        var count = 0
-        val callback = dispatcher.addCallback(enabled = false) {
-            count++
-        }
-        assertWithMessage("Callback should be disabled by default")
-            .that(callback.isEnabled)
-            .isFalse()
-        assertWithMessage("Dispatcher should not have an enabled callback")
-            .that(dispatcher.hasEnabledCallbacks())
-            .isFalse()
-
-        callback.isEnabled = true
-
-        assertWithMessage("Dispatcher should have an enabled callback after setting isEnabled")
-            .that(dispatcher.hasEnabledCallbacks())
-            .isTrue()
-    }
-
-    @UiThreadTest
-    @Test
-    fun testLifecycleCallback() {
-        val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.INITIALIZED)
-        var count = 0
-        dispatcher.addCallback(lifecycleOwner) {
-            count++
-        }
-        assertWithMessage("Handler should return false if the Lifecycle isn't started")
-            .that(dispatcher.hasEnabledCallbacks())
-            .isFalse()
-        dispatcher.onBackPressed()
-        assertWithMessage("Non-started callbacks shouldn't have their count incremented")
-            .that(count)
-            .isEqualTo(0)
-
-        // Now start the Lifecycle
-        lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START)
-        dispatcher.onBackPressed()
-        assertWithMessage("Once the callbacks is started, the count should increment")
-            .that(count)
-            .isEqualTo(1)
-
-        // Now stop the Lifecycle
-        lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
-        assertWithMessage("Handler should return false if the Lifecycle isn't started")
-            .that(dispatcher.hasEnabledCallbacks())
-            .isFalse()
-        dispatcher.onBackPressed()
-        assertWithMessage("Non-started callbacks shouldn't have their count incremented")
-            .that(count)
-            .isEqualTo(1)
-    }
-
-    @UiThreadTest
-    @Test
-    fun testIsEnabledWithinCallback() {
-        var count = 0
-        val callback = dispatcher.addCallback {
-            count++
-            isEnabled = false
-        }
-        assertWithMessage("Callback should be enabled by default")
-            .that(callback.isEnabled)
-            .isTrue()
-        assertWithMessage("Dispatcher should have an enabled callback")
-            .that(dispatcher.hasEnabledCallbacks())
-            .isTrue()
-
-        dispatcher.onBackPressed()
-
-        assertWithMessage("Count should be incremented after onBackPressed")
-            .that(count)
-            .isEqualTo(1)
-        assertWithMessage("Callback should be disabled after onBackPressed()")
-            .that(callback.isEnabled)
-            .isFalse()
-        assertWithMessage("Dispatcher should have no enabled callbacks")
-            .that(dispatcher.hasEnabledCallbacks())
-            .isFalse()
-    }
-
-    @UiThreadTest
-    @Test
-    fun testRemoveWithinCallback() {
-        var count = 0
-        dispatcher.addCallback {
-            count++
-            remove()
-        }
-
-        dispatcher.onBackPressed()
-
-        assertWithMessage("Count should be incremented after onBackPressed")
-            .that(count)
-            .isEqualTo(1)
-        assertWithMessage("Dispatcher should have no enabled callbacks after remove")
-            .that(dispatcher.hasEnabledCallbacks())
-            .isFalse()
-    }
-}
diff --git a/activity/activity-ktx/src/main/java/androidx/activity/OnBackPressedDispatcher.kt b/activity/activity-ktx/src/main/java/androidx/activity/OnBackPressedDispatcher.kt
deleted file mode 100644
index 298e5ff..0000000
--- a/activity/activity-ktx/src/main/java/androidx/activity/OnBackPressedDispatcher.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.activity
-
-import androidx.lifecycle.LifecycleOwner
-
-/**
- * Create and add a new [OnBackPressedCallback] that calls [onBackPressed] in
- * [OnBackPressedCallback.handleOnBackPressed].
- *
- * If an [owner] is specified, the callback will only be added when the Lifecycle is
- * [androidx.lifecycle.Lifecycle.State.STARTED].
- *
- * A default [enabled] state can be supplied.
- */
-public fun OnBackPressedDispatcher.addCallback(
-    owner: LifecycleOwner? = null,
-    enabled: Boolean = true,
-    onBackPressed: OnBackPressedCallback.() -> Unit
-): OnBackPressedCallback {
-    val callback = object : OnBackPressedCallback(enabled) {
-        override fun handleOnBackPressed() {
-            onBackPressed()
-        }
-    }
-    if (owner != null) {
-        addCallback(owner, callback)
-    } else {
-        addCallback(callback)
-    }
-    return callback
-}
diff --git a/activity/activity/api/current.ignore b/activity/activity/api/current.ignore
new file mode 100644
index 0000000..149cb0c
--- /dev/null
+++ b/activity/activity/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+ChangedType: androidx.activity.result.IntentSenderRequest#CREATOR:
+    Field androidx.activity.result.IntentSenderRequest.CREATOR has changed type from android.os.Parcelable.Creator<androidx.activity.result.IntentSenderRequest!> to android.os.Parcelable.Creator<androidx.activity.result.IntentSenderRequest>
diff --git a/activity/activity/api/current.txt b/activity/activity/api/current.txt
index 078f6f9..688b1a0 100644
--- a/activity/activity/api/current.txt
+++ b/activity/activity/api/current.txt
@@ -48,10 +48,12 @@
   public class ComponentDialog extends android.app.Dialog implements androidx.lifecycle.LifecycleOwner androidx.activity.OnBackPressedDispatcherOwner androidx.savedstate.SavedStateRegistryOwner {
     ctor public ComponentDialog(android.content.Context context, optional @StyleRes int themeResId);
     ctor public ComponentDialog(android.content.Context context);
-    method public final androidx.lifecycle.Lifecycle getLifecycle();
+    method public androidx.lifecycle.Lifecycle getLifecycle();
     method public final androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
     method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
     method @CallSuper public void onBackPressed();
+    property public androidx.lifecycle.Lifecycle lifecycle;
+    property public final androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
     property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
   }
 
@@ -84,17 +86,22 @@
   }
 
   public final class OnBackPressedDispatcher {
+    ctor public OnBackPressedDispatcher(optional Runnable? fallbackOnBackPressed);
     ctor public OnBackPressedDispatcher();
-    ctor public OnBackPressedDispatcher(Runnable?);
-    method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback);
-    method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
+    method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback onBackPressedCallback);
+    method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner owner, androidx.activity.OnBackPressedCallback onBackPressedCallback);
     method @MainThread public boolean hasEnabledCallbacks();
     method @MainThread public void onBackPressed();
-    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher invoker);
+  }
+
+  public final class OnBackPressedDispatcherKt {
+    method public static androidx.activity.OnBackPressedCallback addCallback(androidx.activity.OnBackPressedDispatcher, optional androidx.lifecycle.LifecycleOwner? owner, optional boolean enabled, kotlin.jvm.functions.Function1<? super androidx.activity.OnBackPressedCallback,kotlin.Unit> onBackPressed);
   }
 
   public interface OnBackPressedDispatcherOwner extends androidx.lifecycle.LifecycleOwner {
     method public androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
+    property public abstract androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
   }
 
   public final class ViewTreeFullyDrawnReporterOwner {
@@ -174,6 +181,7 @@
 
   public interface ActivityResultRegistryOwner {
     method public androidx.activity.result.ActivityResultRegistry getActivityResultRegistry();
+    property public abstract androidx.activity.result.ActivityResultRegistry activityResultRegistry;
   }
 
   public final class IntentSenderRequest implements android.os.Parcelable {
@@ -182,16 +190,24 @@
     method public int getFlagsMask();
     method public int getFlagsValues();
     method public android.content.IntentSender getIntentSender();
-    method public void writeToParcel(android.os.Parcel, int);
-    field public static final android.os.Parcelable.Creator<androidx.activity.result.IntentSenderRequest!> CREATOR;
+    method public void writeToParcel(android.os.Parcel dest, int flags);
+    property public final android.content.Intent? fillInIntent;
+    property public final int flagsMask;
+    property public final int flagsValues;
+    property public final android.content.IntentSender intentSender;
+    field public static final android.os.Parcelable.Creator<androidx.activity.result.IntentSenderRequest> CREATOR;
+    field public static final androidx.activity.result.IntentSenderRequest.Companion Companion;
   }
 
   public static final class IntentSenderRequest.Builder {
-    ctor public IntentSenderRequest.Builder(android.content.IntentSender);
-    ctor public IntentSenderRequest.Builder(android.app.PendingIntent);
+    ctor public IntentSenderRequest.Builder(android.content.IntentSender intentSender);
+    ctor public IntentSenderRequest.Builder(android.app.PendingIntent pendingIntent);
     method public androidx.activity.result.IntentSenderRequest build();
-    method public androidx.activity.result.IntentSenderRequest.Builder setFillInIntent(android.content.Intent?);
-    method public androidx.activity.result.IntentSenderRequest.Builder setFlags(int, int);
+    method public androidx.activity.result.IntentSenderRequest.Builder setFillInIntent(android.content.Intent? fillInIntent);
+    method public androidx.activity.result.IntentSenderRequest.Builder setFlags(int values, int mask);
+  }
+
+  public static final class IntentSenderRequest.Companion {
   }
 
   public final class PickVisualMediaRequest {
diff --git a/activity/activity/api/public_plus_experimental_current.txt b/activity/activity/api/public_plus_experimental_current.txt
index 078f6f9..688b1a0 100644
--- a/activity/activity/api/public_plus_experimental_current.txt
+++ b/activity/activity/api/public_plus_experimental_current.txt
@@ -48,10 +48,12 @@
   public class ComponentDialog extends android.app.Dialog implements androidx.lifecycle.LifecycleOwner androidx.activity.OnBackPressedDispatcherOwner androidx.savedstate.SavedStateRegistryOwner {
     ctor public ComponentDialog(android.content.Context context, optional @StyleRes int themeResId);
     ctor public ComponentDialog(android.content.Context context);
-    method public final androidx.lifecycle.Lifecycle getLifecycle();
+    method public androidx.lifecycle.Lifecycle getLifecycle();
     method public final androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
     method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
     method @CallSuper public void onBackPressed();
+    property public androidx.lifecycle.Lifecycle lifecycle;
+    property public final androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
     property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
   }
 
@@ -84,17 +86,22 @@
   }
 
   public final class OnBackPressedDispatcher {
+    ctor public OnBackPressedDispatcher(optional Runnable? fallbackOnBackPressed);
     ctor public OnBackPressedDispatcher();
-    ctor public OnBackPressedDispatcher(Runnable?);
-    method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback);
-    method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
+    method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback onBackPressedCallback);
+    method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner owner, androidx.activity.OnBackPressedCallback onBackPressedCallback);
     method @MainThread public boolean hasEnabledCallbacks();
     method @MainThread public void onBackPressed();
-    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher invoker);
+  }
+
+  public final class OnBackPressedDispatcherKt {
+    method public static androidx.activity.OnBackPressedCallback addCallback(androidx.activity.OnBackPressedDispatcher, optional androidx.lifecycle.LifecycleOwner? owner, optional boolean enabled, kotlin.jvm.functions.Function1<? super androidx.activity.OnBackPressedCallback,kotlin.Unit> onBackPressed);
   }
 
   public interface OnBackPressedDispatcherOwner extends androidx.lifecycle.LifecycleOwner {
     method public androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
+    property public abstract androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
   }
 
   public final class ViewTreeFullyDrawnReporterOwner {
@@ -174,6 +181,7 @@
 
   public interface ActivityResultRegistryOwner {
     method public androidx.activity.result.ActivityResultRegistry getActivityResultRegistry();
+    property public abstract androidx.activity.result.ActivityResultRegistry activityResultRegistry;
   }
 
   public final class IntentSenderRequest implements android.os.Parcelable {
@@ -182,16 +190,24 @@
     method public int getFlagsMask();
     method public int getFlagsValues();
     method public android.content.IntentSender getIntentSender();
-    method public void writeToParcel(android.os.Parcel, int);
-    field public static final android.os.Parcelable.Creator<androidx.activity.result.IntentSenderRequest!> CREATOR;
+    method public void writeToParcel(android.os.Parcel dest, int flags);
+    property public final android.content.Intent? fillInIntent;
+    property public final int flagsMask;
+    property public final int flagsValues;
+    property public final android.content.IntentSender intentSender;
+    field public static final android.os.Parcelable.Creator<androidx.activity.result.IntentSenderRequest> CREATOR;
+    field public static final androidx.activity.result.IntentSenderRequest.Companion Companion;
   }
 
   public static final class IntentSenderRequest.Builder {
-    ctor public IntentSenderRequest.Builder(android.content.IntentSender);
-    ctor public IntentSenderRequest.Builder(android.app.PendingIntent);
+    ctor public IntentSenderRequest.Builder(android.content.IntentSender intentSender);
+    ctor public IntentSenderRequest.Builder(android.app.PendingIntent pendingIntent);
     method public androidx.activity.result.IntentSenderRequest build();
-    method public androidx.activity.result.IntentSenderRequest.Builder setFillInIntent(android.content.Intent?);
-    method public androidx.activity.result.IntentSenderRequest.Builder setFlags(int, int);
+    method public androidx.activity.result.IntentSenderRequest.Builder setFillInIntent(android.content.Intent? fillInIntent);
+    method public androidx.activity.result.IntentSenderRequest.Builder setFlags(int values, int mask);
+  }
+
+  public static final class IntentSenderRequest.Companion {
   }
 
   public final class PickVisualMediaRequest {
diff --git a/activity/activity/api/restricted_current.ignore b/activity/activity/api/restricted_current.ignore
new file mode 100644
index 0000000..149cb0c
--- /dev/null
+++ b/activity/activity/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+ChangedType: androidx.activity.result.IntentSenderRequest#CREATOR:
+    Field androidx.activity.result.IntentSenderRequest.CREATOR has changed type from android.os.Parcelable.Creator<androidx.activity.result.IntentSenderRequest!> to android.os.Parcelable.Creator<androidx.activity.result.IntentSenderRequest>
diff --git a/activity/activity/api/restricted_current.txt b/activity/activity/api/restricted_current.txt
index 575b8b9..1d24b8c 100644
--- a/activity/activity/api/restricted_current.txt
+++ b/activity/activity/api/restricted_current.txt
@@ -47,10 +47,12 @@
   public class ComponentDialog extends android.app.Dialog implements androidx.lifecycle.LifecycleOwner androidx.activity.OnBackPressedDispatcherOwner androidx.savedstate.SavedStateRegistryOwner {
     ctor public ComponentDialog(android.content.Context context, optional @StyleRes int themeResId);
     ctor public ComponentDialog(android.content.Context context);
-    method public final androidx.lifecycle.Lifecycle getLifecycle();
+    method public androidx.lifecycle.Lifecycle getLifecycle();
     method public final androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
     method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
     method @CallSuper public void onBackPressed();
+    property public androidx.lifecycle.Lifecycle lifecycle;
+    property public final androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
     property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
   }
 
@@ -83,17 +85,22 @@
   }
 
   public final class OnBackPressedDispatcher {
+    ctor public OnBackPressedDispatcher(optional Runnable? fallbackOnBackPressed);
     ctor public OnBackPressedDispatcher();
-    ctor public OnBackPressedDispatcher(Runnable?);
-    method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback);
-    method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
+    method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback onBackPressedCallback);
+    method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner owner, androidx.activity.OnBackPressedCallback onBackPressedCallback);
     method @MainThread public boolean hasEnabledCallbacks();
     method @MainThread public void onBackPressed();
-    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher invoker);
+  }
+
+  public final class OnBackPressedDispatcherKt {
+    method public static androidx.activity.OnBackPressedCallback addCallback(androidx.activity.OnBackPressedDispatcher, optional androidx.lifecycle.LifecycleOwner? owner, optional boolean enabled, kotlin.jvm.functions.Function1<? super androidx.activity.OnBackPressedCallback,kotlin.Unit> onBackPressed);
   }
 
   public interface OnBackPressedDispatcherOwner extends androidx.lifecycle.LifecycleOwner {
     method public androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
+    property public abstract androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
   }
 
   public final class ViewTreeFullyDrawnReporterOwner {
@@ -173,6 +180,7 @@
 
   public interface ActivityResultRegistryOwner {
     method public androidx.activity.result.ActivityResultRegistry getActivityResultRegistry();
+    property public abstract androidx.activity.result.ActivityResultRegistry activityResultRegistry;
   }
 
   public final class IntentSenderRequest implements android.os.Parcelable {
@@ -181,16 +189,24 @@
     method public int getFlagsMask();
     method public int getFlagsValues();
     method public android.content.IntentSender getIntentSender();
-    method public void writeToParcel(android.os.Parcel, int);
-    field public static final android.os.Parcelable.Creator<androidx.activity.result.IntentSenderRequest!> CREATOR;
+    method public void writeToParcel(android.os.Parcel dest, int flags);
+    property public final android.content.Intent? fillInIntent;
+    property public final int flagsMask;
+    property public final int flagsValues;
+    property public final android.content.IntentSender intentSender;
+    field public static final android.os.Parcelable.Creator<androidx.activity.result.IntentSenderRequest> CREATOR;
+    field public static final androidx.activity.result.IntentSenderRequest.Companion Companion;
   }
 
   public static final class IntentSenderRequest.Builder {
-    ctor public IntentSenderRequest.Builder(android.content.IntentSender);
-    ctor public IntentSenderRequest.Builder(android.app.PendingIntent);
+    ctor public IntentSenderRequest.Builder(android.content.IntentSender intentSender);
+    ctor public IntentSenderRequest.Builder(android.app.PendingIntent pendingIntent);
     method public androidx.activity.result.IntentSenderRequest build();
-    method public androidx.activity.result.IntentSenderRequest.Builder setFillInIntent(android.content.Intent?);
-    method public androidx.activity.result.IntentSenderRequest.Builder setFlags(int, int);
+    method public androidx.activity.result.IntentSenderRequest.Builder setFillInIntent(android.content.Intent? fillInIntent);
+    method public androidx.activity.result.IntentSenderRequest.Builder setFlags(int values, int mask);
+  }
+
+  public static final class IntentSenderRequest.Companion {
   }
 
   public final class PickVisualMediaRequest {
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/ComponentActivityOverrideLifecycleTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityOverrideLifecycleTest.kt
index 23758b0..a7769dc 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityOverrideLifecycleTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityOverrideLifecycleTest.kt
@@ -62,17 +62,15 @@
 
     private val overrideLifecycle = LifecycleRegistry(this)
 
-    override fun getLifecycle(): Lifecycle {
-        return overrideLifecycle
-    }
+    override val lifecycle: Lifecycle
+        get() = overrideLifecycle
 }
 
 class LazyOverrideLifecycleComponentActivity : ComponentActivity() {
     private var overrideLifecycle: LifecycleRegistry? = null
 
-    override fun getLifecycle(): Lifecycle {
-        return overrideLifecycle ?: LifecycleRegistry(this).also {
+    override val lifecycle: Lifecycle
+        get() = overrideLifecycle ?: LifecycleRegistry(this).also {
             overrideLifecycle = it
         }
-    }
 }
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/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
index 7380772..51cf3de 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
@@ -82,6 +82,34 @@
 
     @UiThreadTest
     @Test
+    fun testIsEnabledWithinCallback() {
+        var count = 0
+        val callback = dispatcher.addCallback {
+            count++
+            isEnabled = false
+        }
+        assertWithMessage("Callback should be enabled by default")
+            .that(callback.isEnabled)
+            .isTrue()
+        assertWithMessage("Dispatcher should have an enabled callback")
+            .that(dispatcher.hasEnabledCallbacks())
+            .isTrue()
+
+        dispatcher.onBackPressed()
+
+        assertWithMessage("Count should be incremented after onBackPressed")
+            .that(count)
+            .isEqualTo(1)
+        assertWithMessage("Callback should be disabled after onBackPressed()")
+            .that(callback.isEnabled)
+            .isFalse()
+        assertWithMessage("Dispatcher should have no enabled callbacks")
+            .that(dispatcher.hasEnabledCallbacks())
+            .isFalse()
+    }
+
+    @UiThreadTest
+    @Test
     fun testRemove() {
         val onBackPressedCallback = CountingOnBackPressedCallback()
 
@@ -148,6 +176,25 @@
 
     @UiThreadTest
     @Test
+    fun testRemoveWithinCallback() {
+        var count = 0
+        dispatcher.addCallback {
+            count++
+            remove()
+        }
+
+        dispatcher.onBackPressed()
+
+        assertWithMessage("Count should be incremented after onBackPressed")
+            .that(count)
+            .isEqualTo(1)
+        assertWithMessage("Dispatcher should have no enabled callbacks after remove")
+            .that(dispatcher.hasEnabledCallbacks())
+            .isFalse()
+    }
+
+    @UiThreadTest
+    @Test
     fun testMultipleCalls() {
         val onBackPressedCallback = CountingOnBackPressedCallback()
 
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ViewTreeOnBackPressedDispatcherTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ViewTreeOnBackPressedDispatcherTest.kt
index f78e6ea..16430d7 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ViewTreeOnBackPressedDispatcherTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ViewTreeOnBackPressedDispatcherTest.kt
@@ -115,12 +115,10 @@
     }
 
     private class FakeOnBackPressedDispatcherOwner : OnBackPressedDispatcherOwner {
-        override fun getLifecycle(): Lifecycle {
-            throw UnsupportedOperationException("not a real OnBackPressedDispatcherOwner")
-        }
+        override val lifecycle: Lifecycle
+            get() = throw UnsupportedOperationException("not a real OnBackPressedDispatcherOwner")
 
-        override fun getOnBackPressedDispatcher(): OnBackPressedDispatcher {
-            throw UnsupportedOperationException("not a real OnBackPressedDispatcherOwner")
-        }
+        override val onBackPressedDispatcher
+            get() = throw UnsupportedOperationException("not a real OnBackPressedDispatcherOwner")
     }
 }
\ No newline at end of file
diff --git a/activity/activity/src/main/java/androidx/activity/ComponentDialog.kt b/activity/activity/src/main/java/androidx/activity/ComponentDialog.kt
index b04c8f7..2a2059c 100644
--- a/activity/activity/src/main/java/androidx/activity/ComponentDialog.kt
+++ b/activity/activity/src/main/java/androidx/activity/ComponentDialog.kt
@@ -55,7 +55,8 @@
     override val savedStateRegistry: SavedStateRegistry
         get() = savedStateRegistryController.savedStateRegistry
 
-    final override fun getLifecycle(): Lifecycle = lifecycleRegistry
+    override val lifecycle: Lifecycle
+        get() = lifecycleRegistry
 
     override fun onSaveInstanceState(): Bundle {
         val bundle = super.onSaveInstanceState()
@@ -89,12 +90,10 @@
     }
 
     @Suppress("DEPRECATION")
-    private val onBackPressedDispatcher = OnBackPressedDispatcher {
+    final override val onBackPressedDispatcher = OnBackPressedDispatcher {
         super.onBackPressed()
     }
 
-    final override fun getOnBackPressedDispatcher() = onBackPressedDispatcher
-
     @CallSuper
     override fun onBackPressed() {
         onBackPressedDispatcher.onBackPressed()
diff --git a/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt b/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt
index 7174a2a..3101ef7 100644
--- a/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt
+++ b/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt
@@ -16,9 +16,6 @@
 package androidx.activity
 
 import androidx.annotation.MainThread
-import androidx.annotation.OptIn
-import androidx.core.os.BuildCompat
-import androidx.core.util.Consumer
 import java.util.concurrent.CopyOnWriteArrayList
 
 /**
@@ -39,7 +36,7 @@
  *
  * @param enabled The default enabled state for this callback.
  *
- * @see ComponentActivity.getOnBackPressedDispatcher
+ * @see OnBackPressedDispatcher
  */
 abstract class OnBackPressedCallback(enabled: Boolean) {
     /**
@@ -53,17 +50,14 @@
      */
     @get:MainThread
     @set:MainThread
-    @set:OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
     var isEnabled: Boolean = enabled
         set(value) {
             field = value
-            if (enabledConsumer != null) {
-                enabledConsumer!!.accept(field)
-            }
+            enabledChangedCallback?.invoke()
         }
 
     private val cancellables = CopyOnWriteArrayList<Cancellable>()
-    private var enabledConsumer: Consumer<Boolean>? = null
+    internal var enabledChangedCallback: (() -> Unit)? = null
 
     /**
      * Removes this callback from any [OnBackPressedDispatcher] it is currently
@@ -87,9 +81,4 @@
     internal fun removeCancellable(cancellable: Cancellable) {
         cancellables.remove(cancellable)
     }
-
-    @JvmName("setIsEnabledConsumer")
-    internal fun setIsEnabledConsumer(isEnabled: Consumer<Boolean>?) {
-        enabledConsumer = isEnabled
-    }
 }
diff --git a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.java b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.java
deleted file mode 100644
index 10c865e..0000000
--- a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.java
+++ /dev/null
@@ -1,347 +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.activity;
-
-import android.annotation.SuppressLint;
-import android.os.Build;
-import android.window.OnBackInvokedCallback;
-import android.window.OnBackInvokedDispatcher;
-
-import androidx.annotation.DoNotInline;
-import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.OptIn;
-import androidx.annotation.RequiresApi;
-import androidx.core.os.BuildCompat;
-import androidx.core.util.Consumer;
-import androidx.lifecycle.Lifecycle;
-import androidx.lifecycle.LifecycleEventObserver;
-import androidx.lifecycle.LifecycleOwner;
-
-import java.util.ArrayDeque;
-import java.util.Iterator;
-
-/**
- * Dispatcher that can be used to register {@link OnBackPressedCallback} instances for handling
- * the {@link ComponentActivity#onBackPressed()} callback via composition.
- * <pre>
- * public class FormEntryFragment extends Fragment {
- *     {@literal @}Override
- *     public void onAttach({@literal @}NonNull Context context) {
- *         super.onAttach(context);
- *         OnBackPressedCallback callback = new OnBackPressedCallback(
- *             true // default to enabled
- *         ) {
- *             {@literal @}Override
- *             public void handleOnBackPressed() {
- *                 showAreYouSureDialog();
- *             }
- *         };
- *         requireActivity().getOnBackPressedDispatcher().addCallback(
- *             this, // LifecycleOwner
- *             callback);
- *     }
- * }
- * </pre>
- */
-public final class OnBackPressedDispatcher {
-
-    @Nullable
-    private final Runnable mFallbackOnBackPressed;
-
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final ArrayDeque<OnBackPressedCallback> mOnBackPressedCallbacks = new ArrayDeque<>();
-
-    private Consumer<Boolean> mEnabledConsumer;
-
-    private OnBackInvokedCallback mOnBackInvokedCallback;
-    private OnBackInvokedDispatcher mInvokedDispatcher;
-    private boolean mBackInvokedCallbackRegistered = false;
-
-    /**
-     * Sets the {@link OnBackInvokedDispatcher} for handling system back for Android SDK T+.
-     *
-     * @param invoker the OnBackInvokedDispatcher to be set on this dispatcher
-     */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    public void setOnBackInvokedDispatcher(@NonNull OnBackInvokedDispatcher invoker) {
-        mInvokedDispatcher = invoker;
-        updateBackInvokedCallbackState();
-    }
-
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    void updateBackInvokedCallbackState() {
-        boolean shouldBeRegistered = hasEnabledCallbacks();
-        if (mInvokedDispatcher != null) {
-            if (shouldBeRegistered && !mBackInvokedCallbackRegistered) {
-                Api33Impl.registerOnBackInvokedCallback(
-                        mInvokedDispatcher,
-                        OnBackInvokedDispatcher.PRIORITY_DEFAULT,
-                        mOnBackInvokedCallback
-                );
-                mBackInvokedCallbackRegistered = true;
-            } else if (!shouldBeRegistered && mBackInvokedCallbackRegistered) {
-                Api33Impl.unregisterOnBackInvokedCallback(mInvokedDispatcher,
-                        mOnBackInvokedCallback);
-                mBackInvokedCallbackRegistered = false;
-            }
-        }
-    }
-
-    /**
-     * Create a new OnBackPressedDispatcher that dispatches System back button pressed events
-     * to one or more {@link OnBackPressedCallback} instances.
-     */
-    public OnBackPressedDispatcher() {
-        this(null);
-    }
-
-    /**
-     * Create a new OnBackPressedDispatcher that dispatches System back button pressed events
-     * to one or more {@link OnBackPressedCallback} instances.
-     *
-     * @param fallbackOnBackPressed The Runnable that should be triggered if
-     * {@link #onBackPressed()} is called when {@link #hasEnabledCallbacks()} returns false.
-     */
-    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
-    public OnBackPressedDispatcher(@Nullable Runnable fallbackOnBackPressed) {
-        mFallbackOnBackPressed = fallbackOnBackPressed;
-        if (BuildCompat.isAtLeastT()) {
-            mEnabledConsumer = aBoolean -> {
-                if (BuildCompat.isAtLeastT()) {
-                    updateBackInvokedCallbackState();
-                }
-            };
-            mOnBackInvokedCallback = Api33Impl.createOnBackInvokedCallback(this::onBackPressed);
-        }
-    }
-
-    /**
-     * Add a new {@link OnBackPressedCallback}. Callbacks are invoked in the reverse order in which
-     * they are added, so this newly added {@link OnBackPressedCallback} will be the first
-     * callback to receive a callback if {@link #onBackPressed()} is called.
-     * <p>
-     * This method is <strong>not</strong> {@link Lifecycle} aware - if you'd like to ensure that
-     * you only get callbacks when at least {@link Lifecycle.State#STARTED started}, use
-     * {@link #addCallback(LifecycleOwner, OnBackPressedCallback)}. It is expected that you
-     * call {@link OnBackPressedCallback#remove()} to manually remove your callback.
-     *
-     * @param onBackPressedCallback The callback to add
-     *
-     * @see #onBackPressed()
-     */
-    @MainThread
-    public void addCallback(@NonNull OnBackPressedCallback onBackPressedCallback) {
-        addCancellableCallback(onBackPressedCallback);
-    }
-
-    /**
-     * Internal implementation of {@link #addCallback(OnBackPressedCallback)} that gives
-     * access to the {@link Cancellable} that specifically removes this callback from
-     * the dispatcher without relying on {@link OnBackPressedCallback#remove()} which
-     * is what external developers should be using.
-     *
-     * @param onBackPressedCallback The callback to add
-     * @return a {@link Cancellable} which can be used to {@link Cancellable#cancel() cancel}
-     * the callback and remove it from the set of OnBackPressedCallbacks.
-     */
-    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    @MainThread
-    @NonNull
-    Cancellable addCancellableCallback(@NonNull OnBackPressedCallback onBackPressedCallback) {
-        mOnBackPressedCallbacks.add(onBackPressedCallback);
-        OnBackPressedCancellable cancellable = new OnBackPressedCancellable(onBackPressedCallback);
-        onBackPressedCallback.addCancellable(cancellable);
-        if (BuildCompat.isAtLeastT()) {
-            updateBackInvokedCallbackState();
-            onBackPressedCallback.setIsEnabledConsumer(mEnabledConsumer);
-        }
-        return cancellable;
-    }
-
-    /**
-     * Receive callbacks to a new {@link OnBackPressedCallback} when the given
-     * {@link LifecycleOwner} is at least {@link Lifecycle.State#STARTED started}.
-     * <p>
-     * This will automatically call {@link #addCallback(OnBackPressedCallback)} and
-     * remove the callback as the lifecycle state changes.
-     * As a corollary, if your lifecycle is already at least
-     * {@link Lifecycle.State#STARTED started}, calling this method will result in an immediate
-     * call to {@link #addCallback(OnBackPressedCallback)}.
-     * <p>
-     * When the {@link LifecycleOwner} is {@link Lifecycle.State#DESTROYED destroyed}, it will
-     * automatically be removed from the list of callbacks. The only time you would need to
-     * manually call {@link OnBackPressedCallback#remove()} is if
-     * you'd like to remove the callback prior to destruction of the associated lifecycle.
-     *
-     * <p>
-     * If the Lifecycle is already {@link Lifecycle.State#DESTROYED destroyed}
-     * when this method is called, the callback will not be added.
-     *
-     * @param owner The LifecycleOwner which controls when the callback should be invoked
-     * @param onBackPressedCallback The callback to add
-     *
-     * @see #onBackPressed()
-     */
-    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
-    @SuppressLint("LambdaLast")
-    @MainThread
-    public void addCallback(@NonNull LifecycleOwner owner,
-            @NonNull OnBackPressedCallback onBackPressedCallback) {
-        Lifecycle lifecycle = owner.getLifecycle();
-        if (lifecycle.getCurrentState() == Lifecycle.State.DESTROYED) {
-            return;
-        }
-
-        onBackPressedCallback.addCancellable(
-                new LifecycleOnBackPressedCancellable(lifecycle, onBackPressedCallback));
-        if (BuildCompat.isAtLeastT()) {
-            updateBackInvokedCallbackState();
-            onBackPressedCallback.setIsEnabledConsumer(mEnabledConsumer);
-        }
-    }
-
-    /**
-     * Checks if there is at least one {@link OnBackPressedCallback#isEnabled enabled}
-     * callback registered with this dispatcher.
-     *
-     * @return True if there is at least one enabled callback.
-     */
-    @MainThread
-    public boolean hasEnabledCallbacks() {
-        Iterator<OnBackPressedCallback> iterator =
-                mOnBackPressedCallbacks.descendingIterator();
-        while (iterator.hasNext()) {
-            if (iterator.next().isEnabled()) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Trigger a call to the currently added {@link OnBackPressedCallback callbacks} in reverse
-     * order in which they were added. Only if the most recently added callback is not
-     * {@link OnBackPressedCallback#isEnabled() enabled}
-     * will any previously added callback be called.
-     * <p>
-     * If {@link #hasEnabledCallbacks()} is <code>false</code> when this method is called, the
-     * fallback Runnable set by {@link #OnBackPressedDispatcher(Runnable) the constructor}
-     * will be triggered.
-     */
-    @MainThread
-    public void onBackPressed() {
-        Iterator<OnBackPressedCallback> iterator =
-                mOnBackPressedCallbacks.descendingIterator();
-        while (iterator.hasNext()) {
-            OnBackPressedCallback callback = iterator.next();
-            if (callback.isEnabled()) {
-                callback.handleOnBackPressed();
-                return;
-            }
-        }
-        if (mFallbackOnBackPressed != null) {
-            mFallbackOnBackPressed.run();
-        }
-    }
-
-    private class OnBackPressedCancellable implements Cancellable {
-        private final OnBackPressedCallback mOnBackPressedCallback;
-        OnBackPressedCancellable(OnBackPressedCallback onBackPressedCallback) {
-            mOnBackPressedCallback = onBackPressedCallback;
-        }
-
-        @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
-        @Override
-        public void cancel() {
-            mOnBackPressedCallbacks.remove(mOnBackPressedCallback);
-            mOnBackPressedCallback.removeCancellable(this);
-            if (BuildCompat.isAtLeastT()) {
-                mOnBackPressedCallback.setIsEnabledConsumer(null);
-                updateBackInvokedCallbackState();
-            }
-        }
-    }
-
-    private class LifecycleOnBackPressedCancellable implements LifecycleEventObserver,
-            Cancellable {
-        private final Lifecycle mLifecycle;
-        private final OnBackPressedCallback mOnBackPressedCallback;
-
-        @Nullable
-        private Cancellable mCurrentCancellable;
-
-        LifecycleOnBackPressedCancellable(@NonNull Lifecycle lifecycle,
-                @NonNull OnBackPressedCallback onBackPressedCallback) {
-            mLifecycle = lifecycle;
-            mOnBackPressedCallback = onBackPressedCallback;
-            lifecycle.addObserver(this);
-        }
-
-        @Override
-        public void onStateChanged(@NonNull LifecycleOwner source,
-                @NonNull Lifecycle.Event event) {
-            if (event == Lifecycle.Event.ON_START) {
-                mCurrentCancellable = addCancellableCallback(mOnBackPressedCallback);
-            } else if (event == Lifecycle.Event.ON_STOP) {
-                // Should always be non-null
-                if (mCurrentCancellable != null) {
-                    mCurrentCancellable.cancel();
-                }
-            } else if (event == Lifecycle.Event.ON_DESTROY) {
-                cancel();
-            }
-        }
-
-        @Override
-        public void cancel() {
-            mLifecycle.removeObserver(this);
-            mOnBackPressedCallback.removeCancellable(this);
-            if (mCurrentCancellable != null) {
-                mCurrentCancellable.cancel();
-                mCurrentCancellable = null;
-            }
-        }
-    }
-
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    static class Api33Impl {
-        private Api33Impl() { }
-
-        @DoNotInline
-        static void registerOnBackInvokedCallback(
-                Object dispatcher, int priority, Object callback
-        ) {
-            OnBackInvokedDispatcher onBackInvokedDispatcher = (OnBackInvokedDispatcher) dispatcher;
-            OnBackInvokedCallback onBackInvokedCallback = (OnBackInvokedCallback) callback;
-            onBackInvokedDispatcher.registerOnBackInvokedCallback(priority, onBackInvokedCallback);
-        }
-
-        @DoNotInline
-        static void unregisterOnBackInvokedCallback(Object dispatcher, Object callback) {
-            OnBackInvokedDispatcher onBackInvokedDispatcher = (OnBackInvokedDispatcher) dispatcher;
-            OnBackInvokedCallback onBackInvokedCallback = (OnBackInvokedCallback) callback;
-            onBackInvokedDispatcher.unregisterOnBackInvokedCallback(onBackInvokedCallback);
-        }
-        @DoNotInline
-        static OnBackInvokedCallback createOnBackInvokedCallback(Runnable runnable) {
-            return runnable::run;
-        }
-    }
-}
diff --git a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.kt b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.kt
new file mode 100644
index 0000000..5319d9e
--- /dev/null
+++ b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.kt
@@ -0,0 +1,317 @@
+/*
+ * 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.activity
+
+import android.os.Build
+import android.window.OnBackInvokedCallback
+import android.window.OnBackInvokedDispatcher
+import androidx.annotation.DoNotInline
+import androidx.annotation.MainThread
+import androidx.annotation.RequiresApi
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
+
+/**
+ * Dispatcher that can be used to register [OnBackPressedCallback] instances for handling
+ * the [ComponentActivity.onBackPressed] callback via composition.
+ *
+ * ```
+ * class FormEntryFragment : Fragment() {
+ *   override fun onAttach(context: Context) {
+ *     super.onAttach(context)
+ *     val callback = object : OnBackPressedCallback(
+ *       true // default to enabled
+ *     ) {
+ *       override fun handleOnBackPressed() {
+ *         showAreYouSureDialog()
+ *       }
+ *     }
+ *     requireActivity().onBackPressedDispatcher.addCallback(
+ *       this, // LifecycleOwner
+ *       callback
+ *     )
+ *   }
+ * }
+ * ```
+ *
+ * When constructing an instance of this class, the [fallbackOnBackPressed] can be set to
+ * receive a callback if [onBackPressed] is called when [hasEnabledCallbacks] returns `false`.
+ */
+class OnBackPressedDispatcher @JvmOverloads constructor(
+    private val fallbackOnBackPressed: Runnable? = null
+) {
+    private val onBackPressedCallbacks = ArrayDeque<OnBackPressedCallback>()
+    private var enabledChangedCallback: (() -> Unit)? = null
+    private var onBackInvokedCallback: OnBackInvokedCallback? = null
+    private var invokedDispatcher: OnBackInvokedDispatcher? = null
+    private var backInvokedCallbackRegistered = false
+
+    /**
+     * Sets the [OnBackInvokedDispatcher] for handling system back for Android SDK T+.
+     *
+     * @param invoker the OnBackInvokedDispatcher to be set on this dispatcher
+     */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    fun setOnBackInvokedDispatcher(invoker: OnBackInvokedDispatcher) {
+        invokedDispatcher = invoker
+        updateBackInvokedCallbackState()
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    internal fun updateBackInvokedCallbackState() {
+        val shouldBeRegistered = hasEnabledCallbacks()
+        val dispatcher = invokedDispatcher
+        val onBackInvokedCallback = onBackInvokedCallback
+        if (dispatcher != null && onBackInvokedCallback != null) {
+            if (shouldBeRegistered && !backInvokedCallbackRegistered) {
+                Api33Impl.registerOnBackInvokedCallback(
+                    dispatcher,
+                    OnBackInvokedDispatcher.PRIORITY_DEFAULT,
+                    onBackInvokedCallback
+                )
+                backInvokedCallbackRegistered = true
+            } else if (!shouldBeRegistered && backInvokedCallbackRegistered) {
+                Api33Impl.unregisterOnBackInvokedCallback(
+                    dispatcher,
+                    onBackInvokedCallback
+                )
+                backInvokedCallbackRegistered = false
+            }
+        }
+    }
+
+    init {
+        if (Build.VERSION.SDK_INT >= 33) {
+            enabledChangedCallback = {
+                updateBackInvokedCallbackState()
+            }
+            onBackInvokedCallback = Api33Impl.createOnBackInvokedCallback { onBackPressed() }
+        }
+    }
+
+    /**
+     * Add a new [OnBackPressedCallback]. Callbacks are invoked in the reverse order in which
+     * they are added, so this newly added [OnBackPressedCallback] will be the first
+     * callback to receive a callback if [onBackPressed] is called.
+     *
+     * This method is **not** [Lifecycle] aware - if you'd like to ensure that
+     * you only get callbacks when at least [started][Lifecycle.State.STARTED], use
+     * [addCallback]. It is expected that you
+     * call [OnBackPressedCallback.remove] to manually remove your callback.
+     *
+     * @param onBackPressedCallback The callback to add
+     *
+     * @see onBackPressed
+     */
+    @MainThread
+    fun addCallback(onBackPressedCallback: OnBackPressedCallback) {
+        addCancellableCallback(onBackPressedCallback)
+    }
+
+    /**
+     * Internal implementation of [addCallback] that gives
+     * access to the [Cancellable] that specifically removes this callback from
+     * the dispatcher without relying on [OnBackPressedCallback.remove] which
+     * is what external developers should be using.
+     *
+     * @param onBackPressedCallback The callback to add
+     * @return a [Cancellable] which can be used to [cancel][Cancellable.cancel]
+     * the callback and remove it from the set of OnBackPressedCallbacks.
+     */
+    @MainThread
+    internal fun addCancellableCallback(onBackPressedCallback: OnBackPressedCallback): Cancellable {
+        onBackPressedCallbacks.add(onBackPressedCallback)
+        val cancellable = OnBackPressedCancellable(onBackPressedCallback)
+        onBackPressedCallback.addCancellable(cancellable)
+        if (Build.VERSION.SDK_INT >= 33) {
+            updateBackInvokedCallbackState()
+            onBackPressedCallback.enabledChangedCallback = enabledChangedCallback
+        }
+        return cancellable
+    }
+
+    /**
+     * Receive callbacks to a new [OnBackPressedCallback] when the given
+     * [LifecycleOwner] is at least [started][Lifecycle.State.STARTED].
+     *
+     * This will automatically call [addCallback] and remove the callback as the lifecycle
+     * state changes. As a corollary, if your lifecycle is already at least
+     * [started][Lifecycle.State.STARTED], calling this method will result in an immediate
+     * call to [addCallback].
+     *
+     * When the [LifecycleOwner] is [destroyed][Lifecycle.State.DESTROYED], it will
+     * automatically be removed from the list of callbacks. The only time you would need to
+     * manually call [OnBackPressedCallback.remove] is if
+     * you'd like to remove the callback prior to destruction of the associated lifecycle.
+     *
+     * If the Lifecycle is already [destroyed][Lifecycle.State.DESTROYED]
+     * when this method is called, the callback will not be added.
+     *
+     * @param owner The LifecycleOwner which controls when the callback should be invoked
+     * @param onBackPressedCallback The callback to add
+     *
+     * @see onBackPressed
+     */
+    @MainThread
+    fun addCallback(
+        owner: LifecycleOwner,
+        onBackPressedCallback: OnBackPressedCallback
+    ) {
+        val lifecycle = owner.lifecycle
+        if (lifecycle.currentState === Lifecycle.State.DESTROYED) {
+            return
+        }
+        onBackPressedCallback.addCancellable(
+            LifecycleOnBackPressedCancellable(lifecycle, onBackPressedCallback)
+        )
+        if (Build.VERSION.SDK_INT >= 33) {
+            updateBackInvokedCallbackState()
+            onBackPressedCallback.enabledChangedCallback = enabledChangedCallback
+        }
+    }
+
+    /**
+     * Checks if there is at least one [enabled][OnBackPressedCallback.isEnabled]
+     * callback registered with this dispatcher.
+     *
+     * @return True if there is at least one enabled callback.
+     */
+    @MainThread
+    fun hasEnabledCallbacks(): Boolean = onBackPressedCallbacks.any {
+        it.isEnabled
+    }
+
+    /**
+     * Trigger a call to the currently added [callbacks][OnBackPressedCallback] in reverse
+     * order in which they were added. Only if the most recently added callback is not
+     * [enabled][OnBackPressedCallback.isEnabled]
+     * will any previously added callback be called.
+     *
+     * If [hasEnabledCallbacks] is `false` when this method is called, the
+     * [fallbackOnBackPressed] set by the constructor will be triggered.
+     */
+    @MainThread
+    fun onBackPressed() {
+        val callback = onBackPressedCallbacks.lastOrNull {
+            it.isEnabled
+        }
+        if (callback != null) {
+            callback.handleOnBackPressed()
+            return
+        }
+        fallbackOnBackPressed?.run()
+    }
+
+    private inner class OnBackPressedCancellable(
+        private val onBackPressedCallback: OnBackPressedCallback
+    ) : Cancellable {
+        override fun cancel() {
+            onBackPressedCallbacks.remove(onBackPressedCallback)
+            onBackPressedCallback.removeCancellable(this)
+            if (Build.VERSION.SDK_INT >= 33) {
+                onBackPressedCallback.enabledChangedCallback = null
+                updateBackInvokedCallbackState()
+            }
+        }
+    }
+
+    private inner class LifecycleOnBackPressedCancellable(
+        private val lifecycle: Lifecycle,
+        private val onBackPressedCallback: OnBackPressedCallback
+    ) : LifecycleEventObserver, Cancellable {
+        private var currentCancellable: Cancellable? = null
+
+        init {
+            lifecycle.addObserver(this)
+        }
+
+        override fun onStateChanged(
+            source: LifecycleOwner,
+            event: Lifecycle.Event
+        ) {
+            if (event === Lifecycle.Event.ON_START) {
+                currentCancellable = addCancellableCallback(onBackPressedCallback)
+            } else if (event === Lifecycle.Event.ON_STOP) {
+                // Should always be non-null
+                currentCancellable?.cancel()
+            } else if (event === Lifecycle.Event.ON_DESTROY) {
+                cancel()
+            }
+        }
+
+        override fun cancel() {
+            lifecycle.removeObserver(this)
+            onBackPressedCallback.removeCancellable(this)
+            currentCancellable?.cancel()
+            currentCancellable = null
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    internal object Api33Impl {
+        @DoNotInline
+        fun registerOnBackInvokedCallback(
+            dispatcher: Any,
+            priority: Int,
+            callback: Any
+        ) {
+            val onBackInvokedDispatcher = dispatcher as OnBackInvokedDispatcher
+            val onBackInvokedCallback = callback as OnBackInvokedCallback
+            onBackInvokedDispatcher.registerOnBackInvokedCallback(priority, onBackInvokedCallback)
+        }
+
+        @DoNotInline
+        fun unregisterOnBackInvokedCallback(dispatcher: Any, callback: Any) {
+            val onBackInvokedDispatcher = dispatcher as OnBackInvokedDispatcher
+            val onBackInvokedCallback = callback as OnBackInvokedCallback
+            onBackInvokedDispatcher.unregisterOnBackInvokedCallback(onBackInvokedCallback)
+        }
+
+        @DoNotInline
+        fun createOnBackInvokedCallback(onBackInvoked: () -> Unit): OnBackInvokedCallback {
+            return OnBackInvokedCallback { onBackInvoked() }
+        }
+    }
+}
+
+/**
+ * Create and add a new [OnBackPressedCallback] that calls [onBackPressed] in
+ * [OnBackPressedCallback.handleOnBackPressed].
+ *
+ * If an [owner] is specified, the callback will only be added when the Lifecycle is
+ * [androidx.lifecycle.Lifecycle.State.STARTED].
+ *
+ * A default [enabled] state can be supplied.
+ */
+@Suppress("RegistrationName")
+fun OnBackPressedDispatcher.addCallback(
+    owner: LifecycleOwner? = null,
+    enabled: Boolean = true,
+    onBackPressed: OnBackPressedCallback.() -> Unit
+): OnBackPressedCallback {
+    val callback = object : OnBackPressedCallback(enabled) {
+        override fun handleOnBackPressed() {
+            onBackPressed()
+        }
+    }
+    if (owner != null) {
+        addCallback(owner, callback)
+    } else {
+        addCallback(callback)
+    }
+    return callback
+}
\ No newline at end of file
diff --git a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcherOwner.java b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcherOwner.java
deleted file mode 100644
index e07b3f9..0000000
--- a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcherOwner.java
+++ /dev/null
@@ -1,40 +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.activity;
-
-import androidx.annotation.NonNull;
-import androidx.lifecycle.LifecycleOwner;
-
-/**
- * A class that has an {@link OnBackPressedDispatcher} that allows you to register a
- * {@link OnBackPressedCallback} for handling the system back button.
- * <p>
- * It is expected that classes that implement this interface route the system back button
- * to the dispatcher
- *
- * @see OnBackPressedDispatcher
- */
-public interface OnBackPressedDispatcherOwner extends LifecycleOwner {
-
-    /**
-     * Retrieve the {@link OnBackPressedDispatcher} that should handle the system back button.
-     *
-     * @return The {@link OnBackPressedDispatcher}.
-     */
-    @NonNull
-    OnBackPressedDispatcher getOnBackPressedDispatcher();
-}
diff --git a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcherOwner.kt b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcherOwner.kt
new file mode 100644
index 0000000..21401c7
--- /dev/null
+++ b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcherOwner.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.activity
+
+import androidx.lifecycle.LifecycleOwner
+
+/**
+ * A class that has an [OnBackPressedDispatcher] that allows you to register a
+ * [OnBackPressedCallback] for handling the system back button.
+ *
+ * It is expected that classes that implement this interface route the system back button
+ * to the dispatcher
+ *
+ * @see OnBackPressedDispatcher
+ */
+interface OnBackPressedDispatcherOwner : LifecycleOwner {
+    /**
+     * The [OnBackPressedDispatcher] that should handle the system back button.
+     */
+    val onBackPressedDispatcher: OnBackPressedDispatcher
+}
\ No newline at end of file
diff --git a/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistryOwner.java b/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistryOwner.java
deleted file mode 100644
index c6ba684..0000000
--- a/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistryOwner.java
+++ /dev/null
@@ -1,42 +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.activity.result;
-
-import androidx.activity.result.contract.ActivityResultContract;
-import androidx.annotation.NonNull;
-
-/**
- * A class that has an {@link ActivityResultRegistry} that allows you to register a
- * {@link ActivityResultCallback} for handling an
- * {@link androidx.activity.result.contract.ActivityResultContract}.
- *
- * If it is not safe to call
- * {@link ActivityResultRegistry#register(String, ActivityResultContract, ActivityResultCallback)}
- * in the constructor, it is strongly recommended to also implement {@link ActivityResultCaller}.
- *
- * @see ActivityResultRegistry
- */
-public interface ActivityResultRegistryOwner {
-
-    /**
-     * Returns the ActivityResultRegistry of the provider.
-     *
-     * @return The activity result registry of the provider.
-     */
-    @NonNull
-    ActivityResultRegistry getActivityResultRegistry();
-}
diff --git a/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistryOwner.kt b/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistryOwner.kt
new file mode 100644
index 0000000..9ce51d1
--- /dev/null
+++ b/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistryOwner.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.activity.result
+
+/**
+ * A class that has an [ActivityResultRegistry] that allows you to register a
+ * [ActivityResultCallback] for handling an
+ * [androidx.activity.result.contract.ActivityResultContract].
+ *
+ * If it is not safe to call [ActivityResultRegistry.register]
+ * in the constructor, it is strongly recommended to also implement [ActivityResultCaller].
+ *
+ * @see ActivityResultRegistry
+ */
+interface ActivityResultRegistryOwner {
+    /**
+     * Returns the ActivityResultRegistry of the provider.
+     *
+     * @return The activity result registry of the provider.
+     */
+    val activityResultRegistry: ActivityResultRegistry
+}
\ No newline at end of file
diff --git a/activity/activity/src/main/java/androidx/activity/result/IntentSenderRequest.java b/activity/activity/src/main/java/androidx/activity/result/IntentSenderRequest.java
deleted file mode 100644
index cf43d82..0000000
--- a/activity/activity/src/main/java/androidx/activity/result/IntentSenderRequest.java
+++ /dev/null
@@ -1,242 +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.activity.result;
-
-import static android.content.Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT;
-import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
-import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
-import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
-import static android.content.Intent.FLAG_ACTIVITY_FORWARD_RESULT;
-import static android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY;
-import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
-import static android.content.Intent.FLAG_ACTIVITY_MATCH_EXTERNAL;
-import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
-import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
-import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
-import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
-import static android.content.Intent.FLAG_ACTIVITY_NO_HISTORY;
-import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
-import static android.content.Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP;
-import static android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT;
-import static android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED;
-import static android.content.Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS;
-import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
-import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME;
-import static android.content.Intent.FLAG_DEBUG_LOG_RESOLUTION;
-import static android.content.Intent.FLAG_EXCLUDE_STOPPED_PACKAGES;
-import static android.content.Intent.FLAG_FROM_BACKGROUND;
-import static android.content.Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION;
-import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
-import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
-import static android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
-import static android.content.Intent.FLAG_INCLUDE_STOPPED_PACKAGES;
-
-import android.annotation.SuppressLint;
-import android.app.PendingIntent;
-import android.content.Intent;
-import android.content.IntentSender;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * A request for a
- * {@link androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult}
- * Activity Contract.
- */
-@SuppressLint("BanParcelableUsage")
-public final class IntentSenderRequest implements Parcelable {
-    @NonNull
-    private final IntentSender mIntentSender;
-    @Nullable
-    private final Intent mFillInIntent;
-    private final int mFlagsMask;
-    private final int mFlagsValues;
-
-    IntentSenderRequest(@NonNull IntentSender intentSender, @Nullable Intent intent, int flagsMask,
-            int flagsValues) {
-        mIntentSender = intentSender;
-        mFillInIntent = intent;
-        mFlagsMask = flagsMask;
-        mFlagsValues = flagsValues;
-    }
-
-    /**
-     * Get the intentSender from this IntentSenderRequest.
-     *
-     * @return the IntentSender to launch.
-     */
-    @NonNull
-    public IntentSender getIntentSender() {
-        return mIntentSender;
-    }
-
-    /**
-     * Get the intent from this IntentSender request.  If non-null, this will be provided as the
-     * intent parameter to IntentSender#sendIntent.
-     *
-     * @return the fill in intent.
-     */
-    @Nullable
-    public Intent getFillInIntent() {
-        return mFillInIntent;
-    }
-
-    /**
-     * Get the flag mask from this IntentSender request.
-     *
-     * @return intent flags in the original IntentSender that you would like to change.
-     */
-    public int getFlagsMask() {
-        return mFlagsMask;
-    }
-
-    /**
-     * Get the flag values from this IntentSender request.
-     *
-     * @return desired values for any bits set in flagsMask
-     */
-    public int getFlagsValues() {
-        return mFlagsValues;
-    }
-
-    @SuppressWarnings({"ConstantConditions", "deprecation"})
-    IntentSenderRequest(@NonNull Parcel in) {
-        mIntentSender = in.readParcelable(IntentSender.class.getClassLoader());
-        mFillInIntent = in.readParcelable(Intent.class.getClassLoader());
-        mFlagsMask = in.readInt();
-        mFlagsValues = in.readInt();
-    }
-
-    @NonNull
-    public static final Creator<IntentSenderRequest> CREATOR = new Creator<IntentSenderRequest>() {
-        @Override
-        public IntentSenderRequest createFromParcel(Parcel in) {
-            return new IntentSenderRequest(in);
-        }
-
-        @Override
-        public IntentSenderRequest[] newArray(int size) {
-            return new IntentSenderRequest[size];
-        }
-    };
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeParcelable(mIntentSender, flags);
-        dest.writeParcelable(mFillInIntent, flags);
-        dest.writeInt(mFlagsMask);
-        dest.writeInt(mFlagsValues);
-    }
-
-    /**
-     * A builder for constructing {@link IntentSenderRequest} instances.
-     */
-    public static final class Builder {
-        private IntentSender mIntentSender;
-        private Intent mFillInIntent;
-        private int mFlagsMask;
-        private int mFlagsValues;
-
-        @IntDef({FLAG_GRANT_READ_URI_PERMISSION, FLAG_GRANT_WRITE_URI_PERMISSION,
-                FLAG_FROM_BACKGROUND, FLAG_DEBUG_LOG_RESOLUTION, FLAG_EXCLUDE_STOPPED_PACKAGES,
-                FLAG_INCLUDE_STOPPED_PACKAGES, FLAG_GRANT_PERSISTABLE_URI_PERMISSION,
-                FLAG_GRANT_PREFIX_URI_PERMISSION, FLAG_ACTIVITY_MATCH_EXTERNAL,
-                FLAG_ACTIVITY_NO_HISTORY, FLAG_ACTIVITY_SINGLE_TOP, FLAG_ACTIVITY_NEW_TASK,
-                FLAG_ACTIVITY_MULTIPLE_TASK, FLAG_ACTIVITY_CLEAR_TOP,
-                FLAG_ACTIVITY_FORWARD_RESULT, FLAG_ACTIVITY_PREVIOUS_IS_TOP,
-                FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS, FLAG_ACTIVITY_BROUGHT_TO_FRONT,
-                FLAG_ACTIVITY_RESET_TASK_IF_NEEDED, FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY,
-                FLAG_ACTIVITY_NEW_DOCUMENT, FLAG_ACTIVITY_NO_USER_ACTION,
-                FLAG_ACTIVITY_REORDER_TO_FRONT, FLAG_ACTIVITY_NO_ANIMATION,
-                FLAG_ACTIVITY_CLEAR_TASK, FLAG_ACTIVITY_TASK_ON_HOME,
-                FLAG_ACTIVITY_RETAIN_IN_RECENTS, FLAG_ACTIVITY_LAUNCH_ADJACENT})
-        @Retention(RetentionPolicy.SOURCE)
-        private @interface Flag {}
-
-        /**
-         * Constructor that takes an {@link IntentSender} and sets it for the builder.
-         *
-         * @param intentSender IntentSender to go in the IntentSenderRequest.
-         */
-        public Builder(@NonNull IntentSender intentSender) {
-            mIntentSender = intentSender;
-        }
-
-        /**
-         * Convenience constructor that takes an {@link PendingIntent} and uses
-         * its {@link IntentSender}.
-         *
-         * @param pendingIntent the pendingIntent containing with the intentSender to go in the
-         *                      IntentSenderRequest.
-         */
-        public Builder(@NonNull PendingIntent pendingIntent) {
-            this(pendingIntent.getIntentSender());
-        }
-
-        /**
-         * Set the intent for the {@link IntentSenderRequest}.
-         *
-         * @param fillInIntent intent to go in the IntentSenderRequest. If non-null, this
-         *                     will be provided as the intent parameter to IntentSender#sendIntent.
-         * @return This builder.
-         */
-        @NonNull
-        public Builder setFillInIntent(@Nullable Intent fillInIntent) {
-            mFillInIntent = fillInIntent;
-            return this;
-        }
-
-        /**
-         * Set the flag mask and flag values for the {@link IntentSenderRequest}.
-         *
-         * @param values flagValues to go in the IntentSenderRequest. Desired values for any bits
-         *             set in flagsMask
-         * @param mask mask to go in the IntentSenderRequest. Intent flags in the original
-         *             IntentSender that you would like to change.
-         *
-         * @return This builder.
-         */
-        @NonNull
-        public Builder setFlags(@Flag int values, int mask) {
-            mFlagsValues = values;
-            mFlagsMask = mask;
-            return this;
-        }
-
-        /**
-         * Build the IntentSenderRequest specified by this builder.
-         *
-         * @return the newly constructed IntentSenderRequest.
-         */
-        @NonNull
-        public IntentSenderRequest build() {
-            return new IntentSenderRequest(mIntentSender, mFillInIntent, mFlagsMask, mFlagsValues);
-        }
-    }
-}
diff --git a/activity/activity/src/main/java/androidx/activity/result/IntentSenderRequest.kt b/activity/activity/src/main/java/androidx/activity/result/IntentSenderRequest.kt
new file mode 100644
index 0000000..7da957a
--- /dev/null
+++ b/activity/activity/src/main/java/androidx/activity/result/IntentSenderRequest.kt
@@ -0,0 +1,176 @@
+/*
+ * 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.activity.result
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.content.Intent
+import android.content.IntentSender
+import android.os.Parcel
+import android.os.Parcelable
+import androidx.annotation.IntDef
+
+/**
+ * A request for a
+ * [androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult]
+ * Activity Contract.
+ */
+@SuppressLint("BanParcelableUsage")
+class IntentSenderRequest internal constructor(
+    /**
+     * The intentSender from this IntentSenderRequest.
+     */
+    val intentSender: IntentSender,
+    /**
+     * The intent from this IntentSender request. If non-null, this will be provided as the
+     * intent parameter to IntentSender#sendIntent.
+     */
+    val fillInIntent: Intent? = null,
+    /**
+     * The flag mask from this IntentSender request.
+     */
+    val flagsMask: Int = 0,
+    /**
+     * The flag values from this IntentSender request.
+     */
+    val flagsValues: Int = 0,
+) : Parcelable {
+
+    @Suppress("DEPRECATION")
+    internal constructor(parcel: Parcel) : this(
+        parcel.readParcelable(IntentSender::class.java.classLoader)!!,
+        parcel.readParcelable(Intent::class.java.classLoader),
+        parcel.readInt(),
+        parcel.readInt()
+    )
+
+    override fun describeContents(): Int {
+        return 0
+    }
+
+    override fun writeToParcel(dest: Parcel, flags: Int) {
+        dest.writeParcelable(intentSender, flags)
+        dest.writeParcelable(fillInIntent, flags)
+        dest.writeInt(flagsMask)
+        dest.writeInt(flagsValues)
+    }
+
+    /**
+     * A builder for constructing [IntentSenderRequest] instances.
+     */
+    class Builder(private val intentSender: IntentSender) {
+        private var fillInIntent: Intent? = null
+        private var flagsMask = 0
+        private var flagsValues = 0
+
+        /**
+         * Convenience constructor that takes an [PendingIntent] and uses
+         * its [IntentSender].
+         *
+         * @param pendingIntent the pendingIntent containing with the intentSender to go in the
+         * IntentSenderRequest.
+         */
+        constructor(pendingIntent: PendingIntent) : this(pendingIntent.intentSender)
+
+        @IntDef(
+            flag = true,
+            value = [
+                Intent.FLAG_GRANT_READ_URI_PERMISSION,
+                Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
+                Intent.FLAG_FROM_BACKGROUND,
+                Intent.FLAG_DEBUG_LOG_RESOLUTION,
+                Intent.FLAG_EXCLUDE_STOPPED_PACKAGES,
+                Intent.FLAG_INCLUDE_STOPPED_PACKAGES,
+                Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION,
+                Intent.FLAG_GRANT_PREFIX_URI_PERMISSION,
+                Intent.FLAG_ACTIVITY_MATCH_EXTERNAL,
+                Intent.FLAG_ACTIVITY_NO_HISTORY,
+                Intent.FLAG_ACTIVITY_SINGLE_TOP,
+                Intent.FLAG_ACTIVITY_NEW_TASK,
+                Intent.FLAG_ACTIVITY_MULTIPLE_TASK,
+                Intent.FLAG_ACTIVITY_CLEAR_TOP,
+                Intent.FLAG_ACTIVITY_FORWARD_RESULT,
+                Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP,
+                Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS,
+                Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT,
+                Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED,
+                Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY,
+                Intent.FLAG_ACTIVITY_NEW_DOCUMENT,
+                Intent.FLAG_ACTIVITY_NO_USER_ACTION,
+                Intent.FLAG_ACTIVITY_REORDER_TO_FRONT,
+                Intent.FLAG_ACTIVITY_NO_ANIMATION,
+                Intent.FLAG_ACTIVITY_CLEAR_TASK,
+                Intent.FLAG_ACTIVITY_TASK_ON_HOME,
+                Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS,
+                Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT
+            ]
+        )
+        @Retention(AnnotationRetention.SOURCE)
+        private annotation class Flag
+
+        /**
+         * Set the intent for the [IntentSenderRequest].
+         *
+         * @param fillInIntent intent to go in the IntentSenderRequest. If non-null, this
+         * will be provided as the intent parameter to IntentSender#sendIntent.
+         * @return This builder.
+         */
+        fun setFillInIntent(fillInIntent: Intent?): Builder {
+            this.fillInIntent = fillInIntent
+            return this
+        }
+
+        /**
+         * Set the flag mask and flag values for the [IntentSenderRequest].
+         *
+         * @param values flagValues to go in the IntentSenderRequest. Desired values for any bits
+         * set in flagsMask
+         * @param mask mask to go in the IntentSenderRequest. Intent flags in the original
+         * IntentSender that you would like to change.
+         *
+         * @return This builder.
+         */
+        fun setFlags(@Flag values: Int, mask: Int): Builder {
+            flagsValues = values
+            flagsMask = mask
+            return this
+        }
+
+        /**
+         * Build the IntentSenderRequest specified by this builder.
+         *
+         * @return the newly constructed IntentSenderRequest.
+         */
+        fun build(): IntentSenderRequest {
+            return IntentSenderRequest(intentSender, fillInIntent, flagsMask, flagsValues)
+        }
+    }
+
+    companion object {
+        @Suppress("unused")
+        @JvmField
+        val CREATOR: Parcelable.Creator<IntentSenderRequest> =
+            object : Parcelable.Creator<IntentSenderRequest> {
+                override fun createFromParcel(inParcel: Parcel): IntentSenderRequest {
+                    return IntentSenderRequest(inParcel)
+                }
+
+                override fun newArray(size: Int): Array<IntentSenderRequest?> {
+                    return arrayOfNulls(size)
+                }
+            }
+    }
+}
\ No newline at end of file
diff --git a/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/MainActivity.kt b/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/MainActivity.kt
index 947be53..d795756 100644
--- a/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/MainActivity.kt
+++ b/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/MainActivity.kt
@@ -172,10 +172,13 @@
                     val request = IntentSenderRequest.Builder(
                         PendingIntent.getActivity(
                             context,
-                            0, Intent(MediaStore.ACTION_IMAGE_CAPTURE), 0
-                        ).intentSender
+                            0,
+                            Intent(MediaStore.ACTION_IMAGE_CAPTURE),
+                            PendingIntent.FLAG_IMMUTABLE
+                        )
                     )
-                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK, 1)
+                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP,
+                            1)
                         .build()
                     intentSender.launch(request)
                 }
diff --git a/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/experimental/UseKtExperimentalFromJava.java b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/experimental/UseKtExperimentalFromJava.java
index 6363203..0a06ea2 100644
--- a/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/experimental/UseKtExperimentalFromJava.java
+++ b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/experimental/UseKtExperimentalFromJava.java
@@ -100,8 +100,4 @@
     @SuppressWarnings("deprecation")
     @androidx.annotation.experimental.UseExperimental(markerClass = ExperimentalDateTimeKt.class)
     static class FancyDateProvider extends DateProviderKt {}
-
-    @SuppressWarnings("deprecation")
-    @kotlin.UseExperimental(markerClass = ExperimentalDateTimeKt.class)
-    static class FancyDateProvider2 extends DateProviderKt {}
 }
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/annotation/annotation/api/1.6.0-beta01.txt b/annotation/annotation/api/1.6.0-beta01.txt
new file mode 100644
index 0000000..660800c
--- /dev/null
+++ b/annotation/annotation/api/1.6.0-beta01.txt
@@ -0,0 +1,369 @@
+// Signature format: 4.0
+package androidx.annotation {
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnimRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnimatorRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnyRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface AnyThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface ArrayRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AttrRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface BinderThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface BoolRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface CallSuper {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface CheckResult {
+    method public abstract String suggest() default "";
+    property public abstract String suggest;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ChecksSdkIntAtLeast {
+    method public abstract int api() default -1;
+    method public abstract String codename() default "";
+    method public abstract int extension() default 0;
+    method public abstract int lambda() default -1;
+    method public abstract int parameter() default -1;
+    property public abstract int api;
+    property public abstract String codename;
+    property public abstract int extension;
+    property public abstract int lambda;
+    property public abstract int parameter;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ColorInt {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ColorLong {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface ColorRes {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CONSTRUCTOR) public @interface ContentView {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface DeprecatedSinceApi {
+    method public abstract int api();
+    method public abstract String message() default "";
+    property public abstract int api;
+    property public abstract String message;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface DimenRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface Dimension {
+    method public abstract int unit() default androidx.annotation.Dimension.PX;
+    property public abstract int unit;
+    field public static final androidx.annotation.Dimension.Companion Companion;
+    field public static final int DP = 0; // 0x0
+    field public static final int PX = 1; // 0x1
+    field public static final int SP = 2; // 0x2
+  }
+
+  public static final class Dimension.Companion {
+    field public static final int DP = 0; // 0x0
+    field public static final int PX = 1; // 0x1
+    field public static final int SP = 2; // 0x2
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public @interface Discouraged {
+    method public abstract String message();
+    property public abstract String message;
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface DisplayContext {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface DoNotInline {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface DrawableRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface EmptySuper {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface FloatRange {
+    method public abstract double from() default kotlin.jvm.internal.DoubleCompanionObject.NEGATIVE_INFINITY;
+    method public abstract boolean fromInclusive() default true;
+    method public abstract double to() default kotlin.jvm.internal.DoubleCompanionObject.POSITIVE_INFINITY;
+    method public abstract boolean toInclusive() default true;
+    property public abstract double from;
+    property public abstract boolean fromInclusive;
+    property public abstract double to;
+    property public abstract boolean toInclusive;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface FontRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface FractionRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface GravityInt {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface GuardedBy {
+    method public abstract String value();
+    property public abstract String value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface HalfFloat {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface IdRes {
+  }
+
+  @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InspectableProperty {
+    method @Deprecated public abstract int attributeId() default 0;
+    method @Deprecated public abstract androidx.annotation.InspectableProperty.EnumEntry[] enumMapping();
+    method @Deprecated public abstract androidx.annotation.InspectableProperty.FlagEntry[] flagMapping();
+    method @Deprecated public abstract boolean hasAttributeId() default true;
+    method @Deprecated public abstract String name() default "";
+    method @Deprecated public abstract androidx.annotation.InspectableProperty.ValueType valueType() default androidx.annotation.InspectableProperty.ValueType.INFERRED;
+    property public abstract int attributeId;
+    property public abstract androidx.annotation.InspectableProperty.EnumEntry[] enumMapping;
+    property public abstract androidx.annotation.InspectableProperty.FlagEntry[] flagMapping;
+    property public abstract boolean hasAttributeId;
+    property public abstract String name;
+    property public abstract androidx.annotation.InspectableProperty.ValueType valueType;
+  }
+
+  @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public static @interface InspectableProperty.EnumEntry {
+    method @Deprecated public abstract String name();
+    method @Deprecated public abstract int value();
+    property public abstract String name;
+    property public abstract int value;
+  }
+
+  @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public static @interface InspectableProperty.FlagEntry {
+    method @Deprecated public abstract int mask() default 0;
+    method @Deprecated public abstract String name();
+    method @Deprecated public abstract int target();
+    property public abstract int mask;
+    property public abstract String name;
+    property public abstract int target;
+  }
+
+  @Deprecated public enum InspectableProperty.ValueType {
+    method @Deprecated public static androidx.annotation.InspectableProperty.ValueType valueOf(String name) throws java.lang.IllegalArgumentException;
+    method @Deprecated public static androidx.annotation.InspectableProperty.ValueType[] values();
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType COLOR;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType GRAVITY;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INFERRED;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INT_ENUM;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INT_FLAG;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType NONE;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType RESOURCE_ID;
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface IntDef {
+    method public abstract boolean flag() default false;
+    method public abstract boolean open() default false;
+    method public abstract int[] value();
+    property public abstract boolean flag;
+    property public abstract boolean open;
+    property public abstract int[] value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface IntRange {
+    method public abstract long from() default kotlin.jvm.internal.LongCompanionObject.MIN_VALUE;
+    method public abstract long to() default kotlin.jvm.internal.LongCompanionObject.MAX_VALUE;
+    property public abstract long from;
+    property public abstract long to;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface IntegerRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface InterpolatorRes {
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.FIELD}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FILE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface Keep {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface LayoutRes {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface LongDef {
+    method public abstract boolean flag() default false;
+    method public abstract boolean open() default false;
+    method public abstract long[] value();
+    property public abstract boolean flag;
+    property public abstract boolean open;
+    property public abstract long[] value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface MainThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface MenuRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface NavigationRes {
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FILE}) public @interface NonNull {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface NonUiContext {
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FILE}) public @interface Nullable {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CLASS}) public @interface OpenForTesting {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface PluralsRes {
+  }
+
+  @Dimension(unit=androidx.annotation.Dimension.Companion.PX) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface Px {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface RawRes {
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RequiresApi {
+    method public abstract int api() default 1;
+    method public abstract int value() default 1;
+    property public abstract int api;
+    property public abstract int value;
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RequiresExtension {
+    method public abstract int extension();
+    method public abstract int version();
+    property public abstract int extension;
+    property public abstract int version;
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public static @interface RequiresExtension.Container {
+    method public abstract androidx.annotation.RequiresExtension[] value();
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface RequiresFeature {
+    method public abstract String enforcement();
+    method public abstract String name();
+    property public abstract String enforcement;
+    property public abstract String name;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface RequiresPermission {
+    method public abstract String[] allOf();
+    method public abstract String[] anyOf();
+    method public abstract boolean conditional() default false;
+    method public abstract String value() default "";
+    property public abstract String[] allOf;
+    property public abstract String[] anyOf;
+    property public abstract boolean conditional;
+    property public abstract String value;
+  }
+
+  @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public static @interface RequiresPermission.Read {
+    method public abstract androidx.annotation.RequiresPermission value() default androidx.annotation.RequiresPermission();
+    property public abstract androidx.annotation.RequiresPermission value;
+  }
+
+  @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public static @interface RequiresPermission.Write {
+    method public abstract androidx.annotation.RequiresPermission value() default androidx.annotation.RequiresPermission();
+    property public abstract androidx.annotation.RequiresPermission value;
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RestrictTo {
+    method public abstract androidx.annotation.RestrictTo.Scope[] value();
+    property public abstract androidx.annotation.RestrictTo.Scope[] value;
+  }
+
+  public enum RestrictTo.Scope {
+    method public static androidx.annotation.RestrictTo.Scope valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.annotation.RestrictTo.Scope[] values();
+    enum_constant @Deprecated public static final androidx.annotation.RestrictTo.Scope GROUP_ID;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY_GROUP;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY_GROUP_PREFIX;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope SUBCLASSES;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope TESTS;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.CLASS}) public @interface ReturnThis {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface Size {
+    method public abstract long max() default kotlin.jvm.internal.LongCompanionObject.MAX_VALUE;
+    method public abstract long min() default kotlin.jvm.internal.LongCompanionObject.MIN_VALUE;
+    method public abstract long multiple() default 1;
+    method public abstract long value() default -1;
+    property public abstract long max;
+    property public abstract long min;
+    property public abstract long multiple;
+    property public abstract long value;
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface StringDef {
+    method public abstract boolean open() default false;
+    method public abstract String[] value();
+    property public abstract boolean open;
+    property public abstract String[] value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StringRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StyleRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StyleableRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface TransitionRes {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface UiContext {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface UiThread {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface VisibleForTesting {
+    method public abstract int otherwise() default androidx.annotation.VisibleForTesting.PRIVATE;
+    property public abstract int otherwise;
+    field public static final androidx.annotation.VisibleForTesting.Companion Companion;
+    field public static final int NONE = 5; // 0x5
+    field public static final int PACKAGE_PRIVATE = 3; // 0x3
+    field public static final int PRIVATE = 2; // 0x2
+    field public static final int PROTECTED = 4; // 0x4
+  }
+
+  public static final class VisibleForTesting.Companion {
+    field public static final int NONE = 5; // 0x5
+    field public static final int PACKAGE_PRIVATE = 3; // 0x3
+    field public static final int PRIVATE = 2; // 0x2
+    field public static final int PROTECTED = 4; // 0x4
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface WorkerThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface XmlRes {
+  }
+
+}
+
diff --git a/annotation/annotation/api/public_plus_experimental_1.6.0-beta01.txt b/annotation/annotation/api/public_plus_experimental_1.6.0-beta01.txt
new file mode 100644
index 0000000..660800c
--- /dev/null
+++ b/annotation/annotation/api/public_plus_experimental_1.6.0-beta01.txt
@@ -0,0 +1,369 @@
+// Signature format: 4.0
+package androidx.annotation {
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnimRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnimatorRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnyRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface AnyThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface ArrayRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AttrRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface BinderThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface BoolRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface CallSuper {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface CheckResult {
+    method public abstract String suggest() default "";
+    property public abstract String suggest;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ChecksSdkIntAtLeast {
+    method public abstract int api() default -1;
+    method public abstract String codename() default "";
+    method public abstract int extension() default 0;
+    method public abstract int lambda() default -1;
+    method public abstract int parameter() default -1;
+    property public abstract int api;
+    property public abstract String codename;
+    property public abstract int extension;
+    property public abstract int lambda;
+    property public abstract int parameter;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ColorInt {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ColorLong {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface ColorRes {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CONSTRUCTOR) public @interface ContentView {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface DeprecatedSinceApi {
+    method public abstract int api();
+    method public abstract String message() default "";
+    property public abstract int api;
+    property public abstract String message;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface DimenRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface Dimension {
+    method public abstract int unit() default androidx.annotation.Dimension.PX;
+    property public abstract int unit;
+    field public static final androidx.annotation.Dimension.Companion Companion;
+    field public static final int DP = 0; // 0x0
+    field public static final int PX = 1; // 0x1
+    field public static final int SP = 2; // 0x2
+  }
+
+  public static final class Dimension.Companion {
+    field public static final int DP = 0; // 0x0
+    field public static final int PX = 1; // 0x1
+    field public static final int SP = 2; // 0x2
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public @interface Discouraged {
+    method public abstract String message();
+    property public abstract String message;
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface DisplayContext {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface DoNotInline {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface DrawableRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface EmptySuper {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface FloatRange {
+    method public abstract double from() default kotlin.jvm.internal.DoubleCompanionObject.NEGATIVE_INFINITY;
+    method public abstract boolean fromInclusive() default true;
+    method public abstract double to() default kotlin.jvm.internal.DoubleCompanionObject.POSITIVE_INFINITY;
+    method public abstract boolean toInclusive() default true;
+    property public abstract double from;
+    property public abstract boolean fromInclusive;
+    property public abstract double to;
+    property public abstract boolean toInclusive;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface FontRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface FractionRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface GravityInt {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface GuardedBy {
+    method public abstract String value();
+    property public abstract String value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface HalfFloat {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface IdRes {
+  }
+
+  @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InspectableProperty {
+    method @Deprecated public abstract int attributeId() default 0;
+    method @Deprecated public abstract androidx.annotation.InspectableProperty.EnumEntry[] enumMapping();
+    method @Deprecated public abstract androidx.annotation.InspectableProperty.FlagEntry[] flagMapping();
+    method @Deprecated public abstract boolean hasAttributeId() default true;
+    method @Deprecated public abstract String name() default "";
+    method @Deprecated public abstract androidx.annotation.InspectableProperty.ValueType valueType() default androidx.annotation.InspectableProperty.ValueType.INFERRED;
+    property public abstract int attributeId;
+    property public abstract androidx.annotation.InspectableProperty.EnumEntry[] enumMapping;
+    property public abstract androidx.annotation.InspectableProperty.FlagEntry[] flagMapping;
+    property public abstract boolean hasAttributeId;
+    property public abstract String name;
+    property public abstract androidx.annotation.InspectableProperty.ValueType valueType;
+  }
+
+  @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public static @interface InspectableProperty.EnumEntry {
+    method @Deprecated public abstract String name();
+    method @Deprecated public abstract int value();
+    property public abstract String name;
+    property public abstract int value;
+  }
+
+  @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public static @interface InspectableProperty.FlagEntry {
+    method @Deprecated public abstract int mask() default 0;
+    method @Deprecated public abstract String name();
+    method @Deprecated public abstract int target();
+    property public abstract int mask;
+    property public abstract String name;
+    property public abstract int target;
+  }
+
+  @Deprecated public enum InspectableProperty.ValueType {
+    method @Deprecated public static androidx.annotation.InspectableProperty.ValueType valueOf(String name) throws java.lang.IllegalArgumentException;
+    method @Deprecated public static androidx.annotation.InspectableProperty.ValueType[] values();
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType COLOR;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType GRAVITY;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INFERRED;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INT_ENUM;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INT_FLAG;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType NONE;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType RESOURCE_ID;
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface IntDef {
+    method public abstract boolean flag() default false;
+    method public abstract boolean open() default false;
+    method public abstract int[] value();
+    property public abstract boolean flag;
+    property public abstract boolean open;
+    property public abstract int[] value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface IntRange {
+    method public abstract long from() default kotlin.jvm.internal.LongCompanionObject.MIN_VALUE;
+    method public abstract long to() default kotlin.jvm.internal.LongCompanionObject.MAX_VALUE;
+    property public abstract long from;
+    property public abstract long to;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface IntegerRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface InterpolatorRes {
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.FIELD}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FILE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface Keep {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface LayoutRes {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface LongDef {
+    method public abstract boolean flag() default false;
+    method public abstract boolean open() default false;
+    method public abstract long[] value();
+    property public abstract boolean flag;
+    property public abstract boolean open;
+    property public abstract long[] value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface MainThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface MenuRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface NavigationRes {
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FILE}) public @interface NonNull {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface NonUiContext {
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FILE}) public @interface Nullable {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CLASS}) public @interface OpenForTesting {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface PluralsRes {
+  }
+
+  @Dimension(unit=androidx.annotation.Dimension.Companion.PX) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface Px {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface RawRes {
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RequiresApi {
+    method public abstract int api() default 1;
+    method public abstract int value() default 1;
+    property public abstract int api;
+    property public abstract int value;
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RequiresExtension {
+    method public abstract int extension();
+    method public abstract int version();
+    property public abstract int extension;
+    property public abstract int version;
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public static @interface RequiresExtension.Container {
+    method public abstract androidx.annotation.RequiresExtension[] value();
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface RequiresFeature {
+    method public abstract String enforcement();
+    method public abstract String name();
+    property public abstract String enforcement;
+    property public abstract String name;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface RequiresPermission {
+    method public abstract String[] allOf();
+    method public abstract String[] anyOf();
+    method public abstract boolean conditional() default false;
+    method public abstract String value() default "";
+    property public abstract String[] allOf;
+    property public abstract String[] anyOf;
+    property public abstract boolean conditional;
+    property public abstract String value;
+  }
+
+  @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public static @interface RequiresPermission.Read {
+    method public abstract androidx.annotation.RequiresPermission value() default androidx.annotation.RequiresPermission();
+    property public abstract androidx.annotation.RequiresPermission value;
+  }
+
+  @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public static @interface RequiresPermission.Write {
+    method public abstract androidx.annotation.RequiresPermission value() default androidx.annotation.RequiresPermission();
+    property public abstract androidx.annotation.RequiresPermission value;
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RestrictTo {
+    method public abstract androidx.annotation.RestrictTo.Scope[] value();
+    property public abstract androidx.annotation.RestrictTo.Scope[] value;
+  }
+
+  public enum RestrictTo.Scope {
+    method public static androidx.annotation.RestrictTo.Scope valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.annotation.RestrictTo.Scope[] values();
+    enum_constant @Deprecated public static final androidx.annotation.RestrictTo.Scope GROUP_ID;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY_GROUP;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY_GROUP_PREFIX;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope SUBCLASSES;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope TESTS;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.CLASS}) public @interface ReturnThis {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface Size {
+    method public abstract long max() default kotlin.jvm.internal.LongCompanionObject.MAX_VALUE;
+    method public abstract long min() default kotlin.jvm.internal.LongCompanionObject.MIN_VALUE;
+    method public abstract long multiple() default 1;
+    method public abstract long value() default -1;
+    property public abstract long max;
+    property public abstract long min;
+    property public abstract long multiple;
+    property public abstract long value;
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface StringDef {
+    method public abstract boolean open() default false;
+    method public abstract String[] value();
+    property public abstract boolean open;
+    property public abstract String[] value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StringRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StyleRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StyleableRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface TransitionRes {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface UiContext {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface UiThread {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface VisibleForTesting {
+    method public abstract int otherwise() default androidx.annotation.VisibleForTesting.PRIVATE;
+    property public abstract int otherwise;
+    field public static final androidx.annotation.VisibleForTesting.Companion Companion;
+    field public static final int NONE = 5; // 0x5
+    field public static final int PACKAGE_PRIVATE = 3; // 0x3
+    field public static final int PRIVATE = 2; // 0x2
+    field public static final int PROTECTED = 4; // 0x4
+  }
+
+  public static final class VisibleForTesting.Companion {
+    field public static final int NONE = 5; // 0x5
+    field public static final int PACKAGE_PRIVATE = 3; // 0x3
+    field public static final int PRIVATE = 2; // 0x2
+    field public static final int PROTECTED = 4; // 0x4
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface WorkerThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface XmlRes {
+  }
+
+}
+
diff --git a/annotation/annotation/api/restricted_1.6.0-beta01.txt b/annotation/annotation/api/restricted_1.6.0-beta01.txt
new file mode 100644
index 0000000..660800c
--- /dev/null
+++ b/annotation/annotation/api/restricted_1.6.0-beta01.txt
@@ -0,0 +1,369 @@
+// Signature format: 4.0
+package androidx.annotation {
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnimRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnimatorRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnyRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface AnyThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface ArrayRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AttrRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface BinderThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface BoolRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface CallSuper {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface CheckResult {
+    method public abstract String suggest() default "";
+    property public abstract String suggest;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ChecksSdkIntAtLeast {
+    method public abstract int api() default -1;
+    method public abstract String codename() default "";
+    method public abstract int extension() default 0;
+    method public abstract int lambda() default -1;
+    method public abstract int parameter() default -1;
+    property public abstract int api;
+    property public abstract String codename;
+    property public abstract int extension;
+    property public abstract int lambda;
+    property public abstract int parameter;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ColorInt {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ColorLong {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface ColorRes {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CONSTRUCTOR) public @interface ContentView {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface DeprecatedSinceApi {
+    method public abstract int api();
+    method public abstract String message() default "";
+    property public abstract int api;
+    property public abstract String message;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface DimenRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface Dimension {
+    method public abstract int unit() default androidx.annotation.Dimension.PX;
+    property public abstract int unit;
+    field public static final androidx.annotation.Dimension.Companion Companion;
+    field public static final int DP = 0; // 0x0
+    field public static final int PX = 1; // 0x1
+    field public static final int SP = 2; // 0x2
+  }
+
+  public static final class Dimension.Companion {
+    field public static final int DP = 0; // 0x0
+    field public static final int PX = 1; // 0x1
+    field public static final int SP = 2; // 0x2
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public @interface Discouraged {
+    method public abstract String message();
+    property public abstract String message;
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface DisplayContext {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface DoNotInline {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface DrawableRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface EmptySuper {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface FloatRange {
+    method public abstract double from() default kotlin.jvm.internal.DoubleCompanionObject.NEGATIVE_INFINITY;
+    method public abstract boolean fromInclusive() default true;
+    method public abstract double to() default kotlin.jvm.internal.DoubleCompanionObject.POSITIVE_INFINITY;
+    method public abstract boolean toInclusive() default true;
+    property public abstract double from;
+    property public abstract boolean fromInclusive;
+    property public abstract double to;
+    property public abstract boolean toInclusive;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface FontRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface FractionRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface GravityInt {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface GuardedBy {
+    method public abstract String value();
+    property public abstract String value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface HalfFloat {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface IdRes {
+  }
+
+  @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InspectableProperty {
+    method @Deprecated public abstract int attributeId() default 0;
+    method @Deprecated public abstract androidx.annotation.InspectableProperty.EnumEntry[] enumMapping();
+    method @Deprecated public abstract androidx.annotation.InspectableProperty.FlagEntry[] flagMapping();
+    method @Deprecated public abstract boolean hasAttributeId() default true;
+    method @Deprecated public abstract String name() default "";
+    method @Deprecated public abstract androidx.annotation.InspectableProperty.ValueType valueType() default androidx.annotation.InspectableProperty.ValueType.INFERRED;
+    property public abstract int attributeId;
+    property public abstract androidx.annotation.InspectableProperty.EnumEntry[] enumMapping;
+    property public abstract androidx.annotation.InspectableProperty.FlagEntry[] flagMapping;
+    property public abstract boolean hasAttributeId;
+    property public abstract String name;
+    property public abstract androidx.annotation.InspectableProperty.ValueType valueType;
+  }
+
+  @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public static @interface InspectableProperty.EnumEntry {
+    method @Deprecated public abstract String name();
+    method @Deprecated public abstract int value();
+    property public abstract String name;
+    property public abstract int value;
+  }
+
+  @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public static @interface InspectableProperty.FlagEntry {
+    method @Deprecated public abstract int mask() default 0;
+    method @Deprecated public abstract String name();
+    method @Deprecated public abstract int target();
+    property public abstract int mask;
+    property public abstract String name;
+    property public abstract int target;
+  }
+
+  @Deprecated public enum InspectableProperty.ValueType {
+    method @Deprecated public static androidx.annotation.InspectableProperty.ValueType valueOf(String name) throws java.lang.IllegalArgumentException;
+    method @Deprecated public static androidx.annotation.InspectableProperty.ValueType[] values();
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType COLOR;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType GRAVITY;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INFERRED;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INT_ENUM;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INT_FLAG;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType NONE;
+    enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType RESOURCE_ID;
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface IntDef {
+    method public abstract boolean flag() default false;
+    method public abstract boolean open() default false;
+    method public abstract int[] value();
+    property public abstract boolean flag;
+    property public abstract boolean open;
+    property public abstract int[] value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface IntRange {
+    method public abstract long from() default kotlin.jvm.internal.LongCompanionObject.MIN_VALUE;
+    method public abstract long to() default kotlin.jvm.internal.LongCompanionObject.MAX_VALUE;
+    property public abstract long from;
+    property public abstract long to;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface IntegerRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface InterpolatorRes {
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.FIELD}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FILE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface Keep {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface LayoutRes {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface LongDef {
+    method public abstract boolean flag() default false;
+    method public abstract boolean open() default false;
+    method public abstract long[] value();
+    property public abstract boolean flag;
+    property public abstract boolean open;
+    property public abstract long[] value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface MainThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface MenuRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface NavigationRes {
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FILE}) public @interface NonNull {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface NonUiContext {
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FILE}) public @interface Nullable {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CLASS}) public @interface OpenForTesting {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface PluralsRes {
+  }
+
+  @Dimension(unit=androidx.annotation.Dimension.Companion.PX) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface Px {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface RawRes {
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RequiresApi {
+    method public abstract int api() default 1;
+    method public abstract int value() default 1;
+    property public abstract int api;
+    property public abstract int value;
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RequiresExtension {
+    method public abstract int extension();
+    method public abstract int version();
+    property public abstract int extension;
+    property public abstract int version;
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public static @interface RequiresExtension.Container {
+    method public abstract androidx.annotation.RequiresExtension[] value();
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface RequiresFeature {
+    method public abstract String enforcement();
+    method public abstract String name();
+    property public abstract String enforcement;
+    property public abstract String name;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface RequiresPermission {
+    method public abstract String[] allOf();
+    method public abstract String[] anyOf();
+    method public abstract boolean conditional() default false;
+    method public abstract String value() default "";
+    property public abstract String[] allOf;
+    property public abstract String[] anyOf;
+    property public abstract boolean conditional;
+    property public abstract String value;
+  }
+
+  @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public static @interface RequiresPermission.Read {
+    method public abstract androidx.annotation.RequiresPermission value() default androidx.annotation.RequiresPermission();
+    property public abstract androidx.annotation.RequiresPermission value;
+  }
+
+  @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public static @interface RequiresPermission.Write {
+    method public abstract androidx.annotation.RequiresPermission value() default androidx.annotation.RequiresPermission();
+    property public abstract androidx.annotation.RequiresPermission value;
+  }
+
+  @java.lang.annotation.Target({java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RestrictTo {
+    method public abstract androidx.annotation.RestrictTo.Scope[] value();
+    property public abstract androidx.annotation.RestrictTo.Scope[] value;
+  }
+
+  public enum RestrictTo.Scope {
+    method public static androidx.annotation.RestrictTo.Scope valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.annotation.RestrictTo.Scope[] values();
+    enum_constant @Deprecated public static final androidx.annotation.RestrictTo.Scope GROUP_ID;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY_GROUP;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY_GROUP_PREFIX;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope SUBCLASSES;
+    enum_constant public static final androidx.annotation.RestrictTo.Scope TESTS;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.CLASS}) public @interface ReturnThis {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface Size {
+    method public abstract long max() default kotlin.jvm.internal.LongCompanionObject.MAX_VALUE;
+    method public abstract long min() default kotlin.jvm.internal.LongCompanionObject.MIN_VALUE;
+    method public abstract long multiple() default 1;
+    method public abstract long value() default -1;
+    property public abstract long max;
+    property public abstract long min;
+    property public abstract long multiple;
+    property public abstract long value;
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface StringDef {
+    method public abstract boolean open() default false;
+    method public abstract String[] value();
+    property public abstract boolean open;
+    property public abstract String[] value;
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StringRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StyleRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StyleableRes {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface TransitionRes {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface UiContext {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface UiThread {
+  }
+
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface VisibleForTesting {
+    method public abstract int otherwise() default androidx.annotation.VisibleForTesting.PRIVATE;
+    property public abstract int otherwise;
+    field public static final androidx.annotation.VisibleForTesting.Companion Companion;
+    field public static final int NONE = 5; // 0x5
+    field public static final int PACKAGE_PRIVATE = 3; // 0x3
+    field public static final int PRIVATE = 2; // 0x2
+    field public static final int PROTECTED = 4; // 0x4
+  }
+
+  public static final class VisibleForTesting.Companion {
+    field public static final int NONE = 5; // 0x5
+    field public static final int PACKAGE_PRIVATE = 3; // 0x3
+    field public static final int PRIVATE = 2; // 0x2
+    field public static final int PROTECTED = 4; // 0x4
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface WorkerThread {
+  }
+
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface XmlRes {
+  }
+
+}
+
diff --git a/camera/camera-viewfinder/api/1.1.0-beta03.txt b/appactions/interaction/interaction-capabilities-core/api/current.txt
similarity index 100%
copy from camera/camera-viewfinder/api/1.1.0-beta03.txt
copy to appactions/interaction/interaction-capabilities-core/api/current.txt
diff --git a/camera/camera-viewfinder/api/1.1.0-beta03.txt b/appactions/interaction/interaction-capabilities-core/api/public_plus_experimental_current.txt
similarity index 100%
copy from camera/camera-viewfinder/api/1.1.0-beta03.txt
copy to appactions/interaction/interaction-capabilities-core/api/public_plus_experimental_current.txt
diff --git a/camera/camera-viewfinder/api/res-1.1.0-beta03.txt b/appactions/interaction/interaction-capabilities-core/api/res-current.txt
similarity index 100%
copy from camera/camera-viewfinder/api/res-1.1.0-beta03.txt
copy to appactions/interaction/interaction-capabilities-core/api/res-current.txt
diff --git a/camera/camera-viewfinder/api/1.1.0-beta03.txt b/appactions/interaction/interaction-capabilities-core/api/restricted_current.txt
similarity index 100%
copy from camera/camera-viewfinder/api/1.1.0-beta03.txt
copy to appactions/interaction/interaction-capabilities-core/api/restricted_current.txt
diff --git a/appactions/interaction/interaction-capabilities-core/build.gradle b/appactions/interaction/interaction-capabilities-core/build.gradle
new file mode 100644
index 0000000..57dc092
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/build.gradle
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+import androidx.build.Publish
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+}
+
+dependencies {
+    api(libs.autoValueAnnotations)
+    annotationProcessor(libs.autoValue)
+    implementation(libs.protobufLite)
+    implementation(libs.guavaListenableFuture)
+    implementation("androidx.concurrent:concurrent-futures:1.1.0")
+    implementation(project(":appactions:interaction:interaction-proto"))
+    testImplementation(libs.junit)
+    testImplementation(libs.truth)
+    testImplementation(libs.testCore)
+    testImplementation(libs.mockitoCore)
+    testImplementation(libs.kotlinStdlib)
+    testImplementation(libs.androidLint)
+    testImplementation(libs.androidLintTests)
+    testImplementation(libs.autoValueAnnotations)
+    testImplementation(libs.testRunner)
+    testAnnotationProcessor(libs.autoValue)
+}
+
+android {
+    defaultConfig {
+        // TODO(b/266649259): lower minSdk version to 19.
+        minSdkVersion 33
+    }
+
+    lintOptions {
+        // TODO(b/266849030): Remove when updating library.
+        disable("UnknownNullness", "SyntheticAccessor")
+    }
+
+    namespace "androidx.appactions.interaction.capabilities.core"
+}
+
+androidx {
+    name = "androidx.appactions.interaction:interaction-capabilities-core"
+    type = LibraryType.PUBLISHED_LIBRARY
+    publish = Publish.NONE
+    inceptionYear = "2023"
+    description = "App Interaction library core capabilities API and implementation."
+    failOnDeprecationWarnings = false
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/AbstractCapabilityBuilder.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/AbstractCapabilityBuilder.java
new file mode 100644
index 0000000..a7c0999
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/AbstractCapabilityBuilder.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appactions.interaction.capabilities.core.impl.spec.ActionCapabilityImpl;
+import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec;
+import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater;
+import androidx.appactions.interaction.capabilities.core.task.impl.TaskCapabilityImpl;
+
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * An abstract Builder class for ActionCapability.
+ *
+ * @param <BuilderT>
+ * @param <PropertyT>
+ * @param <ArgumentT>
+ * @param <OutputT>
+ * @param <ConfirmationT>
+ * @param <TaskUpdaterT>
+ */
+public abstract class AbstractCapabilityBuilder<
+        BuilderT extends
+                AbstractCapabilityBuilder<
+                        BuilderT, PropertyT, ArgumentT, OutputT, ConfirmationT, TaskUpdaterT>,
+        PropertyT,
+        ArgumentT,
+        OutputT,
+        ConfirmationT,
+        TaskUpdaterT extends AbstractTaskUpdater> {
+
+    private final ActionSpec<PropertyT, ArgumentT, OutputT> mActionSpec;
+    @Nullable
+    private String mId;
+    @Nullable
+    private PropertyT mProperty;
+    @Nullable
+    private ActionExecutor<ArgumentT, OutputT> mActionExecutor;
+    @Nullable
+    private TaskHandler<ArgumentT, OutputT, ConfirmationT, TaskUpdaterT> mTaskHandler;
+
+    /**
+     * @param actionSpec
+     */
+    protected AbstractCapabilityBuilder(
+            @NonNull ActionSpec<PropertyT, ArgumentT, OutputT> actionSpec) {
+        this.mActionSpec = actionSpec;
+    }
+
+    @SuppressWarnings("unchecked") // cast to child class
+    private BuilderT asBuilder() {
+        return (BuilderT) this;
+    }
+
+    /**
+     * Sets the Id of the capability being built. The Id should be a non-null string that is unique
+     * among all ActionCapability, and should not change during/across activity lifecycles.
+     */
+    @NonNull
+    public final BuilderT setId(@NonNull String id) {
+        this.mId = id;
+        return asBuilder();
+    }
+
+    /**
+     * Sets the Property instance for this capability. Must be called before {@link
+     * AbstractCapabilityBuilder.build}.
+     */
+    protected final BuilderT setProperty(@NonNull PropertyT property) {
+        this.mProperty = property;
+        return asBuilder();
+    }
+
+    /**
+     * Sets the TaskHandler for this capability. The individual capability factory classes can
+     * decide
+     * to expose their own public {@code setTaskHandler} method and invoke this parent method.
+     * Setting
+     * the TaskHandler should build a capability instance that supports multi-turn tasks.
+     */
+    protected final BuilderT setTaskHandler(
+            @NonNull TaskHandler<ArgumentT, OutputT, ConfirmationT, TaskUpdaterT> taskHandler) {
+        this.mTaskHandler = taskHandler;
+        return asBuilder();
+    }
+
+    /** Sets the ActionExecutor for this capability. */
+    @NonNull
+    public final BuilderT setActionExecutor(
+            @NonNull ActionExecutor<ArgumentT, OutputT> actionExecutor) {
+        this.mActionExecutor = actionExecutor;
+        return asBuilder();
+    }
+
+    /** Builds and returns this ActionCapability. */
+    @NonNull
+    public ActionCapability build() {
+        Objects.requireNonNull(mProperty, "property must not be null.");
+        if (mTaskHandler == null) {
+            Objects.requireNonNull(mActionExecutor, "actionExecutor must not be null.");
+            return new ActionCapabilityImpl<>(
+                    mActionSpec, Optional.ofNullable(mId), mProperty, mActionExecutor);
+        }
+        TaskCapabilityImpl<PropertyT, ArgumentT, OutputT, ConfirmationT, TaskUpdaterT>
+                taskCapability =
+                new TaskCapabilityImpl<>(
+                        Objects.requireNonNull(mId, "id field must not be null."),
+                        mActionSpec,
+                        mProperty,
+                        mTaskHandler.getParamsRegistry(),
+                        mTaskHandler.getOnInitListener(),
+                        mTaskHandler.getOnReadyToConfirmListener(),
+                        mTaskHandler.getOnFinishListener(),
+                        mTaskHandler.getConfirmationDataBindings(),
+                        mTaskHandler.getExecutionOutputBindings(),
+                        Runnable::run);
+        taskCapability.setTaskUpdaterSupplier(mTaskHandler.getTaskUpdaterSupplier());
+        return taskCapability;
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/AbstractTaskHandlerBuilder.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/AbstractTaskHandlerBuilder.java
new file mode 100644
index 0000000..b73802c
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/AbstractTaskHandlerBuilder.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core;
+
+import static androidx.appactions.interaction.capabilities.core.impl.utils.ImmutableCollectors.toImmutableList;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appactions.interaction.capabilities.core.impl.converters.DisambigEntityConverter;
+import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter;
+import androidx.appactions.interaction.capabilities.core.impl.converters.SearchActionConverter;
+import androidx.appactions.interaction.capabilities.core.task.AppEntityListResolver;
+import androidx.appactions.interaction.capabilities.core.task.AppEntityResolver;
+import androidx.appactions.interaction.capabilities.core.task.InvalidTaskException;
+import androidx.appactions.interaction.capabilities.core.task.InventoryListResolver;
+import androidx.appactions.interaction.capabilities.core.task.InventoryResolver;
+import androidx.appactions.interaction.capabilities.core.task.OnDialogFinishListener;
+import androidx.appactions.interaction.capabilities.core.task.OnInitListener;
+import androidx.appactions.interaction.capabilities.core.task.ValueListListener;
+import androidx.appactions.interaction.capabilities.core.task.ValueListener;
+import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater;
+import androidx.appactions.interaction.capabilities.core.task.impl.GenericResolverInternal;
+import androidx.appactions.interaction.capabilities.core.task.impl.OnReadyToConfirmListenerInternal;
+import androidx.appactions.interaction.capabilities.core.task.impl.TaskParamRegistry;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * An abstract Builder class for an ActionCapability that supports task.
+ *
+ * @param <BuilderT>
+ * @param <ArgumentT>
+ * @param <OutputT>
+ * @param <ConfirmationT>
+ * @param <TaskUpdaterT>
+ */
+public abstract class AbstractTaskHandlerBuilder<
+        BuilderT extends
+                AbstractTaskHandlerBuilder<BuilderT, ArgumentT, OutputT, ConfirmationT,
+                        TaskUpdaterT>,
+        ArgumentT,
+        OutputT,
+        ConfirmationT,
+        TaskUpdaterT extends AbstractTaskUpdater> {
+
+    private final ConfirmationType mConfirmationType;
+    private final TaskParamRegistry.Builder mParamsRegistry;
+    private final Map<String, Function<OutputT, List<ParamValue>>> mExecutionOutputBindings =
+            new HashMap<>();
+    private final Map<String, Function<ConfirmationT, List<ParamValue>>> mConfirmationDataBindings =
+            new HashMap<>();
+    @Nullable
+    private OnInitListener<TaskUpdaterT> mOnInitListener;
+    @Nullable
+    private OnReadyToConfirmListenerInternal<ConfirmationT> mOnReadyToConfirmListener;
+    @Nullable
+    private OnDialogFinishListener<ArgumentT, OutputT> mOnFinishListener;
+
+    protected AbstractTaskHandlerBuilder() {
+        this(ConfirmationType.NOT_SUPPORTED);
+    }
+
+    protected AbstractTaskHandlerBuilder(@NonNull ConfirmationType confirmationType) {
+        this.mConfirmationType = confirmationType;
+        this.mParamsRegistry = TaskParamRegistry.builder();
+    }
+
+    @SuppressWarnings("unchecked") // cast to child class
+    protected BuilderT asBuilder() {
+        return (BuilderT) this;
+    }
+
+    /** Sets the OnInitListener for this capability. */
+    public final BuilderT setOnInitListener(@NonNull OnInitListener<TaskUpdaterT> onInitListener) {
+        this.mOnInitListener = onInitListener;
+        return asBuilder();
+    }
+
+    /** Sets the onReadyToConfirmListener for this capability. */
+    protected final BuilderT setOnReadyToConfirmListenerInternal(
+            @NonNull OnReadyToConfirmListenerInternal<ConfirmationT> onReadyToConfirm) {
+        this.mOnReadyToConfirmListener = onReadyToConfirm;
+        return asBuilder();
+    }
+
+    /** Sets the onFinishListener for this capability. */
+    public final BuilderT setOnFinishListener(
+            @NonNull OnDialogFinishListener<ArgumentT, OutputT> onFinishListener) {
+        this.mOnFinishListener = onFinishListener;
+        return asBuilder();
+    }
+
+    protected <ValueTypeT> void registerInventoryTaskParam(
+            @NonNull String paramName,
+            @NonNull InventoryResolver<ValueTypeT> listener,
+            @NonNull ParamValueConverter<ValueTypeT> converter) {
+        mParamsRegistry.addTaskParameter(
+                paramName,
+                (paramValue) -> !paramValue.hasIdentifier(),
+                GenericResolverInternal.fromInventoryResolver(listener),
+                Optional.empty(),
+                Optional.empty(),
+                converter);
+    }
+
+    protected <ValueTypeT> void registerInventoryListTaskParam(
+            @NonNull String paramName,
+            @NonNull InventoryListResolver<ValueTypeT> listener,
+            @NonNull ParamValueConverter<ValueTypeT> converter) {
+        mParamsRegistry.addTaskParameter(
+                paramName,
+                (paramValue) -> !paramValue.hasIdentifier(),
+                GenericResolverInternal.fromInventoryListResolver(listener),
+                Optional.empty(),
+                Optional.empty(),
+                converter);
+    }
+
+    protected <ValueTypeT> void registerAppEntityTaskParam(
+            @NonNull String paramName,
+            @NonNull AppEntityResolver<ValueTypeT> listener,
+            @NonNull ParamValueConverter<ValueTypeT> converter,
+            @NonNull DisambigEntityConverter<ValueTypeT> entityConverter,
+            @NonNull SearchActionConverter<ValueTypeT> searchActionConverter) {
+        mParamsRegistry.addTaskParameter(
+                paramName,
+                (paramValue) -> !paramValue.hasIdentifier(),
+                GenericResolverInternal.fromAppEntityResolver(listener),
+                Optional.of(entityConverter),
+                Optional.of(searchActionConverter),
+                converter);
+    }
+
+    protected <ValueTypeT> void registerAppEntityListTaskParam(
+            @NonNull String paramName,
+            @NonNull AppEntityListResolver<ValueTypeT> listener,
+            @NonNull ParamValueConverter<ValueTypeT> converter,
+            @NonNull DisambigEntityConverter<ValueTypeT> entityConverter,
+            @NonNull SearchActionConverter<ValueTypeT> searchActionConverter) {
+        mParamsRegistry.addTaskParameter(
+                paramName,
+                (paramValue) -> !paramValue.hasIdentifier(),
+                GenericResolverInternal.fromAppEntityListResolver(listener),
+                Optional.of(entityConverter),
+                Optional.of(searchActionConverter),
+                converter);
+    }
+
+    protected <ValueTypeT> void registerValueTaskParam(
+            @NonNull String paramName,
+            @NonNull ValueListener<ValueTypeT> listener,
+            @NonNull ParamValueConverter<ValueTypeT> converter) {
+        mParamsRegistry.addTaskParameter(
+                paramName,
+                unused -> false,
+                GenericResolverInternal.fromValueListener(listener),
+                Optional.empty(),
+                Optional.empty(),
+                converter);
+    }
+
+    protected <ValueTypeT> void registerValueListTaskParam(
+            @NonNull String paramName,
+            @NonNull ValueListListener<ValueTypeT> listener,
+            @NonNull ParamValueConverter<ValueTypeT> converter) {
+        mParamsRegistry.addTaskParameter(
+                paramName,
+                unused -> false,
+                GenericResolverInternal.fromValueListListener(listener),
+                Optional.empty(),
+                Optional.empty(),
+                converter);
+    }
+
+    /**
+     * Registers an optional execution output.
+     *
+     * @param paramName    the BII output slot name of this parameter.
+     * @param outputGetter a getter of the output from the {@code OutputT} instance.
+     * @param converter    a converter from an output object to a ParamValue.
+     */
+    protected <T> void registerExecutionOutput(
+            @NonNull String paramName,
+            @NonNull Function<OutputT, Optional<T>> outputGetter,
+            @NonNull Function<T, ParamValue> converter) {
+        mExecutionOutputBindings.put(
+                paramName,
+                output -> outputGetter.apply(output).stream().map(converter).collect(
+                        toImmutableList()));
+    }
+
+    /**
+     * Registers a repeated execution output.
+     *
+     * @param paramName    the BII output slot name of this parameter.
+     * @param outputGetter a getter of the output from the {@code OutputT} instance.
+     * @param converter    a converter from an output object to a ParamValue.
+     */
+    protected <T> void registerRepeatedExecutionOutput(
+            @NonNull String paramName,
+            @NonNull Function<OutputT, List<T>> outputGetter,
+            @NonNull Function<T, ParamValue> converter) {
+        mExecutionOutputBindings.put(
+                paramName,
+                output -> outputGetter.apply(output).stream().map(converter).collect(
+                        toImmutableList()));
+    }
+
+    /**
+     * Registers an optional confirmation data.
+     *
+     * @param paramName          the BIC confirmation data slot name of this parameter.
+     * @param confirmationGetter a getter of the confirmation data from the {@code ConfirmationT}
+     *                           instance.
+     * @param converter          a converter from confirmation data to a ParamValue.
+     */
+    protected <T> void registerConfirmationOutput(
+            @NonNull String paramName,
+            @NonNull Function<ConfirmationT, Optional<T>> confirmationGetter,
+            @NonNull Function<T, ParamValue> converter) {
+        mConfirmationDataBindings.put(
+                paramName,
+                output ->
+                        confirmationGetter.apply(output).stream().map(converter).collect(
+                                toImmutableList()));
+    }
+
+    /** Specific capability builders override this to support BII-specific TaskUpdaters. */
+    @NonNull
+    protected abstract Supplier<TaskUpdaterT> getTaskUpdaterSupplier();
+
+    /**
+     * Build a TaskHandler.
+     */
+    @NonNull
+    public TaskHandler<ArgumentT, OutputT, ConfirmationT, TaskUpdaterT> build() {
+        if (this.mConfirmationType == ConfirmationType.REQUIRED
+                && mOnReadyToConfirmListener == null) {
+            throw new InvalidTaskException(
+                    "ConfirmationType is REQUIRED, but onReadyToConfirmListener is not set.");
+        }
+        if (this.mConfirmationType == ConfirmationType.NOT_SUPPORTED
+                && mOnReadyToConfirmListener != null) {
+            throw new InvalidTaskException(
+                    "ConfirmationType is NOT_SUPPORTED, but onReadyToConfirmListener is set.");
+        }
+        return new TaskHandler<>(
+                mConfirmationType,
+                mParamsRegistry.build(),
+                Optional.ofNullable(mOnInitListener),
+                Optional.ofNullable(mOnReadyToConfirmListener),
+                Objects.requireNonNull(mOnFinishListener, "onTaskFinishListener must not be null."),
+                mConfirmationDataBindings,
+                mExecutionOutputBindings,
+                getTaskUpdaterSupplier());
+    }
+
+    /** Confirmation types for a Capability. */
+    protected enum ConfirmationType {
+        // Confirmation is not supported for this Capability.
+        NOT_SUPPORTED,
+        // This Capability requires confirmation.
+        REQUIRED,
+        // Confirmation is optional for this Capability.
+        OPTIONAL
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionCapability.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionCapability.java
new file mode 100644
index 0000000..7f1b4e5
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionCapability.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core;
+
+import androidx.annotation.NonNull;
+
+import java.util.Optional;
+
+/**
+ * <b>Do not implement this interface yourself.</b>
+ *
+ * <p>An ActionCapability represents some supported App Action that can be given to App Control.
+ *
+ * <p>Use helper classes provided by the capability library to get instances of this interface.
+ */
+public interface ActionCapability {
+
+    /** Returns the unique Id of this capability declaration. */
+    @NonNull
+    Optional<String> getId();
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionExecutor.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionExecutor.java
new file mode 100644
index 0000000..c0c32f5
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionExecutor.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core;
+
+import androidx.annotation.NonNull;
+
+/**
+ * An interface of executing the action.
+ *
+ * @param <ArgumentT>
+ * @param <OutputT>
+ */
+public interface ActionExecutor<ArgumentT, OutputT> {
+    /**
+     * Calls to execute the action.
+     *
+     * @param argument the argument for this action.
+     * @param callback the callback to send back the action execution result.
+     */
+    void execute(@NonNull ArgumentT argument, @NonNull ActionCallback<OutputT> callback);
+
+    /** Reasons for the action execution error. */
+    enum ErrorStatus {
+        CANCELLED,
+        /** The action execution error was caused by a timeout. */
+        TIMEOUT,
+    }
+
+    /**
+     * An interface for receiving the result of action.
+     *
+     * @param <OutputT>
+     */
+    interface ActionCallback<OutputT> {
+
+        /** Invoke to set an action result upon success. */
+        void onSuccess(@NonNull ExecutionResult<OutputT> executionResult);
+
+        /** Invoke to set an action result upon success. */
+        default void onSuccess() {
+            onSuccess(ExecutionResult.<OutputT>newBuilderWithOutput().build());
+        }
+
+        /**
+         * Invokes to set an error status for the action.
+         *
+         * @deprecated
+         */
+        @Deprecated
+        void onError(@NonNull ErrorStatus errorStatus);
+    }
+
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ConfirmationOutput.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ConfirmationOutput.java
new file mode 100644
index 0000000..7a9c9c6
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ConfirmationOutput.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Class that represents the response after all slots are filled and accepted and the task is ready
+ * to enter the confirmation turn.
+ *
+ * @param <ConfirmationT>
+ */
+@AutoValue
+public abstract class ConfirmationOutput<ConfirmationT> {
+
+    /**
+     * Create a Builder instance for building a ConfirmationOutput instance without confirmation
+     * output.
+     */
+    @NonNull
+    public static Builder<Void> newBuilder() {
+        return new AutoValue_ConfirmationOutput.Builder<>();
+    }
+
+    /** Returns a default ConfirmationOutput instance. */
+    @NonNull
+    public static ConfirmationOutput<Void> getDefaultInstance() {
+        return ConfirmationOutput.newBuilder().build();
+    }
+
+    /** Create a Builder instance for building a ConfirmationOutput instance. */
+    @NonNull
+    public static <ConfirmationT> Builder<ConfirmationT> newBuilderWithConfirmation() {
+        return new AutoValue_ConfirmationOutput.Builder<>();
+    }
+
+    /** Returns a default ConfirmationOutput instance with a confirmation output type. */
+    @NonNull
+    public static <ConfirmationT>
+            ConfirmationOutput<ConfirmationT> getDefaultInstanceWithConfirmation() {
+        return ConfirmationOutput.<ConfirmationT>newBuilderWithConfirmation().build();
+    }
+
+    /** The confirmation output. */
+    @Nullable
+    public abstract ConfirmationT getConfirmation();
+
+    /**
+     * Builder for ConfirmationOutput.
+     *
+     * @param <ConfirmationT>
+     */
+    @AutoValue.Builder
+    public abstract static class Builder<ConfirmationT> {
+
+        /** Sets the confirmation output. */
+        @NonNull
+        public abstract Builder<ConfirmationT> setConfirmation(ConfirmationT confirmation);
+
+        /** Builds and returns the ConfirmationOutput instance. */
+        @NonNull
+        public abstract ConfirmationOutput<ConfirmationT> build();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ExecutionResult.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ExecutionResult.java
new file mode 100644
index 0000000..b42f20b
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ExecutionResult.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Class that represents the response after an ActionCapability fulfills an action.
+ *
+ * @param <OutputT>
+ */
+@AutoValue
+public abstract class ExecutionResult<OutputT> {
+
+    /** Create a Builder instance for building a ExecutionResult instance without output. */
+    @NonNull
+    public static Builder<Void> newBuilder() {
+        return new AutoValue_ExecutionResult.Builder<Void>().setStartDictation(false);
+    }
+
+    /** Returns a default ExecutionResult instance. */
+    @NonNull
+    public static ExecutionResult<Void> getDefaultInstance() {
+        return ExecutionResult.newBuilder().build();
+    }
+
+    /** Create a Builder instance for building a ExecutionResult instance. */
+    @NonNull
+    public static <OutputT> Builder<OutputT> newBuilderWithOutput() {
+        return new AutoValue_ExecutionResult.Builder<OutputT>().setStartDictation(false);
+    }
+
+    /** Returns a default ExecutionResult instance with an output type. */
+    @NonNull
+    public static <OutputT> ExecutionResult<OutputT> getDefaultInstanceWithOutput() {
+        return ExecutionResult.<OutputT>newBuilderWithOutput().build();
+    }
+
+    /** Whether to start dictation mode after the fulfillment. */
+    public abstract boolean getStartDictation();
+
+    /** The execution output. */
+    @Nullable
+    public abstract OutputT getOutput();
+
+    /**
+     * Builder for ExecutionResult.
+     *
+     * @param <OutputT>
+     */
+    @AutoValue.Builder
+    public abstract static class Builder<OutputT> {
+        /** Sets whether or not this fulfillment should start dictation. */
+        @NonNull
+        public abstract Builder<OutputT> setStartDictation(boolean startDictation);
+
+        /** Sets the execution output. */
+        @NonNull
+        public abstract Builder<OutputT> setOutput(OutputT output);
+
+        /** Builds and returns the ExecutionResult instance. */
+        @NonNull
+        public abstract ExecutionResult<OutputT> build();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/TaskHandler.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/TaskHandler.java
new file mode 100644
index 0000000..1341b36
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/TaskHandler.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.AbstractTaskHandlerBuilder.ConfirmationType;
+import androidx.appactions.interaction.capabilities.core.task.OnDialogFinishListener;
+import androidx.appactions.interaction.capabilities.core.task.OnInitListener;
+import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater;
+import androidx.appactions.interaction.capabilities.core.task.impl.OnReadyToConfirmListenerInternal;
+import androidx.appactions.interaction.capabilities.core.task.impl.TaskParamRegistry;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Temporary holder for Task related data.
+ *
+ * @param <ArgumentT>
+ * @param <OutputT>
+ * @param <ConfirmationT>
+ * @param <TaskUpdaterT>
+ */
+public final class TaskHandler<
+        ArgumentT, OutputT, ConfirmationT, TaskUpdaterT extends AbstractTaskUpdater> {
+
+    private final ConfirmationType mConfirmationType;
+    private final TaskParamRegistry mParamsRegistry;
+    private final Map<String, Function<OutputT, List<ParamValue>>> mExecutionOutputBindings;
+    private final Map<String, Function<ConfirmationT, List<ParamValue>>> mConfirmationDataBindings;
+    private final Optional<OnInitListener<TaskUpdaterT>> mOnInitListener;
+    private final Optional<OnReadyToConfirmListenerInternal<ConfirmationT>>
+            mOnReadyToConfirmListener;
+    private final OnDialogFinishListener<ArgumentT, OutputT> mOnFinishListener;
+    private final Supplier<TaskUpdaterT> mTaskUpdaterSupplier;
+
+    TaskHandler(
+            @NonNull ConfirmationType confirmationType,
+            @NonNull TaskParamRegistry paramsRegistry,
+            @NonNull Optional<OnInitListener<TaskUpdaterT>> onInitListener,
+            @NonNull Optional<OnReadyToConfirmListenerInternal<ConfirmationT>> onReadyToConfirmListener,
+            @NonNull OnDialogFinishListener<ArgumentT, OutputT> onFinishListener,
+            @NonNull Map<String, Function<ConfirmationT, List<ParamValue>>> confirmationDataBindings,
+            @NonNull Map<String, Function<OutputT, List<ParamValue>>> executionOutputBindings,
+            @NonNull Supplier<TaskUpdaterT> taskUpdaterSupplier) {
+        this.mConfirmationType = confirmationType;
+        this.mParamsRegistry = paramsRegistry;
+        this.mOnInitListener = onInitListener;
+        this.mOnReadyToConfirmListener = onReadyToConfirmListener;
+        this.mOnFinishListener = onFinishListener;
+        this.mConfirmationDataBindings = confirmationDataBindings;
+        this.mExecutionOutputBindings = executionOutputBindings;
+        this.mTaskUpdaterSupplier = taskUpdaterSupplier;
+    }
+
+    ConfirmationType getConfirmationType() {
+        return mConfirmationType;
+    }
+
+    TaskParamRegistry getParamsRegistry() {
+        return mParamsRegistry;
+    }
+
+    Map<String, Function<OutputT, List<ParamValue>>> getExecutionOutputBindings() {
+        return mExecutionOutputBindings;
+    }
+
+    Map<String, Function<ConfirmationT, List<ParamValue>>> getConfirmationDataBindings() {
+        return mConfirmationDataBindings;
+    }
+
+    Optional<OnInitListener<TaskUpdaterT>> getOnInitListener() {
+        return mOnInitListener;
+    }
+
+    Optional<OnReadyToConfirmListenerInternal<ConfirmationT>> getOnReadyToConfirmListener() {
+        return mOnReadyToConfirmListener;
+    }
+
+    OnDialogFinishListener<ArgumentT, OutputT> getOnFinishListener() {
+        return mOnFinishListener;
+    }
+
+    Supplier<TaskUpdaterT> getTaskUpdaterSupplier() {
+        return mTaskUpdaterSupplier;
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ActionCapabilityInternal.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ActionCapabilityInternal.java
new file mode 100644
index 0000000..10ffa4b
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ActionCapabilityInternal.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.ActionCapability;
+import androidx.appactions.interaction.proto.AppActionsContext.AppAction;
+
+/** An interface that represents the capability of an app action. */
+public interface ActionCapabilityInternal extends ActionCapability {
+
+    /** Returns an app action proto describing how to fulfill this capability. */
+    @NonNull
+    AppAction getAppAction();
+
+    /**
+     * Executes the action and returns the result of execution.
+     *
+     * @param argumentsWrapper The arguments send from assistant to the activity.
+     * @param callback         The callback to receive app action result.
+     */
+    void execute(@NonNull ArgumentsWrapper argumentsWrapper, @NonNull CallbackInternal callback);
+
+    /**
+     * Support for manual input. This method should be invoked by AppInteraction SDKs
+     * (background/foreground), so the developers have a way to report state updates back to
+     * Assistant.
+     */
+    default void setTouchEventCallback(@NonNull TouchEventCallback callback) {
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ArgumentsWrapper.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ArgumentsWrapper.java
new file mode 100644
index 0000000..a8ea310
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ArgumentsWrapper.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl;
+
+import static androidx.appactions.interaction.capabilities.core.impl.utils.ImmutableCollectors.toImmutableList;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment;
+import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.FulfillmentParam;
+import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.FulfillmentValue;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/** Represents Fulfillment request sent from assistant, including arguments. */
+@SuppressWarnings("AutoValueImmutableFields")
+@AutoValue
+public abstract class ArgumentsWrapper {
+
+    /**
+     * Creates an instance of ArgumentsWrapper based on the Fulfillment send from Assistant.
+     *
+     * @param fulfillment for a single BII sent from Assistant.
+     */
+    @NonNull
+    public static ArgumentsWrapper create(@NonNull Fulfillment fulfillment) {
+        return new AutoValue_ArgumentsWrapper(
+                Collections.unmodifiableMap(convertToArgumentMap(fulfillment)),
+                createRequestMetadata(fulfillment));
+    }
+
+    private static Optional<RequestMetadata> createRequestMetadata(Fulfillment fulfillment) {
+        if (fulfillment.getType() == Fulfillment.Type.UNKNOWN_TYPE
+                || fulfillment.getType() == Fulfillment.Type.UNRECOGNIZED) {
+            return Optional.empty();
+        }
+        return Optional.of(
+                RequestMetadata.newBuilder().setRequestType(fulfillment.getType()).build());
+    }
+
+    private static Map<String, List<FulfillmentValue>> convertToArgumentMap(
+            Fulfillment fulfillment) {
+        Map<String, List<FulfillmentValue>> result = new LinkedHashMap<>();
+        for (FulfillmentParam fp : fulfillment.getParamsList()) {
+            // Normalize deprecated param value list into new FulfillmentValue.
+            if (!fp.getValuesList().isEmpty()) {
+                result.put(
+                        fp.getName(),
+                        fp.getValuesList().stream()
+                                .map(paramValue -> FulfillmentValue.newBuilder().setValue(
+                                        paramValue).build())
+                                .collect(toImmutableList()));
+            } else {
+                result.put(fp.getName(), fp.getFulfillmentValuesList());
+            }
+        }
+        return result;
+    }
+
+    /**
+     * A map of BII parameter names to a task param value, where each {@code FulfillmentValue} can
+     * have a value and {@code DisambigData} sent from Assistant.
+     */
+    @NonNull
+    public abstract Map<String, List<FulfillmentValue>> paramValues();
+
+    /**
+     * Metadata from the FulfillmentRequest on the current Assistant turn. This field should be
+     * Optional.empty for one-shot capabilities.
+     */
+    @NonNull
+    public abstract Optional<RequestMetadata> requestMetadata();
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/BuilderOf.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/BuilderOf.java
new file mode 100644
index 0000000..03934ba
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/BuilderOf.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl;
+
+import androidx.annotation.NonNull;
+
+/**
+ * A builder of objects of some specific type.
+ *
+ * @param <T>
+ */
+public interface BuilderOf<T> {
+    @NonNull
+    T build();
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/CallbackInternal.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/CallbackInternal.java
new file mode 100644
index 0000000..db2ab01
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/CallbackInternal.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.proto.FulfillmentResponse;
+
+/** An interface for receiving the result of action. */
+public interface CallbackInternal {
+
+    /** Invoke to set an action result upon success. */
+    void onSuccess(@NonNull FulfillmentResponse fulfillmentResponse);
+
+    default void onSuccess() {
+        onSuccess(FulfillmentResponse.getDefaultInstance());
+    }
+
+    /** Invokes to set an error status for the action. */
+    void onError(@NonNull ErrorStatusInternal errorStatus);
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ErrorStatusInternal.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ErrorStatusInternal.java
new file mode 100644
index 0000000..c707770
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ErrorStatusInternal.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl;
+
+/** A class to define exceptions that are reported from dialog capability API. */
+public enum ErrorStatusInternal {
+    CANCELLED(0),
+    TIMEOUT(1),
+    INVALID_REQUEST_TYPE(2),
+    UNCHANGED_DISAMBIG_STATE(3),
+    INVALID_RESOLVER(4),
+    STRUCT_CONVERSION_FAILURE(5),
+    SYNC_REQUEST_FAILURE(6),
+    CONFIRMATION_REQUEST_FAILURE(7),
+    TOUCH_EVENT_REQUEST_FAILURE(8);
+
+    private final int mCode;
+
+    ErrorStatusInternal(int code) {
+        this.mCode = code;
+    }
+
+    public int getCode() {
+        return mCode;
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/RequestMetadata.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/RequestMetadata.java
new file mode 100644
index 0000000..7230d51
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/RequestMetadata.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment;
+
+import com.google.auto.value.AutoValue;
+
+/** Represents metadata from the Assistant FulfillmentRequest. */
+@AutoValue
+public abstract class RequestMetadata {
+
+    /** Create a Builder instance for building a RequestMetadata instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_RequestMetadata.Builder();
+    }
+
+    /** The Type of request Assistant is sending on this FulfillmentRequest. */
+    @NonNull
+    public abstract Fulfillment.Type requestType();
+
+    /** Builder for RequestMetadata. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+        /** Sets the FulfillmentRequest.Type. */
+        @NonNull
+        public abstract Builder setRequestType(@NonNull Fulfillment.Type requestType);
+
+        /** Builds the RequestMetadata instance. */
+        @NonNull
+        public abstract RequestMetadata build();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/TouchEventCallback.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/TouchEventCallback.java
new file mode 100644
index 0000000..c86aeed
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/TouchEventCallback.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.proto.FulfillmentResponse;
+import androidx.appactions.interaction.proto.TouchEventMetadata;
+
+/**
+ * An internal interface to allow the AppInteraction SDKs to be notified of results from processing
+ * touch events.
+ */
+public interface TouchEventCallback {
+
+    /** Results from a successful touch event invocation. */
+    void onSuccess(
+            @NonNull FulfillmentResponse fulfillmentResponse,
+            @NonNull TouchEventMetadata touchEventMetadata);
+
+    /** Results from an unsuccessful touch event invocation. */
+    void onError(@NonNull ErrorStatusInternal errorStatus);
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/FutureCallback.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/FutureCallback.java
new file mode 100644
index 0000000..6d2ca04
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/FutureCallback.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.concurrent;
+
+import androidx.annotation.NonNull;
+
+/**
+ * A FutureCallback that can be attached to a ListenableFuture with Futures#addCallback.
+ *
+ * @param <V>
+ */
+public interface FutureCallback<V> {
+    /** Called with the ListenableFuture's result if it completes successfully. */
+    void onSuccess(V result);
+
+    /** Called with the ListenableFuture's exception if it fails. */
+    void onFailure(@NonNull Throwable t);
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/Futures.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/Futures.java
new file mode 100644
index 0000000..51a4614
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/Futures.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.concurrent;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+import java.util.function.Function;
+
+/** Future/ListenableFuture related utility methods. */
+public final class Futures {
+    private Futures() {
+    }
+
+    /** Attach a FutureCallback to a ListenableFuture instance. */
+    public static <V> void addCallback(
+            @NonNull final ListenableFuture<V> future,
+            @NonNull final FutureCallback<? super V> callback,
+            @NonNull Executor executor) {
+        Utils.checkNotNull(callback);
+        future.addListener(new CallbackListener<>(future, callback), executor);
+    }
+
+    /**
+     * Transforms an input ListenableFuture into a second ListenableFuture by applying a
+     * transforming
+     * function to the result of the input ListenableFuture.
+     */
+    @NonNull
+    @SuppressLint("LambdaLast")
+    public static <I, O> ListenableFuture<O> transform(
+            @NonNull ListenableFuture<I> input,
+            @NonNull Function<I, O> function,
+            @NonNull Executor executor,
+            @Nullable String tag) {
+        return CallbackToFutureAdapter.getFuture(
+                completer -> {
+                    addCallback(input, transformFutureCallback(completer, function), executor);
+                    return tag;
+                });
+    }
+
+    /**
+     * Transforms an input ListenableFuture into a second ListenableFuture by applying an
+     * asynchronous
+     * transforming function to the result of the input ListenableFuture.
+     */
+    @NonNull
+    @SuppressLint("LambdaLast")
+    public static <I, O> ListenableFuture<O> transformAsync(
+            @NonNull ListenableFuture<I> input,
+            @NonNull Function<I, ListenableFuture<O>> asyncFunction,
+            @NonNull Executor executor,
+            @NonNull String tag) {
+
+        return CallbackToFutureAdapter.getFuture(
+                completer -> {
+                    addCallback(input, asyncTransformFutureCallback(completer, asyncFunction),
+                            executor);
+                    return tag;
+                });
+    }
+
+    /** Returns a Future that is immediately complete with the given value. */
+    @NonNull
+    public static <V> ListenableFuture<V> immediateFuture(V value) {
+        return CallbackToFutureAdapter.getFuture(completer -> completer.set(value));
+    }
+
+    /** Returns a Future that is immediately complete with null value. */
+    @NonNull
+    public static ListenableFuture<Void> immediateVoidFuture() {
+        return CallbackToFutureAdapter.getFuture(completer -> completer.set(null));
+    }
+
+    /** Returns a Future that is immediately complete with an exception. */
+    @NonNull
+    public static <V> ListenableFuture<V> immediateFailedFuture(@NonNull Throwable throwable) {
+        return CallbackToFutureAdapter.getFuture(completer -> completer.setException(throwable));
+    }
+
+    /**
+     * Returns a FutureCallback that transform the result in onSuccess, and then set the result in
+     * completer.
+     */
+    static <I, O> FutureCallback<I> transformFutureCallback(
+            Completer<O> completer, Function<I, O> function) {
+        return new FutureCallback<I>() {
+            @Override
+            public void onSuccess(I result) {
+                try {
+                    completer.set(function.apply(result));
+                } catch (Throwable t) {
+                    if (t instanceof InterruptedException) {
+                        Thread.currentThread().interrupt();
+                    }
+                    completer.setException(t);
+                }
+            }
+
+            @Override
+            public void onFailure(Throwable failure) {
+                completer.setException(failure);
+            }
+        };
+    }
+
+    /** Returns a FutureCallback that asynchronously transform the result. */
+    private static <I, O> FutureCallback<I> asyncTransformFutureCallback(
+            Completer<O> completer, Function<I, ListenableFuture<O>> asyncFunction) {
+        return new FutureCallback<I>() {
+            @Override
+            public void onSuccess(I inputResult) {
+                try {
+                    addCallback(
+                            asyncFunction.apply(inputResult),
+                            transformFutureCallback(completer, Function.identity()),
+                            Runnable::run);
+                } catch (Throwable t) {
+                    if (t instanceof InterruptedException) {
+                        Thread.currentThread().interrupt();
+                    }
+                    completer.setException(t);
+                }
+            }
+
+            @Override
+            public void onFailure(@NonNull Throwable failure) {
+                completer.setException(failure);
+            }
+        };
+    }
+
+    static <V> V getDone(Future<V> future) throws ExecutionException {
+        Utils.checkState(future.isDone(), "future is expected to be done already.");
+        boolean interrupted = false;
+        try {
+            while (true) {
+                try {
+                    return future.get();
+                } catch (InterruptedException e) {
+                    interrupted = true;
+                }
+            }
+        } finally {
+            if (interrupted) {
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    private static final class CallbackListener<V> implements Runnable {
+        final Future<V> mFuture;
+        final FutureCallback<? super V> mCallback;
+
+        CallbackListener(Future<V> future, FutureCallback<? super V> callback) {
+            this.mFuture = future;
+            this.mCallback = callback;
+        }
+
+        @Override
+        public void run() {
+            final V value;
+            try {
+                value = getDone(mFuture);
+            } catch (ExecutionException e) {
+                Throwable cause = e.getCause();
+                mCallback.onFailure(cause != null ? cause : e);
+                return;
+            } catch (RuntimeException | Error e) {
+                mCallback.onFailure(e);
+                return;
+            }
+            mCallback.onSuccess(value);
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/Utils.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/Utils.java
new file mode 100644
index 0000000..aeb0e9b
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/Utils.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.concurrent;
+
+import androidx.annotation.Nullable;
+
+final class Utils {
+
+    private Utils() {
+    }
+
+    public static <T> T checkNotNull(@Nullable T reference) {
+        if (reference == null) {
+            throw new NullPointerException();
+        }
+        return reference;
+    }
+
+    public static <T> T checkNotNull(@Nullable T reference, String errorMessage) {
+        if (reference == null) {
+            throw new NullPointerException(errorMessage);
+        }
+        return reference;
+    }
+
+    public static void checkState(boolean b, String message) {
+        if (!b) {
+            throw new IllegalStateException(message);
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/CheckedInterfaces.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/CheckedInterfaces.java
new file mode 100644
index 0000000..056c6e8
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/CheckedInterfaces.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.converters;
+
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+
+/** Contains function interfaces that throw checked exceptions. */
+public final class CheckedInterfaces {
+
+    private CheckedInterfaces() {
+    }
+
+    /** A BiConsumer interface that can throw StructConversionException. */
+    @FunctionalInterface
+    interface BiConsumer<T, U> {
+        void accept(T t, U u) throws StructConversionException;
+    }
+
+    /**
+     * A Function interface that can throw StructConversionException.
+     *
+     * @param <T>
+     * @param <R>
+     */
+    @FunctionalInterface
+    public interface Function<T, R> {
+        R apply(T t) throws StructConversionException;
+    }
+
+    /** A Consumer interface that can throw StructConversionException. */
+    @FunctionalInterface
+    interface Consumer<T> {
+        void accept(T t) throws StructConversionException;
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/DisambigEntityConverter.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/DisambigEntityConverter.java
new file mode 100644
index 0000000..9a95a32
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/DisambigEntityConverter.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.converters;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+import androidx.appactions.interaction.proto.Entity;
+
+/**
+ * Converter from {@code ValueTypeT} to the app-driven disambig entity i.e. {@code Entity} proto.
+ * The ValueTypeT instance is usually a value object provided by the app.
+ *
+ * @param <ValueTypeT>
+ */
+@FunctionalInterface
+public interface DisambigEntityConverter<ValueTypeT> {
+    @NonNull
+    Entity convert(ValueTypeT type) throws StructConversionException;
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/FieldBinding.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/FieldBinding.java
new file mode 100644
index 0000000..048f5bb
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/FieldBinding.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.converters;
+
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+
+import com.google.auto.value.AutoValue;
+import com.google.protobuf.Value;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+@AutoValue
+abstract class FieldBinding<T, BuilderT extends BuilderOf<T>> {
+
+    static <T, BuilderT extends BuilderOf<T>> FieldBinding<T, BuilderT> create(
+            String name,
+            Function<T, Optional<Value>> valueGetter,
+            CheckedInterfaces.BiConsumer<BuilderT, Optional<Value>> valueSetter) {
+        return new AutoValue_FieldBinding<>(name, valueGetter, valueSetter);
+    }
+
+    abstract String name();
+
+    abstract Function<T, Optional<Value>> valueGetter();
+
+    abstract CheckedInterfaces.BiConsumer<BuilderT, Optional<Value>> valueSetter();
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/ParamValueConverter.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/ParamValueConverter.java
new file mode 100644
index 0000000..21d5e41
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/ParamValueConverter.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.converters;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+import androidx.appactions.interaction.proto.ParamValue;
+
+/**
+ * A function that converts a single ParamValue to some generic object visible to the SDK user.
+ *
+ * @param <T>
+ */
+@FunctionalInterface
+public interface ParamValueConverter<T> {
+    @NonNull
+    T convert(@NonNull ParamValue paramValue) throws StructConversionException;
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/PropertyConverter.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/PropertyConverter.java
new file mode 100644
index 0000000..f665360
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/PropertyConverter.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.converters;
+
+import static androidx.appactions.interaction.capabilities.core.impl.utils.ImmutableCollectors.toImmutableList;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.properties.EntityProperty;
+import androidx.appactions.interaction.capabilities.core.properties.EnumProperty;
+import androidx.appactions.interaction.capabilities.core.properties.IntegerProperty;
+import androidx.appactions.interaction.capabilities.core.properties.ParamProperty;
+import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringOrEnumProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringProperty.PossibleValue;
+import androidx.appactions.interaction.proto.AppActionsContext.IntentParameter;
+import androidx.appactions.interaction.proto.Entity;
+
+import java.util.List;
+import java.util.function.Function;
+
+/** Contains utility functions that convert properties to IntentParameter proto. */
+public final class PropertyConverter {
+
+    private PropertyConverter() {
+    }
+
+    /** Create IntentParameter proto from a StringProperty. */
+    @NonNull
+    public static IntentParameter getIntentParameter(
+            @NonNull String paramName, @NonNull StringProperty property) {
+        IntentParameter.Builder builder = newIntentParameterBuilder(paramName, property);
+        extractPossibleValues(property, PropertyConverter::possibleValueToProto).stream()
+                .forEach(builder::addPossibleEntities);
+        return builder.build();
+    }
+
+    /** Create IntentParameter proto from a StringOrEnumProperty. */
+    @NonNull
+    public static <EnumT extends Enum<EnumT>> IntentParameter getIntentParameter(
+            @NonNull String paramName, @NonNull StringOrEnumProperty<EnumT> property) {
+        IntentParameter.Builder builder = newIntentParameterBuilder(paramName, property);
+        extractPossibleValues(property, PropertyConverter::possibleValueToProto).stream()
+                .forEach(builder::addPossibleEntities);
+        return builder.build();
+    }
+
+    /** Create IntentParameter proto from a EntityProperty. */
+    @NonNull
+    public static IntentParameter getIntentParameter(
+            @NonNull String paramName, @NonNull EntityProperty property) {
+        IntentParameter.Builder builder = newIntentParameterBuilder(paramName, property);
+        extractPossibleValues(property, PropertyConverter::entityToProto).stream()
+                .forEach(builder::addPossibleEntities);
+        return builder.build();
+    }
+
+    /** Create IntentParameter proto from a EnumProperty. */
+    @NonNull
+    public static <EnumT extends Enum<EnumT>> IntentParameter getIntentParameter(
+            @NonNull String paramName, @NonNull EnumProperty<EnumT> property) {
+        IntentParameter.Builder builder = newIntentParameterBuilder(paramName, property);
+        extractPossibleValues(property, PropertyConverter::enumToProto).stream()
+                .forEach(builder::addPossibleEntities);
+        return builder.build();
+    }
+
+    /** Create IntentParameter proto from a IntegerProperty. */
+    @NonNull
+    public static IntentParameter getIntentParameter(
+            @NonNull String paramName, @NonNull IntegerProperty property) {
+        return newIntentParameterBuilder(paramName, property).build();
+    }
+
+    /** Create IntentParameter proto from a SimpleProperty. */
+    @NonNull
+    public static IntentParameter getIntentParameter(
+            @NonNull String paramName, @NonNull SimpleProperty property) {
+        return newIntentParameterBuilder(paramName, property).build();
+    }
+
+    /** Create IntentParameter.Builder from a generic ParamProperty, fills in the common fields. */
+    private static IntentParameter.Builder newIntentParameterBuilder(
+            String paramName, ParamProperty<?> paramProperty) {
+        return IntentParameter.newBuilder()
+                .setName(paramName)
+                .setIsRequired(paramProperty.isRequired())
+                .setEntityMatchRequired(paramProperty.isValueMatchRequired())
+                .setIsProhibited(paramProperty.isProhibited());
+    }
+
+    private static <T> List<Entity> extractPossibleValues(
+            ParamProperty<T> property, Function<T, Entity> function) {
+        return property.getPossibleValues().stream().map(function).collect(toImmutableList());
+    }
+
+    /** Converts a properties/Entity to a appactions Entity proto. */
+    @NonNull
+    public static Entity entityToProto(
+            @NonNull androidx.appactions.interaction.capabilities.core.properties.Entity entity) {
+        Entity.Builder builder = Entity.newBuilder().addAllAlternateNames(
+                entity.getAlternateNames());
+        entity.getName().ifPresent(builder::setName);
+        entity.getId().ifPresent(builder::setIdentifier);
+        return builder.build();
+    }
+
+    /**
+     * Converts a capabilities library StringProperty.PossibleValue to a appactions Entity proto
+     * .
+     */
+    @NonNull
+    public static Entity possibleValueToProto(@NonNull PossibleValue possibleValue) {
+        return androidx.appactions.interaction.proto.Entity.newBuilder()
+                .setIdentifier(possibleValue.getName())
+                .setName(possibleValue.getName())
+                .addAllAlternateNames(possibleValue.getAlternateNames())
+                .build();
+    }
+
+    /**
+     * Converts a capabilities library StringOrEnumProperty.PossibleValue to a appactions Entity
+     * proto.
+     */
+    @NonNull
+    public static <EnumT extends Enum<EnumT>> Entity possibleValueToProto(
+            @NonNull StringOrEnumProperty.PossibleValue<EnumT> possibleValue) {
+
+        switch (possibleValue.getKind()) {
+            case STRING_VALUE:
+                return possibleValueToProto(possibleValue.stringValue());
+            case ENUM_VALUE:
+                return enumToProto(possibleValue.enumValue());
+        }
+        throw new IllegalStateException("unreachable");
+    }
+
+    @NonNull
+    public static <EnumT extends Enum<EnumT>> Entity enumToProto(EnumT enumValue) {
+        return androidx.appactions.interaction.proto.Entity.newBuilder()
+                .setIdentifier(enumValue.toString())
+                .build();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/SearchActionConverter.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/SearchActionConverter.java
new file mode 100644
index 0000000..a9e98f5
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/SearchActionConverter.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.converters;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+import androidx.appactions.interaction.capabilities.core.values.SearchAction;
+import androidx.appactions.interaction.proto.ParamValue;
+
+/**
+ * Converts an ungrounded ParamValue to a SearchAction object.
+ *
+ * @param <T>
+ */
+@FunctionalInterface
+public interface SearchActionConverter<T> {
+    @NonNull
+    SearchAction<T> toSearchAction(@NonNull ParamValue paramValue) throws StructConversionException;
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/SlotTypeConverter.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/SlotTypeConverter.java
new file mode 100644
index 0000000..d9ae329
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/SlotTypeConverter.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.converters;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Converts from internal proto representation as defined in the AppActionContext to a public java
+ * type which can be consumed by developers.
+ *
+ * @param <T>
+ */
+@FunctionalInterface
+public interface SlotTypeConverter<T> {
+    @NonNull
+    static <T> SlotTypeConverter<List<T>> ofRepeated(
+            @NonNull ParamValueConverter<T> singularConverter) {
+        return (paramValues) -> {
+            List<T> results = new ArrayList<>();
+            for (ParamValue paramValue : paramValues) {
+                results.add(singularConverter.convert(paramValue));
+            }
+            return results;
+        };
+    }
+
+    /** This converter will throw IndexOutOfBoundsException if the input List is empty. */
+    @NonNull
+    static <T> SlotTypeConverter<T> ofSingular(
+            @NonNull ParamValueConverter<T> singularConverter) {
+        return (paramValues) -> singularConverter.convert(paramValues.get(0));
+    }
+
+    T convert(@NonNull List<ParamValue> protoList) throws StructConversionException;
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConverters.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConverters.java
new file mode 100644
index 0000000..14c48d5
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConverters.java
@@ -0,0 +1,766 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.converters;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.ExecutionResult;
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+import androidx.appactions.interaction.capabilities.core.values.Alarm;
+import androidx.appactions.interaction.capabilities.core.values.CalendarEvent;
+import androidx.appactions.interaction.capabilities.core.values.Call;
+import androidx.appactions.interaction.capabilities.core.values.EntityValue;
+import androidx.appactions.interaction.capabilities.core.values.ItemList;
+import androidx.appactions.interaction.capabilities.core.values.ListItem;
+import androidx.appactions.interaction.capabilities.core.values.Message;
+import androidx.appactions.interaction.capabilities.core.values.Order;
+import androidx.appactions.interaction.capabilities.core.values.OrderItem;
+import androidx.appactions.interaction.capabilities.core.values.Organization;
+import androidx.appactions.interaction.capabilities.core.values.ParcelDelivery;
+import androidx.appactions.interaction.capabilities.core.values.Person;
+import androidx.appactions.interaction.capabilities.core.values.SafetyCheck;
+import androidx.appactions.interaction.capabilities.core.values.SearchAction;
+import androidx.appactions.interaction.capabilities.core.values.Timer;
+import androidx.appactions.interaction.capabilities.core.values.properties.Attendee;
+import androidx.appactions.interaction.capabilities.core.values.properties.Participant;
+import androidx.appactions.interaction.capabilities.core.values.properties.Recipient;
+import androidx.appactions.interaction.proto.Entity;
+import androidx.appactions.interaction.proto.FulfillmentResponse;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import com.google.protobuf.ListValue;
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
+
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeParseException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/** Converters for capability argument values. Convert from internal proto types to public types. */
+public final class TypeConverters {
+    public static final String FIELD_NAME_TYPE = "@type";
+    public static final TypeSpec<ListItem> LIST_ITEM_TYPE_SPEC =
+            TypeSpecBuilder.newBuilderForThing("ListItem", ListItem::newBuilder).build();
+    public static final TypeSpec<ItemList> ITEM_LIST_TYPE_SPEC =
+            TypeSpecBuilder.newBuilderForThing("ItemList", ItemList::newBuilder)
+                    .bindRepeatedSpecField(
+                            "itemListElement",
+                            ItemList::getListItems,
+                            ItemList.Builder::addAllListItems,
+                            LIST_ITEM_TYPE_SPEC)
+                    .build();
+    public static final TypeSpec<OrderItem> ORDER_ITEM_TYPE_SPEC =
+            TypeSpecBuilder.newBuilderForThing("OrderItem", OrderItem::newBuilder).build();
+    public static final TypeSpec<Organization> ORGANIZATION_TYPE_SPEC =
+            TypeSpecBuilder.newBuilderForThing("Organization", Organization::newBuilder).build();
+    public static final TypeSpec<ParcelDelivery> PARCEL_DELIVERY_TYPE_SPEC =
+            TypeSpecBuilder.newBuilder("ParcelDelivery", ParcelDelivery::newBuilder)
+                    .bindStringField(
+                            "deliveryAddress",
+                            ParcelDelivery::getDeliveryAddress,
+                            ParcelDelivery.Builder::setDeliveryAddress)
+                    .bindZonedDateTimeField(
+                            "expectedArrivalFrom",
+                            ParcelDelivery::getExpectedArrivalFrom,
+                            ParcelDelivery.Builder::setExpectedArrivalFrom)
+                    .bindZonedDateTimeField(
+                            "expectedArrivalUntil",
+                            ParcelDelivery::getExpectedArrivalUntil,
+                            ParcelDelivery.Builder::setExpectedArrivalUntil)
+                    .bindStringField(
+                            "hasDeliveryMethod",
+                            ParcelDelivery::getDeliveryMethod,
+                            ParcelDelivery.Builder::setDeliveryMethod)
+                    .bindStringField(
+                            "trackingNumber",
+                            ParcelDelivery::getTrackingNumber,
+                            ParcelDelivery.Builder::setTrackingNumber)
+                    .bindStringField(
+                            "trackingUrl", ParcelDelivery::getTrackingUrl,
+                            ParcelDelivery.Builder::setTrackingUrl)
+                    .build();
+    public static final TypeSpec<Order> ORDER_TYPE_SPEC =
+            TypeSpecBuilder.newBuilderForThing("Order", Order::newBuilder)
+                    .bindZonedDateTimeField("orderDate", Order::getOrderDate,
+                            Order.Builder::setOrderDate)
+                    .bindSpecField(
+                            "orderDelivery",
+                            Order::getOrderDelivery,
+                            Order.Builder::setOrderDelivery,
+                            PARCEL_DELIVERY_TYPE_SPEC)
+                    .bindRepeatedSpecField(
+                            "orderedItem",
+                            Order::getOrderedItems,
+                            Order.Builder::addAllOrderedItems,
+                            ORDER_ITEM_TYPE_SPEC)
+                    .bindEnumField(
+                            "orderStatus",
+                            Order::getOrderStatus,
+                            Order.Builder::setOrderStatus,
+                            Order.OrderStatus.class)
+                    .bindSpecField(
+                            "seller", Order::getSeller, Order.Builder::setSeller,
+                            ORGANIZATION_TYPE_SPEC)
+                    .build();
+    public static final TypeSpec<Person> PERSON_TYPE_SPEC =
+            TypeSpecBuilder.newBuilderForThing("Person", Person::newBuilder)
+                    .bindStringField("email", Person::getEmail, Person.Builder::setEmail)
+                    .bindStringField("telephone", Person::getTelephone,
+                            Person.Builder::setTelephone)
+                    .bindStringField("name", Person::getName, Person.Builder::setName)
+                    .build();
+    public static final TypeSpec<Alarm> ALARM_TYPE_SPEC =
+            TypeSpecBuilder.newBuilderForThing("Alarm", Alarm::newBuilder).build();
+    public static final TypeSpec<Timer> TIMER_TYPE_SPEC =
+            TypeSpecBuilder.newBuilderForThing("Timer", Timer::newBuilder).build();
+    public static final TypeSpec<CalendarEvent> CALENDAR_EVENT_TYPE_SPEC =
+            TypeSpecBuilder.newBuilderForThing("CalendarEvent", CalendarEvent::newBuilder)
+                    .bindZonedDateTimeField(
+                            "startDate", CalendarEvent::getStartDate,
+                            CalendarEvent.Builder::setStartDate)
+                    .bindZonedDateTimeField(
+                            "endDate", CalendarEvent::getEndDate, CalendarEvent.Builder::setEndDate)
+                    .bindRepeatedSpecField(
+                            "attendee",
+                            CalendarEvent::getAttendeeList,
+                            CalendarEvent.Builder::addAllAttendee,
+                            new AttendeeTypeSpec())
+                    .build();
+    public static final TypeSpec<SafetyCheck> SAFETY_CHECK_TYPE_SPEC =
+            TypeSpecBuilder.newBuilderForThing("SafetyCheck", SafetyCheck::newBuilder)
+                    .bindDurationField("duration", SafetyCheck::getDuration,
+                            SafetyCheck.Builder::setDuration)
+                    .bindZonedDateTimeField(
+                            "checkinTime", SafetyCheck::getCheckinTime,
+                            SafetyCheck.Builder::setCheckinTime)
+                    .build();
+    private static final String FIELD_NAME_CALL_FORMAT = "callFormat";
+    private static final String FIELD_NAME_PARTICIPANT = "participant";
+    private static final String FIELD_NAME_TYPE_CALL = "Call";
+    private static final String FIELD_NAME_TYPE_PERSON = "Person";
+    private static final String FIELD_NAME_TYPE_MESSAGE = "Message";
+    private static final String FIELD_NAME_RECIPIENT = "recipient";
+    private static final String FIELD_NAME_TEXT = "text";
+
+    private TypeConverters() {
+    }
+
+    /**
+     * @param paramValue
+     * @return
+     */
+    @NonNull
+    public static EntityValue toEntityValue(@NonNull ParamValue paramValue) {
+        EntityValue.Builder value = EntityValue.newBuilder();
+        if (paramValue.hasIdentifier()) {
+            value.setId(paramValue.getIdentifier());
+        }
+        value.setValue(paramValue.getStringValue());
+        return value.build();
+    }
+
+    /**
+     * @param paramValue
+     * @return
+     */
+    public static int toIntegerValue(@NonNull ParamValue paramValue) {
+        return (int) paramValue.getNumberValue();
+    }
+
+    /** Converts a ParamValue to a Boolean object.
+     *
+     * @param paramValue
+     * @return
+     * @throws StructConversionException
+     */
+    @NonNull
+    public static Boolean toBooleanValue(@NonNull ParamValue paramValue)
+            throws StructConversionException {
+        if (paramValue.hasBoolValue()) {
+            return paramValue.getBoolValue();
+        }
+
+        throw new StructConversionException(
+                "Cannot parse boolean because bool_value is missing from ParamValue.");
+    }
+
+    /**
+     * @param paramValue
+     * @return
+     * @throws StructConversionException
+     */
+    @NonNull
+    public static LocalDate toLocalDate(@NonNull ParamValue paramValue)
+            throws StructConversionException {
+        if (paramValue.hasStringValue()) {
+            try {
+                return LocalDate.parse(paramValue.getStringValue());
+            } catch (DateTimeParseException e) {
+                throw new StructConversionException("Failed to parse ISO 8601 string to LocalDate",
+                        e);
+            }
+        }
+        throw new StructConversionException(
+                "Cannot parse date because string_value is missing from ParamValue.");
+    }
+
+    /**
+     * @param paramValue
+     * @return
+     * @throws StructConversionException
+     */
+    @NonNull
+    public static LocalTime toLocalTime(@NonNull ParamValue paramValue)
+            throws StructConversionException {
+        if (paramValue.hasStringValue()) {
+            try {
+                return LocalTime.parse(paramValue.getStringValue());
+            } catch (DateTimeParseException e) {
+                throw new StructConversionException("Failed to parse ISO 8601 string to LocalTime",
+                        e);
+            }
+        }
+        throw new StructConversionException(
+                "Cannot parse time because string_value is missing from ParamValue.");
+    }
+
+    /**
+     * @param paramValue
+     * @return
+     * @throws StructConversionException
+     */
+    @NonNull
+    public static ZoneId toZoneId(@NonNull ParamValue paramValue) throws StructConversionException {
+        if (paramValue.hasStringValue()) {
+            try {
+                return ZoneId.of(paramValue.getStringValue());
+            } catch (DateTimeParseException e) {
+                throw new StructConversionException("Failed to parse ISO 8601 string to ZoneId", e);
+            }
+        }
+        throw new StructConversionException(
+                "Cannot parse ZoneId because string_value is missing from ParamValue.");
+    }
+
+    /**
+     * Gets String value for a string property.
+     *
+     * <p>If identifier is present, it's the String value, otherwise it is {@code
+     * paramValue.getStringValue()}
+     *
+     * @param paramValue
+     * @return
+     */
+    @NonNull
+    public static String toStringValue(@NonNull ParamValue paramValue) {
+        if (paramValue.hasIdentifier()) {
+            return paramValue.getIdentifier();
+        }
+        return paramValue.getStringValue();
+    }
+
+    /**
+     * @param entityValue
+     * @return
+     */
+    @NonNull
+    public static Entity toEntity(@NonNull EntityValue entityValue) {
+        return Entity.newBuilder()
+                .setIdentifier(entityValue.getId().get())
+                .setName(entityValue.getValue())
+                .build();
+    }
+
+    /**
+     * Converts an ItemList object to an Entity proto message.
+     *
+     * @param itemList
+     * @return
+     */
+    @NonNull
+    public static Entity toEntity(@NonNull ItemList itemList) {
+        Entity.Builder builder = Entity.newBuilder().setValue(
+                ITEM_LIST_TYPE_SPEC.toStruct(itemList));
+        itemList.getId().ifPresent(builder::setIdentifier);
+        return builder.build();
+    }
+
+    /**
+     * Converts a ListItem object to an Entity proto message.
+     *
+     * @param listItem
+     * @return
+     */
+    @NonNull
+    public static Entity toEntity(@NonNull ListItem listItem) {
+        Entity.Builder builder = Entity.newBuilder().setValue(
+                LIST_ITEM_TYPE_SPEC.toStruct(listItem));
+        listItem.getId().ifPresent(builder::setIdentifier);
+        return builder.build();
+    }
+
+    /**
+     * Converts an Order object to an Entity proto message.
+     *
+     * @param order
+     * @return
+     */
+    @NonNull
+    public static Entity toEntity(@NonNull Order order) {
+        Entity.Builder builder = Entity.newBuilder().setValue(ORDER_TYPE_SPEC.toStruct(order));
+        order.getId().ifPresent(builder::setIdentifier);
+        return builder.build();
+    }
+
+    /**
+     * Converts an Alarm object to an Entity proto message.
+     *
+     * @param alarm
+     * @return
+     */
+    @NonNull
+    public static Entity toEntity(@NonNull Alarm alarm) {
+        Entity.Builder builder = Entity.newBuilder().setValue(ALARM_TYPE_SPEC.toStruct(alarm));
+        alarm.getId().ifPresent(builder::setIdentifier);
+        return builder.build();
+    }
+
+    /**
+     * Converts a ParamValue to a single ItemList object.
+     *
+     * @param paramValue
+     * @return
+     * @throws StructConversionException
+     */
+    @NonNull
+    public static ItemList toItemList(@NonNull ParamValue paramValue)
+            throws StructConversionException {
+        return ITEM_LIST_TYPE_SPEC.fromStruct(paramValue.getStructValue());
+    }
+
+    /**
+     * Converts a ParamValue to a single ListItem object.
+     *
+     * @param paramValue
+     * @return
+     * @throws StructConversionException
+     */
+    @NonNull
+    public static ListItem toListItem(@NonNull ParamValue paramValue)
+            throws StructConversionException {
+        return LIST_ITEM_TYPE_SPEC.fromStruct(paramValue.getStructValue());
+    }
+
+    /**
+     * Converts a ParamValue to a single Order object.
+     *
+     * @param paramValue
+     * @return
+     * @throws StructConversionException
+     */
+    @NonNull
+    public static Order toOrder(@NonNull ParamValue paramValue) throws StructConversionException {
+        return ORDER_TYPE_SPEC.fromStruct(paramValue.getStructValue());
+    }
+
+    /**
+     * Converts a ParamValue to a Timer object.
+     *
+     * @param paramValue
+     * @return
+     * @throws StructConversionException
+     */
+    @NonNull
+    public static Timer toTimer(@NonNull ParamValue paramValue) throws StructConversionException {
+        return TIMER_TYPE_SPEC.fromStruct(paramValue.getStructValue());
+    }
+
+    /**
+     * Converts a ParamValue to a single Alarm object.
+     *
+     * @param paramValue
+     * @return
+     */
+    @NonNull
+    public static Alarm toAssistantAlarm(@NonNull ParamValue paramValue) {
+        return Alarm.newBuilder().setId(paramValue.getIdentifier()).build();
+    }
+
+    /**
+     * @param executionResult
+     * @return
+     */
+    @NonNull
+    public static FulfillmentResponse toFulfillmentResponseProto(
+            @NonNull ExecutionResult<Void> executionResult) {
+        return FulfillmentResponse.newBuilder()
+                .setStartDictation(executionResult.getStartDictation())
+                .build();
+    }
+
+    /**
+     * @param paramValue
+     * @return
+     * @throws StructConversionException
+     */
+    @NonNull
+    public static ZonedDateTime toZonedDateTime(@NonNull ParamValue paramValue)
+            throws StructConversionException {
+        if (paramValue.hasStringValue()) {
+            try {
+                return ZonedDateTime.parse(paramValue.getStringValue());
+            } catch (DateTimeParseException e) {
+                throw new StructConversionException(
+                        "Failed to parse ISO 8601 string to ZonedDateTime", e);
+            }
+        }
+        throw new StructConversionException(
+                "Cannot parse datetime because string_value is missing from ParamValue.");
+    }
+
+    /**
+     * @param paramValue
+     * @return
+     * @throws StructConversionException
+     */
+    @NonNull
+    public static Duration toDuration(@NonNull ParamValue paramValue)
+            throws StructConversionException {
+        if (!paramValue.hasStringValue()) {
+            throw new StructConversionException(
+                    "Cannot parse duration because string_value is missing from ParamValue.");
+        }
+        try {
+            return Duration.parse(paramValue.getStringValue());
+        } catch (DateTimeParseException e) {
+            throw new StructConversionException("Failed to parse ISO 8601 string to Duration", e);
+        }
+    }
+
+    /**
+     * @param nestedTypeSpec
+     * @param <T>
+     * @return
+     */
+    @NonNull
+    public static <T> TypeSpec<SearchAction<T>> createSearchActionTypeSpec(
+            @NonNull TypeSpec<T> nestedTypeSpec) {
+        return TypeSpecBuilder.<SearchAction<T>, SearchAction.Builder<T>>newBuilder(
+                        "SearchAction", SearchAction::newBuilder)
+                .bindStringField("query", SearchAction<T>::getQuery,
+                        SearchAction.Builder<T>::setQuery)
+                .bindSpecField(
+                        "object",
+                        SearchAction<T>::getObject,
+                        SearchAction.Builder<T>::setObject,
+                        nestedTypeSpec)
+                .build();
+    }
+
+    /** Converts a ParamValue to a single Participant object. */
+    @NonNull
+    public static Participant toParticipant(@NonNull ParamValue paramValue)
+            throws StructConversionException {
+        if (FIELD_NAME_TYPE_PERSON.equals(getStructType(paramValue.getStructValue()))) {
+            return new Participant(PERSON_TYPE_SPEC.fromStruct(paramValue.getStructValue()));
+        }
+        throw new StructConversionException("The type is not expected.");
+    }
+
+    /** Converts a ParamValue to a single Recipient object. */
+    @NonNull
+    public static Recipient toRecipient(@NonNull ParamValue paramValue)
+            throws StructConversionException {
+        if (FIELD_NAME_TYPE_PERSON.equals(getStructType(paramValue.getStructValue()))) {
+            return new Recipient(PERSON_TYPE_SPEC.fromStruct(paramValue.getStructValue()));
+        }
+        throw new StructConversionException("The type is not expected.");
+    }
+
+    /** Given some class with a corresponding TypeSpec, create a SearchActionConverter instance. */
+    @NonNull
+    public static <T> SearchActionConverter<T> createSearchActionConverter(
+            @NonNull TypeSpec<T> nestedTypeSpec) {
+        final TypeSpec<SearchAction<T>> typeSpec = createSearchActionTypeSpec(nestedTypeSpec);
+        return (paramValue) -> typeSpec.fromStruct(paramValue.getStructValue());
+    }
+
+    /** Converts a string to a ParamValue. */
+    @NonNull
+    public static ParamValue toParamValue(@NonNull String value) {
+        return ParamValue.newBuilder().setStringValue(value).build();
+    }
+
+    /** Converts an EntityValue to a ParamValue. */
+    @NonNull
+    public static ParamValue toParamValue(@NonNull EntityValue value) {
+        ParamValue.Builder builder = ParamValue.newBuilder().setStringValue(value.getValue());
+        value.getId().ifPresent(builder::setIdentifier);
+        return builder.build();
+    }
+
+    /** Converts a ItemList to a ParamValue. */
+    @NonNull
+    public static ParamValue toParamValue(@NonNull ItemList value) {
+        ParamValue.Builder builder =
+                ParamValue.newBuilder().setStructValue(ITEM_LIST_TYPE_SPEC.toStruct(value));
+        value.getId().ifPresent(builder::setIdentifier);
+        return builder.build();
+    }
+
+    /** Converts a ListItem to a ParamValue. */
+    @NonNull
+    public static ParamValue toParamValue(@NonNull ListItem value) {
+        ParamValue.Builder builder =
+                ParamValue.newBuilder().setStructValue(LIST_ITEM_TYPE_SPEC.toStruct(value));
+        value.getId().ifPresent(builder::setIdentifier);
+        return builder.build();
+    }
+
+    /** Converts an Order to a ParamValue. */
+    @NonNull
+    public static ParamValue toParamValue(@NonNull Order value) {
+        ParamValue.Builder builder =
+                ParamValue.newBuilder().setStructValue(ORDER_TYPE_SPEC.toStruct(value));
+        value.getId().ifPresent(builder::setIdentifier);
+        return builder.build();
+    }
+
+    /** Converts an Alarm to a ParamValue. */
+    @NonNull
+    public static ParamValue toParamValue(@NonNull Alarm value) {
+        ParamValue.Builder builder =
+                ParamValue.newBuilder().setStructValue(ALARM_TYPE_SPEC.toStruct(value));
+        value.getId().ifPresent(builder::setIdentifier);
+        return builder.build();
+    }
+
+    /** Converts an SafetyCheck to a ParamValue. */
+    @NonNull
+    public static ParamValue toParamValue(@NonNull SafetyCheck value) {
+        ParamValue.Builder builder =
+                ParamValue.newBuilder().setStructValue(SAFETY_CHECK_TYPE_SPEC.toStruct(value));
+        value.getId().ifPresent(builder::setIdentifier);
+        return builder.build();
+    }
+
+    /** Converts a Participant to a ParamValue. */
+    @NonNull
+    public static ParamValue toParamValue(@NonNull Participant value) {
+        ParticipantTypeSpec typeSpec = new ParticipantTypeSpec();
+        ParamValue.Builder builder = ParamValue.newBuilder().setStructValue(
+                typeSpec.toStruct(value));
+        typeSpec.getId(value).ifPresent(builder::setIdentifier);
+        return builder.build();
+    }
+
+    /** Converts a Recipient to a ParamValue. */
+    @NonNull
+    public static ParamValue toParamValue(@NonNull Recipient value) {
+        RecipientTypeSpec typeSpec = new RecipientTypeSpec();
+        ParamValue.Builder builder = ParamValue.newBuilder().setStructValue(
+                typeSpec.toStruct(value));
+        typeSpec.getId(value).ifPresent(builder::setIdentifier);
+        return builder.build();
+    }
+
+    /** Converts a Call to a ParamValue. */
+    @NonNull
+    public static ParamValue toParamValue(@NonNull Call value) {
+        ParamValue.Builder builder = ParamValue.newBuilder();
+        Map<String, Value> fieldsMap = new HashMap<>();
+        fieldsMap.put(FIELD_NAME_TYPE,
+                Value.newBuilder().setStringValue(FIELD_NAME_TYPE_CALL).build());
+        if (value.getCallFormat().isPresent()) {
+            fieldsMap.put(
+                    FIELD_NAME_CALL_FORMAT,
+                    Value.newBuilder().setStringValue(
+                            value.getCallFormat().get().toString()).build());
+        }
+        ListValue.Builder participantListBuilder = ListValue.newBuilder();
+        for (Participant participant : value.getParticipantList()) {
+            if (participant.asPerson().isPresent()) {
+                participantListBuilder.addValues(
+                        Value.newBuilder()
+                                .setStructValue(
+                                        PERSON_TYPE_SPEC.toStruct(participant.asPerson().get()))
+                                .build());
+            }
+        }
+        if (!participantListBuilder.getValuesList().isEmpty()) {
+            fieldsMap.put(
+                    FIELD_NAME_PARTICIPANT,
+                    Value.newBuilder().setListValue(participantListBuilder.build()).build());
+        }
+        value.getId().ifPresent(builder::setIdentifier);
+        return builder.setStructValue(Struct.newBuilder().putAllFields(fieldsMap).build()).build();
+    }
+
+    /** Converts a Message to a ParamValue. */
+    @NonNull
+    public static ParamValue toParamValue(@NonNull Message value) {
+        ParamValue.Builder builder = ParamValue.newBuilder();
+        Map<String, Value> fieldsMap = new HashMap<>();
+        fieldsMap.put(
+                FIELD_NAME_TYPE,
+                Value.newBuilder().setStringValue(FIELD_NAME_TYPE_MESSAGE).build());
+        if (value.getMessageText().isPresent()) {
+            fieldsMap.put(
+                    FIELD_NAME_TEXT,
+                    Value.newBuilder().setStringValue(value.getMessageText().get()).build());
+        }
+        ListValue.Builder recipientListBuilder = ListValue.newBuilder();
+        for (Recipient recipient : value.getRecipientList()) {
+            if (recipient.asPerson().isPresent()) {
+                recipientListBuilder.addValues(
+                        Value.newBuilder()
+                                .setStructValue(
+                                        PERSON_TYPE_SPEC.toStruct(recipient.asPerson().get()))
+                                .build());
+            }
+        }
+        if (!recipientListBuilder.getValuesList().isEmpty()) {
+            fieldsMap.put(
+                    FIELD_NAME_RECIPIENT,
+                    Value.newBuilder().setListValue(recipientListBuilder.build()).build());
+        }
+        value.getId().ifPresent(builder::setIdentifier);
+        return builder.setStructValue(Struct.newBuilder().putAllFields(fieldsMap).build()).build();
+    }
+
+    private static String getStructType(Struct struct) throws StructConversionException {
+        Map<String, Value> fieldsMap = struct.getFieldsMap();
+        if (!fieldsMap.containsKey(FIELD_NAME_TYPE)
+                || fieldsMap.get(FIELD_NAME_TYPE).getStringValue().isEmpty()) {
+            throw new StructConversionException("There is no type specified.");
+        }
+        return fieldsMap.get(FIELD_NAME_TYPE).getStringValue();
+    }
+
+    /** {@link TypeSpec} for {@link Participant}. */
+    public static class ParticipantTypeSpec implements TypeSpec<Participant> {
+        @Override
+        @NonNull
+        public Struct toStruct(@NonNull Participant object) {
+            if (object.asPerson().isPresent()) {
+                return PERSON_TYPE_SPEC.toStruct(object.asPerson().get());
+            }
+            return Struct.getDefaultInstance();
+        }
+
+        @Override
+        @NonNull
+        public Participant fromStruct(@NonNull Struct struct) throws StructConversionException {
+            if (FIELD_NAME_TYPE_PERSON.equals(getStructType(struct))) {
+                return new Participant(PERSON_TYPE_SPEC.fromStruct(struct));
+            }
+            throw new StructConversionException(
+                    String.format(
+                            "Unexpected type, expected type is %s while actual type is %s",
+                            FIELD_NAME_TYPE_PERSON, getStructType(struct)));
+        }
+
+        /**
+         * Retrieves identifier from the object within union value.
+         *
+         * @param object
+         * @return
+         */
+        @NonNull
+        public Optional<String> getId(@NonNull Participant object) {
+            return object.asPerson().isPresent() ? object.asPerson().get().getId()
+                    : Optional.empty();
+        }
+    }
+
+    /** {@link TypeSpec} for {@link Recipient}. */
+    public static class RecipientTypeSpec implements TypeSpec<Recipient> {
+        @NonNull
+        @Override
+        public Struct toStruct(@NonNull Recipient object) {
+            if (object.asPerson().isPresent()) {
+                return PERSON_TYPE_SPEC.toStruct(object.asPerson().get());
+            }
+            return Struct.getDefaultInstance();
+        }
+
+        @Override
+        @NonNull
+        public Recipient fromStruct(@NonNull Struct struct) throws StructConversionException {
+            if (FIELD_NAME_TYPE_PERSON.equals(getStructType(struct))) {
+                return new Recipient(PERSON_TYPE_SPEC.fromStruct(struct));
+            }
+            throw new StructConversionException(
+                    String.format(
+                            "Unexpected type, expected type is %s while actual type is %s",
+                            FIELD_NAME_TYPE_PERSON, getStructType(struct)));
+        }
+
+        /**
+         * Retrieves identifier from the object within union value.
+         *
+         * @param object
+         * @return
+         */
+        @NonNull
+        public Optional<String> getId(@NonNull Recipient object) {
+            return object.asPerson().isPresent() ? object.asPerson().get().getId()
+                    : Optional.empty();
+        }
+    }
+
+    /** {@link TypeSpec} for {@link Attendee}. */
+    public static class AttendeeTypeSpec implements TypeSpec<Attendee> {
+        @Override
+        @NonNull
+        public Struct toStruct(@NonNull Attendee object) {
+            if (object.asPerson().isPresent()) {
+                return PERSON_TYPE_SPEC.toStruct(object.asPerson().get());
+            }
+            return Struct.getDefaultInstance();
+        }
+
+        @NonNull
+        @Override
+        public Attendee fromStruct(@NonNull Struct struct) throws StructConversionException {
+            if (FIELD_NAME_TYPE_PERSON.equals(getStructType(struct))) {
+                return new Attendee(TypeConverters.PERSON_TYPE_SPEC.fromStruct(struct));
+            }
+            throw new StructConversionException(
+                    String.format(
+                            "Unexpected type, expected type is %s while actual type is %s",
+                            FIELD_NAME_TYPE_PERSON, getStructType(struct)));
+        }
+
+        /**
+         * Retrieves identifier from the object within union value.
+         *
+         * @param object
+         * @return
+         */
+        @NonNull
+        public Optional<String> getId(@NonNull Attendee object) {
+            return object.asPerson().isPresent() ? object.asPerson().get().getId()
+                    : Optional.empty();
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpec.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpec.java
new file mode 100644
index 0000000..6781668
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpec.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.converters;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+
+import com.google.protobuf.Struct;
+
+/**
+ * TypeSpec is used to convert between java objects in capabilities/values and Struct proto.
+ *
+ * @param <T>
+ */
+public interface TypeSpec<T> {
+
+    /** Converts a java object into a Struct proto. */
+    @NonNull
+    Struct toStruct(@NonNull T object);
+
+    /**
+     * Converts a Struct into java object.
+     *
+     * @throws StructConversionException if the Struct is malformed.
+     */
+    @NonNull
+    T fromStruct(@NonNull Struct struct) throws StructConversionException;
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecBuilder.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecBuilder.java
new file mode 100644
index 0000000..38bab1f
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecBuilder.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.converters;
+
+import static androidx.appactions.interaction.capabilities.core.impl.utils.ImmutableCollectors.toImmutableList;
+
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+import androidx.appactions.interaction.capabilities.core.values.Thing;
+
+import com.google.protobuf.ListValue;
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
+
+import java.time.Duration;
+import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/** Builder for {@link TypeSpec}. */
+final class TypeSpecBuilder<T, BuilderT extends BuilderOf<T>> {
+    private final List<FieldBinding<T, BuilderT>> mBindings = new ArrayList<>();
+    private final Supplier<BuilderT> mBuilderSupplier;
+    private CheckedInterfaces.Consumer<Struct> mStructValidator;
+
+    private TypeSpecBuilder(Supplier<BuilderT> builderSupplier) {
+        this.mBuilderSupplier = builderSupplier;
+    }
+
+    private static Value getStringValue(String string) {
+        return Value.newBuilder().setStringValue(string).build();
+    }
+
+    private static Value getStructValue(Struct struct) {
+        return Value.newBuilder().setStructValue(struct).build();
+    }
+
+    private static Value getListValue(List<Value> values) {
+        return Value.newBuilder()
+                .setListValue(ListValue.newBuilder().addAllValues(values).build())
+                .build();
+    }
+
+    /**
+     * Returns a Value in a Struct, IllegalArgumentException is caught and wrapped in
+     * StructConversionException.
+     *
+     * @param struct the Struct to get values from.
+     * @param key    the String key of the field to retrieve.
+     */
+    private static Value getFieldFromStruct(Struct struct, String key)
+            throws StructConversionException {
+        try {
+            return struct.getFieldsOrThrow(key);
+        } catch (IllegalArgumentException e) {
+            throw new StructConversionException(String.format("%s does not exist in Struct", key),
+                    e);
+        }
+    }
+
+    static <T, BuilderT extends BuilderOf<T>> TypeSpecBuilder<T, BuilderT> newBuilder(
+            String typeName, Supplier<BuilderT> builderSupplier) {
+        return new TypeSpecBuilder<>(builderSupplier)
+                .bindStringField("@type", (unused) -> Optional.of(typeName), (builder, val) -> {
+                })
+                .setStructValidator(
+                        struct -> {
+                            if (!getFieldFromStruct(struct, "@type").getStringValue().equals(
+                                    typeName)) {
+                                throw new StructConversionException(
+                                        String.format("Struct @type field must be equal to %s.",
+                                                typeName));
+                            }
+                        });
+    }
+
+    /**
+     * Creates a new TypeSpecBuilder for a child class of Thing.
+     *
+     * <p>Comes with bindings for Thing fields.
+     */
+    static <T extends Thing, BuilderT extends Thing.Builder<BuilderT> & BuilderOf<T>>
+            TypeSpecBuilder<T, BuilderT> newBuilderForThing(
+                    String typeName, Supplier<BuilderT> builderSupplier) {
+        return newBuilder(typeName, builderSupplier)
+                .bindStringField("identifier", T::getId, BuilderT::setId)
+                .bindStringField("name", T::getName, BuilderT::setName);
+    }
+
+    private TypeSpecBuilder<T, BuilderT> setStructValidator(
+            CheckedInterfaces.Consumer<Struct> structValidator) {
+        this.mStructValidator = structValidator;
+        return this;
+    }
+
+    private TypeSpecBuilder<T, BuilderT> bindFieldInternal(
+            String name,
+            Function<T, Optional<Value>> valueGetter,
+            CheckedInterfaces.BiConsumer<BuilderT, Optional<Value>> valueSetter) {
+        mBindings.add(FieldBinding.create(name, valueGetter, valueSetter));
+        return this;
+    }
+
+    private <V> TypeSpecBuilder<T, BuilderT> bindRepeatedFieldInternal(
+            String name,
+            Function<T, List<V>> valueGetter,
+            BiConsumer<BuilderT, List<V>> valueSetter,
+            Function<V, Optional<Value>> toValue,
+            CheckedInterfaces.Function<Value, V> fromValue) {
+        return bindFieldInternal(
+                name,
+                /** valueGetter= */
+                object -> {
+                    if (valueGetter.apply(object).isEmpty()) {
+                        return Optional.empty();
+                    }
+                    return Optional.of(
+                            getListValue(
+                                    valueGetter.apply(object).stream()
+                                            .map(toValue)
+                                            .filter(Optional::isPresent)
+                                            .map(Optional::get)
+                                            .collect(toImmutableList())));
+                },
+                /** valueSetter= */
+                (builder, repeatedValue) -> {
+                    List<Value> values =
+                            repeatedValue
+                                    .map(Value::getListValue)
+                                    .map(ListValue::getValuesList)
+                                    .orElseGet(Collections::emptyList);
+                    List<V> convertedValues = new ArrayList<>();
+                    for (Value value : values) {
+                        convertedValues.add(fromValue.apply(value));
+                    }
+                    valueSetter.accept(builder, Collections.unmodifiableList(convertedValues));
+                });
+    }
+
+    /** binds a String field to read from / write to Struct */
+    TypeSpecBuilder<T, BuilderT> bindStringField(
+            String name,
+            Function<T, Optional<String>> stringGetter,
+            BiConsumer<BuilderT, String> stringSetter) {
+        return bindFieldInternal(
+                name,
+                (object) -> stringGetter.apply(object).map(TypeSpecBuilder::getStringValue),
+                (builder, value) ->
+                        value
+                                .map(Value::getStringValue)
+                                .ifPresent(
+                                        stringValue -> stringSetter.accept(builder, stringValue)));
+    }
+
+    /**
+     * Binds an enum field to read from / write to Struct. The enum will be represented as a string
+     * when converted to a Struct proto.
+     */
+    <E extends Enum<E>> TypeSpecBuilder<T, BuilderT> bindEnumField(
+            String name,
+            Function<T, Optional<E>> valueGetter,
+            BiConsumer<BuilderT, E> valueSetter,
+            Class<E> enumClass) {
+        return bindFieldInternal(
+                name,
+                (object) ->
+                        valueGetter.apply(object).map(Enum::toString).map(
+                                TypeSpecBuilder::getStringValue),
+                (builder, value) -> {
+                    if (value.isPresent()) {
+                        String stringValue = value.get().getStringValue();
+                        E[] enumValues = enumClass.getEnumConstants();
+                        if (enumValues != null) {
+                            for (E enumValue : enumValues) {
+                                if (enumValue.toString().equals(stringValue)) {
+                                    valueSetter.accept(builder, enumValue);
+                                    return;
+                                }
+                            }
+                        }
+                        throw new StructConversionException(
+                                String.format("Failed to get enum from string %s", stringValue));
+                    }
+                });
+    }
+
+    /**
+     * Binds a Duration field to read from / write to Struct. The Duration will be represented as an
+     * ISO 8601 string when converted to a Struct proto.
+     */
+    TypeSpecBuilder<T, BuilderT> bindDurationField(
+            String name,
+            Function<T, Optional<Duration>> valueGetter,
+            BiConsumer<BuilderT, Duration> valueSetter) {
+        return bindFieldInternal(
+                name,
+                (object) ->
+                        valueGetter.apply(object).map(Duration::toString).map(
+                                TypeSpecBuilder::getStringValue),
+                (builder, value) -> {
+                    if (value.isPresent()) {
+                        try {
+                            valueSetter.accept(builder,
+                                    Duration.parse(value.get().getStringValue()));
+                        } catch (DateTimeParseException e) {
+                            throw new StructConversionException(
+                                    "Failed to parse ISO 8601 string to Duration", e);
+                        }
+                    }
+                });
+    }
+
+    /**
+     * Binds a ZonedDateTime field to read from / write to Struct. The ZonedDateTime will be
+     * represented as an ISO 8601 string when converted to a Struct proto.
+     */
+    TypeSpecBuilder<T, BuilderT> bindZonedDateTimeField(
+            String name,
+            Function<T, Optional<ZonedDateTime>> valueGetter,
+            BiConsumer<BuilderT, ZonedDateTime> valueSetter) {
+        return bindFieldInternal(
+                name,
+                (object) ->
+                        valueGetter
+                                .apply(object)
+                                .map(ZonedDateTime::toOffsetDateTime)
+                                .map(OffsetDateTime::toString)
+                                .map(TypeSpecBuilder::getStringValue),
+                (builder, value) -> {
+                    if (value.isPresent()) {
+                        try {
+                            valueSetter.accept(builder,
+                                    ZonedDateTime.parse(value.get().getStringValue()));
+                        } catch (DateTimeParseException e) {
+                            throw new StructConversionException(
+                                    "Failed to parse ISO 8601 string to ZonedDateTime", e);
+                        }
+                    }
+                });
+    }
+
+    /** Binds a spec field to read from / write to Struct. */
+    <V> TypeSpecBuilder<T, BuilderT> bindSpecField(
+            String name,
+            Function<T, Optional<V>> valueGetter,
+            BiConsumer<BuilderT, V> valueSetter,
+            TypeSpec<V> spec) {
+        return bindFieldInternal(
+                name,
+                (object) ->
+                        valueGetter
+                                .apply(object)
+                                .map(
+                                        Function
+                                                .identity()) // Static analyzer incorrectly
+                                // throws error stating that the
+                                // input to toStruct is nullable. This is a workaround to avoid
+                                // the error from the analyzer.
+                                .map(spec::toStruct)
+                                .map(TypeSpecBuilder::getStructValue),
+                (builder, value) -> {
+                    if (value.isPresent()) {
+                        valueSetter.accept(builder, spec.fromStruct(value.get().getStructValue()));
+                    }
+                });
+    }
+
+    /** binds a repeated spec field to read from / write to Struct. */
+    <V> TypeSpecBuilder<T, BuilderT> bindRepeatedSpecField(
+            String name,
+            Function<T, List<V>> valueGetter,
+            BiConsumer<BuilderT, List<V>> valueSetter,
+            TypeSpec<V> spec) {
+        return bindRepeatedFieldInternal(
+                name,
+                valueGetter,
+                valueSetter,
+                (element) ->
+                        Optional.ofNullable(element).map(
+                                value -> getStructValue(spec.toStruct(value))),
+                (value) -> spec.fromStruct(value.getStructValue()));
+    }
+
+    TypeSpec<T> build() {
+        return new TypeSpecImpl<>(mBindings, mBuilderSupplier, Optional.ofNullable(mStructValidator));
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImpl.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImpl.java
new file mode 100644
index 0000000..13d36ed
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImpl.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.converters;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+/** TypeSpecImpl is used to convert between java objects in capabilities/values and Struct proto. */
+final class TypeSpecImpl<T, BuilderT extends BuilderOf<T>> implements TypeSpec<T> {
+
+    /** The list of FieldBinding objects. */
+    final List<FieldBinding<T, BuilderT>> mBindings;
+
+    /** Validates the Struct during conversion to java object. */
+    final Optional<CheckedInterfaces.Consumer<Struct>> mStructValidator;
+
+    /** Supplies BuilderT instances. */
+    final Supplier<BuilderT> mBuilderSupplier;
+
+    TypeSpecImpl(
+            List<FieldBinding<T, BuilderT>> bindings,
+            Supplier<BuilderT> builderSupplier,
+            Optional<CheckedInterfaces.Consumer<Struct>> structValidator) {
+        this.mBindings = Collections.unmodifiableList(bindings);
+        this.mBuilderSupplier = builderSupplier;
+        this.mStructValidator = structValidator;
+    }
+
+    /** Converts a java object into a Struct proto using List of FieldBinding. */
+    @NonNull
+    @Override
+    public Struct toStruct(@NonNull T object) {
+        Struct.Builder builder = Struct.newBuilder();
+        for (FieldBinding<T, BuilderT> binding : mBindings) {
+            binding
+                    .valueGetter()
+                    .apply(object)
+                    .ifPresent(value -> builder.putFields(binding.name(), value));
+        }
+        return builder.build();
+    }
+
+    /**
+     * Converts a Struct back into java object.
+     *
+     * @throws StructConversionException if the Struct is malformed.
+     */
+    @NonNull
+    @Override
+    public T fromStruct(@NonNull Struct struct) throws StructConversionException {
+        if (mStructValidator.isPresent()) {
+            mStructValidator.get().accept(struct);
+        }
+
+        BuilderT builder = mBuilderSupplier.get();
+        Map<String, Value> fieldsMap = struct.getFieldsMap();
+        for (FieldBinding<T, BuilderT> binding : mBindings) {
+            Optional<Value> value = Optional.ofNullable(fieldsMap.get(binding.name()));
+            binding.valueSetter().accept(builder, value);
+        }
+        return builder.build();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/exceptions/StructConversionException.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/exceptions/StructConversionException.java
new file mode 100644
index 0000000..be13634
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/exceptions/StructConversionException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.exceptions;
+
+import androidx.annotation.Nullable;
+
+/** Represents exceptions that happen during object conversion to/from Struct proto. */
+public class StructConversionException extends Exception {
+    public StructConversionException(@Nullable String message) {
+        super(message);
+    }
+
+    public StructConversionException(@Nullable String message, @Nullable Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionCapabilityImpl.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionCapabilityImpl.java
new file mode 100644
index 0000000..b4f281b
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionCapabilityImpl.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.spec;
+
+import static androidx.appactions.interaction.capabilities.core.impl.utils.ImmutableCollectors.toImmutableList;
+import static androidx.appactions.interaction.capabilities.core.impl.utils.ImmutableCollectors.toImmutableMap;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.ActionExecutor;
+import androidx.appactions.interaction.capabilities.core.ExecutionResult;
+import androidx.appactions.interaction.capabilities.core.impl.ActionCapabilityInternal;
+import androidx.appactions.interaction.capabilities.core.impl.ArgumentsWrapper;
+import androidx.appactions.interaction.capabilities.core.impl.CallbackInternal;
+import androidx.appactions.interaction.capabilities.core.impl.ErrorStatusInternal;
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+import androidx.appactions.interaction.capabilities.core.impl.utils.CapabilityLogger;
+import androidx.appactions.interaction.capabilities.core.impl.utils.LoggerInternal;
+import androidx.appactions.interaction.proto.AppActionsContext.AppAction;
+import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.FulfillmentValue;
+import androidx.appactions.interaction.proto.FulfillmentResponse;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * The implementation of the {@link ActionCapability} interface.
+ *
+ * @param <PropertyT>
+ * @param <ArgumentT>
+ * @param <OutputT>
+ */
+public final class ActionCapabilityImpl<PropertyT, ArgumentT, OutputT>
+        implements ActionCapabilityInternal {
+
+    private static final String LOG_TAG = "ActionCapability";
+    private final ActionSpec<PropertyT, ArgumentT, OutputT> mActionSpec;
+    private final Optional<String> mIdentifier;
+    private final PropertyT mProperty;
+    private final ActionExecutor<ArgumentT, OutputT> mActionExecutor;
+
+    public ActionCapabilityImpl(
+            @NonNull ActionSpec<PropertyT, ArgumentT, OutputT> actionSpec,
+            @NonNull Optional<String> identifier,
+            @NonNull PropertyT property,
+            @NonNull ActionExecutor<ArgumentT, OutputT> actionExecutor) {
+        this.mActionSpec = actionSpec;
+        this.mIdentifier = identifier;
+        this.mProperty = property;
+        this.mActionExecutor = actionExecutor;
+    }
+
+    @NonNull
+    @Override
+    public Optional<String> getId() {
+        return mIdentifier;
+    }
+
+    @NonNull
+    @Override
+    public AppAction getAppAction() {
+        AppAction appAction = mActionSpec.convertPropertyToProto(mProperty);
+        if (mIdentifier.isPresent()) {
+            appAction = appAction.toBuilder().setIdentifier(mIdentifier.get()).build();
+        }
+        return appAction;
+    }
+
+    @Override
+    public void execute(
+            @NonNull ArgumentsWrapper argumentsWrapper, @NonNull CallbackInternal callback) {
+        // Filter out the task parts of ArgumentsWrapper. Send the raw arguments for one-shot
+        // capabilities.
+        Map<String, List<ParamValue>> args =
+                argumentsWrapper.paramValues().entrySet().stream()
+                        .collect(
+                                toImmutableMap(
+                                        Map.Entry::getKey,
+                                        e ->
+                                                e.getValue().stream()
+                                                        .filter(FulfillmentValue::hasValue)
+                                                        .map(FulfillmentValue::getValue)
+                                                        .collect(toImmutableList())));
+        try {
+            mActionExecutor.execute(mActionSpec.buildArgument(args), convertCallback(callback));
+        } catch (StructConversionException e) {
+            if (e.getMessage() != null) {
+                LoggerInternal.log(CapabilityLogger.LogLevel.ERROR, LOG_TAG, e.getMessage());
+            }
+            callback.onError(ErrorStatusInternal.STRUCT_CONVERSION_FAILURE);
+        }
+    }
+
+    /** Converts {@link CallbackInternal} to {@link ActionExecutor.ActionCallback}. */
+    private ActionExecutor.ActionCallback<OutputT> convertCallback(CallbackInternal callback) {
+        return new ActionExecutor.ActionCallback<OutputT>() {
+            @Override
+            public void onSuccess(ExecutionResult<OutputT> executionResult) {
+                callback.onSuccess(convertToFulfillmentResponse(executionResult));
+            }
+
+            @Override
+            public void onError(ActionExecutor.ErrorStatus errorStatus) {
+                switch (errorStatus) {
+                    case CANCELLED:
+                        callback.onError(ErrorStatusInternal.CANCELLED);
+                        break;
+                    case TIMEOUT:
+                        callback.onError(ErrorStatusInternal.TIMEOUT);
+                }
+            }
+        };
+    }
+
+    /** Converts typed {@link ExecutionResult} to {@link FulfillmentResponse} proto. */
+    FulfillmentResponse convertToFulfillmentResponse(ExecutionResult<OutputT> executionResult) {
+        FulfillmentResponse.Builder fulfillmentResponseBuilder =
+                FulfillmentResponse.newBuilder().setStartDictation(
+                        executionResult.getStartDictation());
+        OutputT output = executionResult.getOutput();
+        if (output != null && !(output instanceof Void)) {
+            fulfillmentResponseBuilder.setExecutionOutput(mActionSpec.convertOutputToProto(output));
+        }
+        return fulfillmentResponseBuilder.build();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpec.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpec.java
new file mode 100644
index 0000000..182cef8
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpec.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.spec;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+import androidx.appactions.interaction.proto.AppActionsContext.AppAction;
+import androidx.appactions.interaction.proto.FulfillmentResponse.StructuredOutput;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A specification for an action, describing it from the app's point of view.
+ *
+ * @param <PropertyT> typed description of action's characteristics.
+ * @param <ArgumentT> typed representation of action's arguments.
+ * @param <OutputT>   typed action's execution output.
+ */
+public interface ActionSpec<PropertyT, ArgumentT, OutputT> {
+
+    /** Converts the property to the {@code AppAction} proto. */
+    @NonNull
+    AppAction convertPropertyToProto(PropertyT property);
+
+    /** Builds this action's arguments from an ArgumentsWrapper instance. */
+    @NonNull
+    ArgumentT buildArgument(@NonNull Map<String, List<ParamValue>> args)
+            throws StructConversionException;
+
+    /** Converts the output to the {@code StructuredOutput} proto. */
+    @NonNull
+    StructuredOutput convertOutputToProto(OutputT output);
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecBuilder.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecBuilder.java
new file mode 100644
index 0000000..209d7ff
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecBuilder.java
@@ -0,0 +1,611 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.spec;
+
+import static androidx.appactions.interaction.capabilities.core.impl.utils.ImmutableCollectors.toImmutableList;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter;
+import androidx.appactions.interaction.capabilities.core.impl.converters.PropertyConverter;
+import androidx.appactions.interaction.capabilities.core.impl.converters.SlotTypeConverter;
+import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters;
+import androidx.appactions.interaction.capabilities.core.impl.spec.ParamBinding.ArgumentSetter;
+import androidx.appactions.interaction.capabilities.core.properties.EntityProperty;
+import androidx.appactions.interaction.capabilities.core.properties.EnumProperty;
+import androidx.appactions.interaction.capabilities.core.properties.IntegerProperty;
+import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringOrEnumProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
+import androidx.appactions.interaction.capabilities.core.values.EntityValue;
+import androidx.appactions.interaction.capabilities.core.values.StringOrEnumValue;
+import androidx.appactions.interaction.proto.AppActionsContext.IntentParameter;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * A builder for the {@code ActionSpec}.
+ *
+ * @param <PropertyT>
+ * @param <ArgumentT>
+ * @param <ArgumentBuilderT>
+ * @param <OutputT>
+ */
+public final class ActionSpecBuilder<
+        PropertyT, ArgumentT, ArgumentBuilderT extends BuilderOf<ArgumentT>, OutputT> {
+
+    private final String mCapabilityName;
+    private final Supplier<ArgumentBuilderT> mArgumentBuilderSupplier;
+    private final ArrayList<ParamBinding<PropertyT, ArgumentT, ArgumentBuilderT>>
+            mParamBindingList = new ArrayList<>();
+    private final Map<String, Function<OutputT, List<ParamValue>>> mOutputBindings =
+            new HashMap<>();
+
+    private ActionSpecBuilder(
+            String capabilityName, Supplier<ArgumentBuilderT> argumentBuilderSupplier) {
+        this.mCapabilityName = capabilityName;
+        this.mArgumentBuilderSupplier = argumentBuilderSupplier;
+    }
+
+    /**
+     * Creates an empty {@code ActionSpecBuilder} with the given capability name. ArgumentT is set
+     * to Object as a placeholder, which must be replaced by calling setArgument.
+     */
+    @NonNull
+    public static ActionSpecBuilder<Void, Object, BuilderOf<Object>, Void> ofCapabilityNamed(
+            @NonNull String capabilityName) {
+        return new ActionSpecBuilder<>(capabilityName, () -> Object::new);
+    }
+
+    /** Sets the property type and returns a new {@code ActionSpecBuilder}. */
+    @NonNull
+    public <NewPropertyT>
+            ActionSpecBuilder<NewPropertyT, ArgumentT, ArgumentBuilderT, OutputT> setDescriptor(
+                    @NonNull Class<NewPropertyT> unused) {
+        return new ActionSpecBuilder<>(this.mCapabilityName, this.mArgumentBuilderSupplier);
+    }
+
+    /** Sets the argument type and its builder and returns a new {@code ActionSpecBuilder}. */
+    @NonNull
+    public <NewArgumentT, NewArgumentBuilderT extends BuilderOf<NewArgumentT>>
+            ActionSpecBuilder<PropertyT, NewArgumentT, NewArgumentBuilderT, OutputT> setArgument(
+                    @NonNull Class<NewArgumentT> unused,
+                    @NonNull Supplier<NewArgumentBuilderT> argumentBuilderSupplier) {
+        return new ActionSpecBuilder<>(this.mCapabilityName, argumentBuilderSupplier);
+    }
+
+    @NonNull
+    public <NewOutputT>
+            ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, NewOutputT> setOutput(
+                    @NonNull Class<NewOutputT> unused) {
+        return new ActionSpecBuilder<>(this.mCapabilityName, this.mArgumentBuilderSupplier);
+    }
+
+    /**
+     * Binds the parameter name, getter and setter.
+     *
+     * @param paramName the name of this action' parameter.
+     * @param paramGetter a getter of the param-specific info from the property.
+     * @param argumentSetter a setter to the argument with the input from {@code ParamValue}.
+     * @return the builder itself.
+     */
+    @NonNull
+    public ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT> bindParameter(
+            @NonNull String paramName,
+            @NonNull Function<? super PropertyT, Optional<IntentParameter>> paramGetter,
+            @NonNull ArgumentSetter<ArgumentBuilderT> argumentSetter) {
+        mParamBindingList.add(ParamBinding.create(paramName, paramGetter, argumentSetter));
+        return this;
+    }
+
+    /**
+     * Binds the parameter name, getter, and setter for a {@link EntityProperty}.
+     *
+     * <p>This parameter is required for any capability built from the generated {@link ActionSpec}.
+     *
+     * @param paramName the name of this action' parameter.
+     * @param propertyGetter a getter of the EntityProperty from the property, which must be able to
+     *     fetch a non-null {@code EntityProperty} from {@code PropertyT}.
+     * @param paramConsumer a setter to set the string value in the argument builder.
+     * @return the builder itself.
+     */
+    @NonNull
+    public ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT>
+            bindRequiredEntityParameter(
+                    @NonNull String paramName,
+                    @NonNull Function<? super PropertyT, EntityProperty> propertyGetter,
+                    @NonNull BiConsumer<? super ArgumentBuilderT, EntityValue> paramConsumer) {
+        return bindEntityParameter(
+                paramName,
+                property ->
+                        Optional.of(
+                                PropertyConverter.getIntentParameter(
+                                        paramName, propertyGetter.apply(property))),
+                paramConsumer);
+    }
+
+    /**
+     * Binds the parameter name, getter, and setter for a {@link EntityProperty}.
+     *
+     * <p>This parameter is optional for any capability built from the generated {@link ActionSpec}.
+     * If the Property Optional is not set, this parameter will not exist in the parameter
+     * definition of the capability.
+     *
+     * @param paramName the name of this action' parameter.
+     * @param optionalPropertyGetter an optional getter of the EntityProperty from the property,
+     *     which may be able to fetch a non-null {@code EntityProperty} from {@code PropertyT}, or
+     *     get {@link Optional#empty}.
+     * @param paramConsumer a setter to set the string value in the argument builder.
+     * @return the builder itself.
+     */
+    @NonNull
+    public ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT>
+            bindOptionalEntityParameter(
+                    @NonNull String paramName,
+                    @NonNull
+                            Function<? super PropertyT, Optional<EntityProperty>>
+                                    optionalPropertyGetter,
+                    @NonNull BiConsumer<? super ArgumentBuilderT, EntityValue> paramConsumer) {
+        return bindEntityParameter(
+                paramName,
+                property ->
+                        optionalPropertyGetter
+                                .apply(property)
+                                .map(p -> PropertyConverter.getIntentParameter(paramName, p)),
+                paramConsumer);
+    }
+
+    /**
+     * This is similar to {@link ActionSpectBuilder#bindOptionalEntityParameter} but for setting a
+     * list of entities instead.
+     *
+     * <p>This parameter is optional for any capability built from the generated {@link ActionSpec}.
+     * If the Property Optional is not set, this parameter will not exist in the parameter
+     * definition of the capability.
+     */
+    @NonNull
+    public ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT>
+            bindRepeatedEntityParameter(
+                    @NonNull String paramName,
+                    @NonNull
+                            Function<? super PropertyT, Optional<EntityProperty>>
+                                    optionalPropertyGetter,
+                    @NonNull
+                            BiConsumer<? super ArgumentBuilderT, List<EntityValue>> paramConsumer) {
+        return bindParameter(
+                paramName,
+                property ->
+                        optionalPropertyGetter
+                                .apply(property)
+                                .map(p -> PropertyConverter.getIntentParameter(paramName, p)),
+                (argBuilder, paramList) ->
+                        paramConsumer.accept(
+                                argBuilder,
+                                SlotTypeConverter.ofRepeated(TypeConverters::toEntityValue)
+                                        .convert(paramList)));
+    }
+
+    private ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT> bindEntityParameter(
+            String paramName,
+            Function<? super PropertyT, Optional<IntentParameter>> propertyGetter,
+            BiConsumer<? super ArgumentBuilderT, EntityValue> paramConsumer) {
+        return bindParameter(
+                paramName,
+                propertyGetter,
+                (argBuilder, paramList) -> {
+                    if (!paramList.isEmpty()) {
+                        paramConsumer.accept(
+                                argBuilder,
+                                SlotTypeConverter.ofSingular(TypeConverters::toEntityValue)
+                                        .convert(paramList));
+                    }
+                });
+    }
+
+    @NonNull
+    public <T>
+            ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT> bindStructParameter(
+                    @NonNull String paramName,
+                    @NonNull
+                            Function<? super PropertyT, Optional<SimpleProperty>>
+                                    optionalPropertyGetter,
+                    @NonNull BiConsumer<? super ArgumentBuilderT, T> paramConsumer,
+                    @NonNull ParamValueConverter<T> paramValueConverter) {
+        return bindParameter(
+                paramName,
+                property ->
+                        optionalPropertyGetter
+                                .apply(property)
+                                .map(p -> PropertyConverter.getIntentParameter(paramName, p)),
+                (argBuilder, paramList) ->
+                        paramConsumer.accept(
+                                argBuilder,
+                                SlotTypeConverter.ofSingular(paramValueConverter)
+                                        .convert(paramList)));
+    }
+
+    @NonNull
+    public <T>
+            ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT>
+                    bindRepeatedStructParameter(
+                            @NonNull String paramName,
+                            @NonNull
+                                    Function<? super PropertyT, Optional<SimpleProperty>>
+                                            optionalPropertyGetter,
+                            @NonNull BiConsumer<? super ArgumentBuilderT, List<T>> paramConsumer,
+                            @NonNull ParamValueConverter<T> paramValueConverter) {
+        return bindParameter(
+                paramName,
+                property ->
+                        optionalPropertyGetter
+                                .apply(property)
+                                .map(p -> PropertyConverter.getIntentParameter(paramName, p)),
+                (argBuilder, paramList) ->
+                        paramConsumer.accept(
+                                argBuilder,
+                                SlotTypeConverter.ofRepeated(paramValueConverter)
+                                        .convert(paramList)));
+    }
+
+    /**
+     * Binds an optional string parameter.
+     *
+     * @param paramName the BII slot name of this parameter.
+     * @param propertyGetter a function that returns a {@code Optional<StringProperty>} given a
+     *     {@code PropertyT} instance
+     * @param paramConsumer a function that accepts a String into the argument builder.
+     */
+    @NonNull
+    public ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT>
+            bindOptionalStringParameter(
+                    @NonNull String paramName,
+                    @NonNull Function<? super PropertyT, Optional<StringProperty>> propertyGetter,
+                    @NonNull BiConsumer<? super ArgumentBuilderT, String> paramConsumer) {
+        return bindParameter(
+                paramName,
+                property ->
+                        propertyGetter
+                                .apply(property)
+                                .map(
+                                        stringProperty ->
+                                                PropertyConverter.getIntentParameter(
+                                                        paramName, stringProperty)),
+                (argBuilder, paramList) -> {
+                    if (!paramList.isEmpty()) {
+                        paramConsumer.accept(
+                                argBuilder,
+                                SlotTypeConverter.ofSingular(TypeConverters::toStringValue)
+                                        .convert(paramList));
+                    }
+                });
+    }
+
+    /**
+     * Binds an required string parameter.
+     *
+     * @param paramName the BII slot name of this parameter.
+     * @param propertyGetter a function that returns a {@code StringProperty} given a {@code
+     *     PropertyT} instance
+     * @param paramConsumer a function that accepts a String into the argument builder.
+     */
+    @NonNull
+    public ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT>
+            bindRequiredStringParameter(
+                    @NonNull String paramName,
+                    @NonNull Function<? super PropertyT, StringProperty> propertyGetter,
+                    @NonNull BiConsumer<? super ArgumentBuilderT, String> paramConsumer) {
+        return bindOptionalStringParameter(
+                paramName, property -> Optional.of(propertyGetter.apply(property)), paramConsumer);
+    }
+
+    /**
+     * Binds an repeated string parameter.
+     *
+     * @param paramName the BII slot name of this parameter.
+     * @param propertyGetter a function that returns a {@code Optional<StringProperty>} given a
+     *     {@code PropertyT} instance
+     * @param paramConsumer a function that accepts a {@code List<String>} into the argument
+     *     builder.
+     */
+    @NonNull
+    public ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT>
+            bindRepeatedStringParameter(
+                    @NonNull String paramName,
+                    @NonNull Function<? super PropertyT, Optional<StringProperty>> propertyGetter,
+                    @NonNull BiConsumer<? super ArgumentBuilderT, List<String>> paramConsumer) {
+        return bindParameter(
+                paramName,
+                property ->
+                        propertyGetter
+                                .apply(property)
+                                .map(
+                                        stringProperty ->
+                                                PropertyConverter.getIntentParameter(
+                                                        paramName, stringProperty)),
+                (argBuilder, paramList) -> {
+                    if (!paramList.isEmpty()) {
+                        paramConsumer.accept(
+                                argBuilder,
+                                SlotTypeConverter.ofRepeated(TypeConverters::toStringValue)
+                                        .convert(paramList));
+                    }
+                });
+    }
+
+    /**
+     * Binds the parameter name, getter, and setter for a {@link EnumProperty}.
+     *
+     * <p>This parameter is optional for any capability built from the generated {@link ActionSpec}.
+     * If the Property Optional is not set, this parameter will not exist in the parameter
+     * definition of the capability.
+     *
+     * @param paramName the name of this action parameter.
+     * @param enumType
+     * @param optionalPropertyGetter an optional getter of the EntityProperty from the property,
+     *     which may be able to fetch a non-null {@code EnumProperty} from {@code PropertyT}, or get
+     *     {@link Optional#empty}.
+     * @param paramConsumer a setter to set the enum value in the argument builder.
+     * @return the builder itself.
+     * @param <EnumT>
+     */
+    @NonNull
+    public <EnumT extends Enum<EnumT>>
+            ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT>
+                    bindOptionalEnumParameter(
+                            @NonNull String paramName,
+                            @NonNull Class<EnumT> enumType,
+                            @NonNull
+                                    Function<? super PropertyT, Optional<EnumProperty<EnumT>>>
+                                            optionalPropertyGetter,
+                            @NonNull BiConsumer<? super ArgumentBuilderT, EnumT> paramConsumer) {
+        return bindEnumParameter(
+                paramName,
+                enumType,
+                property ->
+                        optionalPropertyGetter
+                                .apply(property)
+                                .map(e -> PropertyConverter.getIntentParameter(paramName, e)),
+                paramConsumer);
+    }
+
+    private <EnumT extends Enum<EnumT>>
+            ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT> bindEnumParameter(
+                    String paramName,
+                    Class<EnumT> enumType,
+                    Function<? super PropertyT, Optional<IntentParameter>> enumParamGetter,
+                    BiConsumer<? super ArgumentBuilderT, EnumT> paramConsumer) {
+        return bindParameter(
+                paramName,
+                enumParamGetter,
+                (argBuilder, paramList) -> {
+                    if (!paramList.isEmpty()) {
+                        Optional<EnumT> enumValue =
+                                EnumSet.allOf(enumType).stream()
+                                        .filter(
+                                                element ->
+                                                        element.toString()
+                                                                .equals(
+                                                                        paramList
+                                                                                .get(0)
+                                                                                .getIdentifier()))
+                                        .findFirst();
+                        if (enumValue.isPresent()) {
+                            paramConsumer.accept(argBuilder, enumValue.get());
+                        }
+                    }
+                });
+    }
+
+    @NonNull
+    public <EnumT extends Enum<EnumT>>
+            ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT>
+                    bindRequiredStringOrEnumParameter(
+                            @NonNull String paramName,
+                            @NonNull Class<EnumT> enumType,
+                            @NonNull
+                                    Function<? super PropertyT, StringOrEnumProperty<EnumT>>
+                                            propertyGetter,
+                            @NonNull
+                                    BiConsumer<? super ArgumentBuilderT, StringOrEnumValue<EnumT>>
+                                            paramConsumer) {
+        return bindStringOrEnumParameter(
+                paramName,
+                enumType,
+                property ->
+                        Optional.of(
+                                PropertyConverter.getIntentParameter(
+                                        paramName, propertyGetter.apply(property))),
+                paramConsumer);
+    }
+
+    private <EnumT extends Enum<EnumT>>
+            ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT>
+                    bindStringOrEnumParameter(
+                            String paramName,
+                            Class<EnumT> enumType,
+                            Function<? super PropertyT, Optional<IntentParameter>> propertyGetter,
+                            BiConsumer<? super ArgumentBuilderT, StringOrEnumValue<EnumT>>
+                                    paramConsumer) {
+        return bindParameter(
+                paramName,
+                propertyGetter,
+                (argBuilder, paramList) -> {
+                    if (!paramList.isEmpty()) {
+                        ParamValue param = paramList.get(0);
+                        if (param.hasIdentifier()) {
+                            Optional<EnumT> enumValue =
+                                    EnumSet.allOf(enumType).stream()
+                                            .filter(
+                                                    element ->
+                                                            element.toString()
+                                                                    .equals(param.getIdentifier()))
+                                            .findFirst();
+                            if (enumValue.isPresent()) {
+                                paramConsumer.accept(
+                                        argBuilder, StringOrEnumValue.ofEnumValue(enumValue.get()));
+                                return;
+                            }
+                        }
+                        paramConsumer.accept(
+                                argBuilder,
+                                StringOrEnumValue.ofStringValue(
+                                        SlotTypeConverter.ofSingular(TypeConverters::toStringValue)
+                                                .convert(paramList)));
+                    }
+                });
+    }
+
+    /**
+     * Binds the integer parameter name and setter for a {@link IntegerProperty}.
+     *
+     * <p>This parameter is optional for any capability built from the generated {@link ActionSpec}.
+     * If the Property Optional is not set, this parameter will not exist in the parameter
+     * definition of the capability.
+     *
+     * @param paramName the name of this action' parameter.
+     * @param optionalPropertyGetter
+     * @param paramConsumer a setter to set the int value in the argument builder.
+     * @return the builder itself.
+     */
+    @NonNull
+    public ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT>
+            bindOptionalIntegerParameter(
+                    @NonNull String paramName,
+                    @NonNull
+                            Function<? super PropertyT, Optional<IntegerProperty>>
+                                    optionalPropertyGetter,
+                    @NonNull BiConsumer<? super ArgumentBuilderT, Integer> paramConsumer) {
+        return bindParameter(
+                paramName,
+                property ->
+                        optionalPropertyGetter
+                                .apply(property)
+                                .map(e -> PropertyConverter.getIntentParameter(paramName, e)),
+                (argBuilder, paramList) -> {
+                    if (!paramList.isEmpty()) {
+                        paramConsumer.accept(
+                                argBuilder,
+                                SlotTypeConverter.ofSingular(TypeConverters::toIntegerValue)
+                                        .convert(paramList));
+                    }
+                });
+    }
+
+    /**
+     * Binds a Boolean parameter.
+     *
+     * <p>This parameter is optional for any capability built from the generated {@link ActionSpec}.
+     * If the Property Optional is not set, this parameter will not exist in the parameter
+     * definition of the capability.
+     *
+     * @param paramName the name of this action' parameter.
+     * @param paramConsumer a setter to set the boolean value in the argument builder.
+     * @param optionalPropertyGetter an optional getter of the EntityProperty from the property,
+     *     which may be able to fetch a non-null {@code SimpleProperty} from {@code PropertyT}, or
+     *     get {@link Optional#empty}.
+     * @return the builder itself.
+     */
+    @NonNull
+    public ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT>
+            bindOptionalBooleanParameter(
+                    @NonNull String paramName,
+                    @NonNull
+                            Function<? super PropertyT, Optional<SimpleProperty>>
+                                    optionalPropertyGetter,
+                    @NonNull BiConsumer<? super ArgumentBuilderT, Boolean> paramConsumer) {
+        return bindParameter(
+                paramName,
+                property ->
+                        optionalPropertyGetter
+                                .apply(property)
+                                .map(e -> PropertyConverter.getIntentParameter(paramName, e)),
+                (argBuilder, paramList) -> {
+                    if (!paramList.isEmpty()) {
+                        paramConsumer.accept(
+                                argBuilder,
+                                SlotTypeConverter.ofSingular(TypeConverters::toBooleanValue)
+                                        .convert(paramList));
+                    }
+                });
+    }
+
+    /**
+     * Binds an optional output.
+     *
+     * @param name the BII output slot name of this parameter.
+     * @param outputGetter a getter of the output from the {@code OutputT} instance.
+     * @param converter a converter from an output object to a ParamValue.
+     */
+    @NonNull
+    @SuppressWarnings("JdkCollectors")
+    public <T>
+            ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT> bindOptionalOutput(
+                    @NonNull String name,
+                    @NonNull Function<OutputT, Optional<T>> outputGetter,
+                    @NonNull Function<T, ParamValue> converter) {
+        mOutputBindings.put(
+                name,
+                output ->
+                        outputGetter.apply(output).stream()
+                                .map(converter)
+                                .collect(toImmutableList()));
+        return this;
+    }
+
+    /**
+     * Binds a repeated output.
+     *
+     * @param name the BII output slot name of this parameter.
+     * @param outputGetter a getter of the output from the {@code OutputT} instance.
+     * @param converter a converter from an output object to a ParamValue.
+     */
+    @NonNull
+    @SuppressWarnings("JdkCollectors")
+    public <T>
+            ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT> bindRepeatedOutput(
+                    @NonNull String name,
+                    @NonNull Function<OutputT, List<T>> outputGetter,
+                    @NonNull Function<T, ParamValue> converter) {
+        mOutputBindings.put(
+                name,
+                output ->
+                        outputGetter.apply(output).stream()
+                                .map(converter)
+                                .collect(toImmutableList()));
+        return this;
+    }
+
+    /** Builds an {@code ActionSpec} from this builder. */
+    @NonNull
+    public ActionSpec<PropertyT, ArgumentT, OutputT> build() {
+        return new ActionSpecImpl<>(
+                mCapabilityName,
+                mArgumentBuilderSupplier,
+                Collections.unmodifiableList(mParamBindingList),
+                mOutputBindings);
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecImpl.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecImpl.java
new file mode 100644
index 0000000..de3aa6a
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecImpl.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.spec;
+
+import static androidx.appactions.interaction.capabilities.core.impl.utils.ImmutableCollectors.toImmutableList;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+import androidx.appactions.interaction.proto.AppActionsContext.AppAction;
+import androidx.appactions.interaction.proto.FulfillmentResponse.StructuredOutput;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/** The implementation of {@code ActionSpec} interface. */
+final class ActionSpecImpl<
+        PropertyT, ArgumentT, ArgumentBuilderT extends BuilderOf<ArgumentT>, OutputT>
+        implements ActionSpec<PropertyT, ArgumentT, OutputT> {
+
+    private final String mCapabilityName;
+    private final Supplier<ArgumentBuilderT> mArgumentBuilderSupplier;
+    private final List<ParamBinding<PropertyT, ArgumentT, ArgumentBuilderT>> mParamBindingList;
+    private final Map<String, Function<OutputT, List<ParamValue>>> mOutputBindings;
+
+    ActionSpecImpl(
+            String capabilityName,
+            Supplier<ArgumentBuilderT> argumentBuilderSupplier,
+            List<ParamBinding<PropertyT, ArgumentT, ArgumentBuilderT>> paramBindingList,
+            Map<String, Function<OutputT, List<ParamValue>>> outputBindings) {
+        this.mCapabilityName = capabilityName;
+        this.mArgumentBuilderSupplier = argumentBuilderSupplier;
+        this.mParamBindingList = paramBindingList;
+        this.mOutputBindings = outputBindings;
+    }
+
+    @NonNull
+    @Override
+    public AppAction convertPropertyToProto(PropertyT property) {
+        return AppAction.newBuilder()
+                .setName(mCapabilityName)
+                .addAllParams(
+                        mParamBindingList.stream()
+                                .map(binding -> binding.paramGetter().apply(property))
+                                .filter(Optional::isPresent)
+                                .map(Optional::get)
+                                .collect(toImmutableList()))
+                .build();
+    }
+
+    @NonNull
+    @Override
+    public ArgumentT buildArgument(Map<String, List<ParamValue>> args)
+            throws StructConversionException {
+        ArgumentBuilderT argumentBuilder = mArgumentBuilderSupplier.get();
+        for (ParamBinding<PropertyT, ArgumentT, ArgumentBuilderT> binding : mParamBindingList) {
+            List<ParamValue> paramValues = args.get(binding.name());
+            if (paramValues == null) {
+                continue;
+            }
+            try {
+                binding.argumentSetter().setArgument(argumentBuilder, paramValues);
+            } catch (StructConversionException e) {
+                // Wrap the exception with a more meaningful error message.
+                throw new StructConversionException(
+                        String.format(
+                                "Failed to parse parameter '%s' from assistant because of "
+                                        + "failure: %s",
+                                binding.name(), e.getMessage()));
+            }
+        }
+        return argumentBuilder.build();
+    }
+
+    @Override
+    public StructuredOutput convertOutputToProto(OutputT output) {
+        StructuredOutput.Builder outputBuilder = StructuredOutput.newBuilder();
+        for (Map.Entry<String, Function<OutputT, List<ParamValue>>> entry :
+                mOutputBindings.entrySet()) {
+            outputBuilder.addOutputValues(
+                    StructuredOutput.OutputValue.newBuilder()
+                            .setName(entry.getKey())
+                            .addAllValues(entry.getValue().apply(output))
+                            .build());
+        }
+        return outputBuilder.build();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ParamBinding.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ParamBinding.java
new file mode 100644
index 0000000..bdd5e08
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ParamBinding.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.spec;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+import androidx.appactions.interaction.proto.AppActionsContext.IntentParameter;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * A binding between a parameter and its Property converter / Argument setter.
+ *
+ * @param <PropertyT>
+ * @param <ArgumentT>
+ * @param <ArgumentBuilderT>
+ */
+@AutoValue
+public abstract class ParamBinding<
+        PropertyT, ArgumentT, ArgumentBuilderT extends BuilderOf<ArgumentT>> {
+
+    static <PropertyT, ArgumentT, ArgumentBuilderT extends BuilderOf<ArgumentT>>
+            ParamBinding<PropertyT, ArgumentT, ArgumentBuilderT> create(
+                    String name,
+                    Function<? super PropertyT, Optional<IntentParameter>> paramGetter,
+                    ArgumentSetter<ArgumentBuilderT> argumentSetter) {
+        return new AutoValue_ParamBinding<>(name, paramGetter, argumentSetter);
+    }
+
+    /** Returns the name of this param. */
+    @NonNull
+    public abstract String name();
+
+    /**
+     * Converts a {@code PropertyT} to an {@code IntentParameter} proto. The resulting proto is the
+     * format which we send the current params to Assistant (via. app actions context).
+     */
+    @NonNull
+    public abstract Function<? super PropertyT, Optional<IntentParameter>> paramGetter();
+
+    /**
+     * Populates the {@code ArgumentBuilderT} for this param with the {@code ParamValue} sent from
+     * Assistant in Fulfillment.
+     */
+    @NonNull
+    public abstract ArgumentSetter<ArgumentBuilderT> argumentSetter();
+
+    /**
+     * Givne a {@code List<ParamValue>}, convert it to user-visible type and set it into
+     * ArgumentBuilder.
+     *
+     * @param <ArgumentBuilderT>
+     */
+    @FunctionalInterface
+    public interface ArgumentSetter<ArgumentBuilderT> {
+        void setArgument(@NonNull ArgumentBuilderT builder, @NonNull List<ParamValue> paramValues)
+                throws StructConversionException;
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/utils/CapabilityLogger.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/utils/CapabilityLogger.java
new file mode 100644
index 0000000..48ff8a4
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/utils/CapabilityLogger.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.utils;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Define the logger interface to hide specific logger implementations from the capability library.
+ * This is needed because the capabilities library cannot depend on android deps directly.
+ */
+public interface CapabilityLogger {
+
+    void log(@NonNull LogLevel logLevel, @NonNull String logTag, @NonNull String message);
+
+    void log(
+            @NonNull LogLevel logLevel,
+            @NonNull String logTag,
+            @NonNull String message,
+            @NonNull Throwable throwable);
+
+    /** Log levels to match the visibility of log errors from android.util.Log. */
+    enum LogLevel {
+        INFO,
+        WARN,
+        ERROR,
+    }
+
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/utils/ImmutableCollectors.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/utils/ImmutableCollectors.java
new file mode 100644
index 0000000..8f30f68
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/utils/ImmutableCollectors.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.utils;
+
+import static java.util.stream.Collectors.collectingAndThen;
+
+import androidx.annotation.NonNull;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+
+/**
+ * Immutable collectors without guava dependencies. Collectors.toUnmodifiable*() function calls
+ * should not be used because they are only available on API 33+.
+ */
+public final class ImmutableCollectors {
+
+    private ImmutableCollectors() {
+    }
+
+    /** Collecting to immutable list. */
+    @NonNull
+    public static <E> Collector<E, ?, List<E>> toImmutableList() {
+        return collectingAndThen(Collectors.toList(), Collections::unmodifiableList);
+    }
+
+    /** Collecting to immutable set. */
+    @NonNull
+    public static <E> Collector<E, ?, Set<E>> toImmutableSet() {
+        return collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet);
+    }
+
+    /** Collecting to immutable map. */
+    @NonNull
+    public static <T, K, V> Collector<T, ?, Map<K, V>> toImmutableMap(
+            @NonNull Function<? super T, ? extends K> keyFunction,
+            @NonNull Function<? super T, ? extends V> valueFunction) {
+        return collectingAndThen(
+                Collectors.toMap(keyFunction, valueFunction), Collections::unmodifiableMap);
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/utils/LoggerInternal.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/utils/LoggerInternal.java
new file mode 100644
index 0000000..7a9d7d6
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/utils/LoggerInternal.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.utils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appactions.interaction.capabilities.core.impl.utils.CapabilityLogger.LogLevel;
+
+/**
+ * A singleton class to define the logger allows logger implementations to be defined outside of the
+ * capabilities classes, where it is permitted to depend on Android Logger(android.util.Log).
+ */
+public final class LoggerInternal {
+
+    @Nullable
+    private static CapabilityLogger sDelegate = null;
+
+    private LoggerInternal() {
+    }
+
+    public static void setLogger(@NonNull CapabilityLogger logger) {
+        sDelegate = logger;
+    }
+
+    public static void log(
+            @NonNull LogLevel logLevel, @NonNull String logTag, @NonNull String message) {
+        if (sDelegate != null) {
+            sDelegate.log(logLevel, logTag, message);
+        }
+    }
+
+    public static void log(
+            @NonNull LogLevel logLevel,
+            @NonNull String logTag,
+            @NonNull String message,
+            @NonNull Throwable t) {
+        if (sDelegate != null) {
+            sDelegate.log(logLevel, logTag, message, t);
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/package-info.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/package-info.java
new file mode 100644
index 0000000..ee3d3fa
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** @hide */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package androidx.appactions.interaction.capabilities.core;
+
+import androidx.annotation.RestrictTo;
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/Entity.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/Entity.java
new file mode 100644
index 0000000..a40eae1
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/Entity.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.properties;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Entities are used when defining ActionCapability for defining possible values for ParamProperty.
+ */
+public final class Entity {
+    private final Optional<String> mId;
+    private final Optional<String> mName;
+    private final List<String> mAlternateNames;
+
+    private Entity(Builder builder) {
+        this.mId = builder.mId;
+        this.mName = builder.mName;
+        this.mAlternateNames = builder.mAlternateNames;
+    }
+
+    /** Returns a new Builder to build an Entity instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    /** The id of this Entity. */
+    @NonNull
+    public Optional<String> getId() {
+        return mId;
+    }
+
+    /** The name of this entity. The name is what a user may say to refer to this Entity. */
+    @NonNull
+    public Optional<String> getName() {
+        return mName;
+    }
+
+    /**
+     * The alternate names of this entity. These are alternate names a user may say to refer to
+     * this
+     * Entity.
+     */
+    @NonNull
+    public List<String> getAlternateNames() {
+        if (mAlternateNames == null) {
+            return Collections.emptyList();
+        }
+        return mAlternateNames;
+    }
+
+    /** Builder class for Entity. */
+    public static class Builder {
+        private Optional<String> mId = Optional.empty();
+        private Optional<String> mName = Optional.empty();
+        private @Nullable List<String> mAlternateNames = null;
+
+        /** Sets the id of the Entity to be built. */
+        @NonNull
+        public Builder setId(@NonNull String id) {
+            this.mId = Optional.of(id);
+            return this;
+        }
+
+        /** Sets the name of the Entity to be built. */
+        @NonNull
+        public Builder setName(@NonNull String name) {
+            this.mName = Optional.of(name);
+            return this;
+        }
+
+        /** Sets the list of alternate names of the Entity to be built. */
+        @NonNull
+        public Builder setAlternateNames(@NonNull List<String> alternateNames) {
+            this.mAlternateNames = alternateNames;
+            return this;
+        }
+
+        /** Sets the list of alternate names of the Entity to be built. */
+        @NonNull
+        public final Builder setAlternateNames(@NonNull String... alternateNames) {
+            return setAlternateNames(Collections.unmodifiableList(Arrays.asList(alternateNames)));
+        }
+
+        /** Builds and returns an Entity. */
+        @NonNull
+        public Entity build() {
+            return new Entity(this);
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/EntityProperty.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/EntityProperty.java
new file mode 100644
index 0000000..274a91d
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/EntityProperty.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.properties;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/** The property which describes a entity parameter for {@code ActionCapability}. */
+@AutoValue
+public abstract class EntityProperty extends ParamProperty<Entity> {
+    /** A default EntityProperty instance. This property will accept any entity value. */
+    public static final EntityProperty EMPTY = EntityProperty.newBuilder().build();
+
+    /**
+     * Using EntityProperty.PROHIBITED will ensure no Argument containing a value for this field
+     * will
+     * be received by this capability.
+     */
+    public static final EntityProperty PROHIBITED =
+            EntityProperty.newBuilder().setIsProhibited(true).build();
+
+    // TODO (b/260137899)
+
+    /** Returns a Builder instance for EntityProperty. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    /** Builder for {@link EntityProperty}. */
+    public static class Builder {
+
+        private final ArrayList<Entity> mPossibleEntityList = new ArrayList<>();
+        private boolean mIsRequired;
+        private boolean mEntityMatchRequired;
+        private boolean mIsProhibited;
+
+        private Builder() {
+        }
+
+        /**
+         * Adds one or more possible entities for this entity parameter.
+         *
+         * @param entities the possible entities.
+         */
+        @NonNull
+        public Builder addPossibleEntity(@NonNull Entity... entities) {
+            Collections.addAll(mPossibleEntityList, entities);
+            return this;
+        }
+
+        /** Sets whether or not this property requires a value for fulfillment. */
+        @NonNull
+        public Builder setIsRequired(boolean isRequired) {
+            this.mIsRequired = isRequired;
+            return this;
+        }
+
+        /**
+         * Sets whether or not this property requires that the value for this property must match
+         * one of
+         * the Entity in the defined possible entities.
+         */
+        @NonNull
+        public Builder setValueMatchRequired(boolean valueMatchRequired) {
+            this.mEntityMatchRequired = valueMatchRequired;
+            return this;
+        }
+
+        /**
+         * Sets whether this property is prohibited in the response.
+         *
+         * @param isProhibited Whether this property is prohibited in the response.
+         */
+        @NonNull
+        public Builder setIsProhibited(boolean isProhibited) {
+            this.mIsProhibited = isProhibited;
+            return this;
+        }
+
+        /** Builds the property for this entity parameter. */
+        @NonNull
+        public EntityProperty build() {
+            return new AutoValue_EntityProperty(
+                    Collections.unmodifiableList(mPossibleEntityList),
+                    mIsRequired,
+                    mEntityMatchRequired,
+                    mIsProhibited);
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/EnumProperty.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/EnumProperty.java
new file mode 100644
index 0000000..eddd714
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/EnumProperty.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.properties;
+
+import static java.util.Arrays.stream;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * The property which describes an Enum parameter for {@code ActionCapability}.
+ *
+ * @param <EnumT>
+ */
+@AutoValue
+public abstract class EnumProperty<EnumT extends Enum<EnumT>> extends ParamProperty<EnumT> {
+
+    /**
+     * Returns a Builder to build an EnumProperty instance.
+     *
+     * @param enumType the class of the enum.
+     */
+    @NonNull
+    public static <EnumT extends Enum<EnumT>> Builder<EnumT> newBuilder(
+            @NonNull Class<EnumT> enumType) {
+        return new Builder<>(enumType);
+    }
+
+    // TODO (b/260137899)
+
+    abstract Class<EnumT> enumType();
+
+    /**
+     * Builder for {@link EnumProperty}.
+     *
+     * @param <EnumT>
+     */
+    public static final class Builder<EnumT extends Enum<EnumT>> {
+
+        private final ArrayList<EnumT> mPossibleEnumList = new ArrayList<>();
+        private final Class<EnumT> mEnumType;
+        private boolean mIsRequired;
+        private boolean mEntityMatchRequired;
+
+        private Builder(Class<EnumT> enumType) {
+            this.mEnumType = enumType;
+        }
+
+        /**
+         * Adds all app supported entity for this enum parameter. If any supported enum value is
+         * added
+         * then the entity matched is reuqired.
+         *
+         * @param supportedEnumValue supported enum values.
+         */
+        @NonNull
+        @SuppressWarnings("unchecked")
+        public Builder<EnumT> addSupportedEnumValues(@NonNull EnumT... supportedEnumValue) {
+            stream(supportedEnumValue).forEach(mPossibleEnumList::add);
+            this.mEntityMatchRequired = true;
+            return this;
+        }
+
+        /** Sets whether or not this property requires a value for fulfillment. */
+        @NonNull
+        public Builder<EnumT> setIsRequired(boolean isRequired) {
+            this.mIsRequired = isRequired;
+            return this;
+        }
+
+        /** Builds the property for this Enum parameter. */
+        @NonNull
+        public EnumProperty<EnumT> build() {
+            return new AutoValue_EnumProperty<>(
+                    Collections.unmodifiableList(mPossibleEnumList),
+                    mIsRequired,
+                    mEntityMatchRequired,
+                    /* isProhibited= */ false,
+                    mEnumType);
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/IntegerProperty.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/IntegerProperty.java
new file mode 100644
index 0000000..4145d76
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/IntegerProperty.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.properties;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Collections;
+
+/** The property which describes an integer parameter for {@code ActionCapability}. */
+@AutoValue
+public abstract class IntegerProperty extends ParamProperty<Integer> {
+    // The default instance of IntegerProperty. When add EMPTY IntegerProperty to capability, the
+    // intent param will be set with default values.
+    public static final IntegerProperty EMPTY = IntegerProperty.newBuilder().build();
+
+    /**
+     * Using IntegerProperty.REQUIRED ensures that any FulfillmentRequest to this capability will
+     * contain a value for this property.
+     */
+    public static final IntegerProperty REQUIRED =
+            IntegerProperty.newBuilder().setIsRequired(true).build();
+
+    // TODO (b/260137899)
+
+    /** Creates a new Builder for an IntegerProperty. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    /** Builder for {@link IntegerProperty}. */
+    public static class Builder {
+        private boolean mIsRequired;
+
+        private Builder() {
+        }
+
+        /** Sets whether this property is required for fulfillment. */
+        @NonNull
+        public Builder setIsRequired(boolean isRequired) {
+            this.mIsRequired = isRequired;
+            return this;
+        }
+
+        /** Builds the property for this integer parameter. */
+        @NonNull
+        public IntegerProperty build() {
+            return new AutoValue_IntegerProperty(
+                    /** getPossibleValues */
+                    Collections.unmodifiableList(Collections.emptyList()),
+                    mIsRequired,
+                    /* valueMatchRequired= */ false,
+                    /* prohibited= */ false);
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/ParamProperty.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/ParamProperty.java
new file mode 100644
index 0000000..3625a46
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/ParamProperty.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.properties;
+
+import androidx.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * Base class for the property which describes a parameter for {@code ActionCapability}. This class
+ * should not be used directly. Instead, use the typed property classes such as {@link
+ * StringProperty}, etc.
+ *
+ * @param <V>
+ */
+public abstract class ParamProperty<V> {
+
+    /** The list of added possible values for this parameter. */
+    @NonNull
+    public abstract List<V> getPossibleValues();
+
+    /** Indicates that a value for this property is required to be present for fulfillment. */
+    public abstract boolean isRequired();
+
+    /**
+     * Indicates that a match of possible value for the given property must be present. Defaults to
+     * false.
+     *
+     * <p>If true, Assistant skips the capability if there is no match.
+     */
+    public abstract boolean isValueMatchRequired();
+
+    /**
+     * If true, the {@code ActionCapability} will be rejected by assistant if corresponding param is
+     * set in argument. And the value of |isRequired| and |entityMatchRequired| will also be ignored
+     * by assistant.
+     */
+    public abstract boolean isProhibited();
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/SimpleProperty.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/SimpleProperty.java
new file mode 100644
index 0000000..fb8f9d2
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/SimpleProperty.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.properties;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Collections;
+
+/**
+ * A simple property which describes a parameter for {@code ActionCapability}. This property has
+ * simple configurations available and is not tied to a specific type.
+ */
+@AutoValue
+public abstract class SimpleProperty extends ParamProperty<Void> {
+    /** Returns a default SimpleProperty instance. */
+    public static final SimpleProperty DEFAULT = SimpleProperty.newBuilder().build();
+
+    /**
+     * Using SimpleProperty.REQUIRED ensures that the corresponding Argument will contain a
+     * value.
+     */
+    public static final SimpleProperty REQUIRED =
+            SimpleProperty.newBuilder().setIsRequired(true).build();
+
+    // TODO (b/260137899)
+
+    /** Returns a Builder instance for SimpleProperty. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    /** Builder for {@link SimpleProperty}. */
+    public static class Builder {
+
+        private boolean mIsRequired;
+
+        private Builder() {
+        }
+
+        /** Sets whether or not this property requires a value for fulfillment. */
+        @NonNull
+        public Builder setIsRequired(boolean isRequired) {
+            this.mIsRequired = isRequired;
+            return this;
+        }
+
+        /** Builds the property for this string parameter. */
+        @NonNull
+        public SimpleProperty build() {
+            return new AutoValue_SimpleProperty(
+                    /** getPossibleValues */
+                    Collections.unmodifiableList(Collections.emptyList()),
+                    mIsRequired,
+                    /* valueMatchRequired= */ false,
+                    /* prohibited= */ false);
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/StringOrEnumProperty.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/StringOrEnumProperty.java
new file mode 100644
index 0000000..61c1b0b
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/StringOrEnumProperty.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.properties;
+
+import static java.util.Arrays.stream;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.value.AutoOneOf;
+import com.google.auto.value.AutoValue;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * The property which describes a parameter with String or Enum entity for {@code ActionCapability}.
+ *
+ * @param <EnumT>
+ */
+@AutoValue
+public abstract class StringOrEnumProperty<EnumT extends Enum<EnumT>>
+        extends ParamProperty<StringOrEnumProperty.PossibleValue<EnumT>> {
+
+    /** Returns a Builder for StringOrEnumProperty for the given enumType. */
+    @NonNull
+    public static <EnumT extends Enum<EnumT>> Builder<EnumT> newBuilder(
+            @NonNull Class<EnumT> enumType) {
+        return new Builder<>(enumType);
+    }
+
+    @NonNull
+    abstract Class<EnumT> enumType();
+
+    // TODO (b/260137899)
+
+    /**
+     * Returns a new Builder that contains all the existing configuration in this
+     * StringOrEnumProperty.
+     */
+    @NonNull
+    @SuppressWarnings("unchecked")
+    public Builder<EnumT> toBuilder() {
+        Builder<EnumT> builder = new Builder<>(enumType());
+        if (!getPossibleValues().isEmpty()) {
+            for (PossibleValue<EnumT> possibleValue : getPossibleValues()) {
+                if (possibleValue.getKind() == PossibleValue.Kind.STRING_VALUE) {
+                    builder.addPossibleValue(
+                            possibleValue.stringValue().getName(),
+                            possibleValue.stringValue().getAlternateNames().toArray(String[]::new));
+                } else if (possibleValue.getKind() == PossibleValue.Kind.ENUM_VALUE) {
+                    builder.addPossibleValue(possibleValue.enumValue());
+                }
+            }
+        }
+        return builder.setIsRequired(isRequired());
+    }
+
+    /**
+     * Represents a single possible value in StringOrEnumProperty.
+     *
+     * @param <EnumT>
+     */
+    @AutoOneOf(PossibleValue.Kind.class)
+    public abstract static class PossibleValue<EnumT extends Enum<EnumT>> {
+        /** Create a new StringOrEnumProperty.PossibleValue for Kind.STRING_VALUE. */
+        @NonNull
+        public static <EnumT extends Enum<EnumT>> PossibleValue<EnumT> of(
+                @NonNull String name, @NonNull String... alternateNames) {
+            return AutoOneOf_StringOrEnumProperty_PossibleValue.stringValue(
+                    StringProperty.PossibleValue.of(name, alternateNames));
+        }
+
+        /** Create a new StringOrEnumProperty.PossibleValue for Kind.ENUM_VALUE. */
+        @NonNull
+        public static <EnumT extends Enum<EnumT>> PossibleValue<EnumT> of(
+                @NonNull EnumT possibleValue) {
+            return AutoOneOf_StringOrEnumProperty_PossibleValue.enumValue(possibleValue);
+        }
+
+        /** The Kind of this possible value. */
+        @NonNull
+        public abstract Kind getKind();
+
+        /** Returns the StringProperty.PossibleValue, corresponds to Kind.STRING_VALUE. */
+        @NonNull
+        public abstract StringProperty.PossibleValue stringValue();
+
+        /** Returns the StringProperty.PossibleValue, corresponds to Kind.ENUM_VALUE. */
+        @NonNull
+        public abstract EnumT enumValue();
+
+        /** The Kind of PossibleValue. */
+        public enum Kind {
+            STRING_VALUE,
+            ENUM_VALUE,
+        }
+    }
+
+    /**
+     * Builder for {@link StringOrEnumProperty}.
+     *
+     * @param <EnumT>
+     */
+    public static class Builder<EnumT extends Enum<EnumT>> {
+
+        private final ArrayList<PossibleValue<EnumT>> mPossibleValueList = new ArrayList<>();
+        private final Class<EnumT> mEnumType;
+        private boolean mIsRequired;
+        private boolean mEntityMatchRequired;
+
+        private Builder(Class<EnumT> enumType) {
+            this.mEnumType = enumType;
+        }
+
+        /**
+         * Adds a possible string value for this property.
+         *
+         * @param name           the possible string value.
+         * @param alternateNames the alternative names for this value.
+         */
+        @NonNull
+        public Builder<EnumT> addPossibleValue(
+                @NonNull String name, @NonNull String... alternateNames) {
+            mPossibleValueList.add(PossibleValue.of(name, alternateNames));
+            this.mEntityMatchRequired = true;
+            return this;
+        }
+
+        /**
+         * Adds possible Enum values for this parameter.
+         *
+         * @param enumValues possible enum entity values.
+         */
+        @NonNull
+        @SuppressWarnings("unchecked")
+        public Builder<EnumT> addPossibleValue(@NonNull EnumT... enumValues) {
+            stream(enumValues).forEach(
+                    enumValue -> mPossibleValueList.add(PossibleValue.of(enumValue)));
+            this.mEntityMatchRequired = true;
+            return this;
+        }
+
+        /** Sets whether or not this property requires a value for fulfillment. */
+        @NonNull
+        public Builder<EnumT> setIsRequired(boolean isRequired) {
+            this.mIsRequired = isRequired;
+            return this;
+        }
+
+        /**
+         * Sets whether matching a possible value is required for this parameter. Note that this
+         * value
+         * can be overrided by assistant.
+         *
+         * @param valueMatchRequired whether value match is required
+         */
+        @NonNull
+        public Builder<EnumT> setValueMatchRequired(boolean valueMatchRequired) {
+            this.mEntityMatchRequired = valueMatchRequired;
+            return this;
+        }
+
+        /** Builds the property for this Entity or Enum parameter. */
+        @NonNull
+        public StringOrEnumProperty<EnumT> build() {
+            return new AutoValue_StringOrEnumProperty<>(
+                    Collections.unmodifiableList(mPossibleValueList),
+                    mIsRequired,
+                    mEntityMatchRequired,
+                    /* isProhibited= */ false,
+                    mEnumType);
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/StringProperty.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/StringProperty.java
new file mode 100644
index 0000000..69c020a
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/StringProperty.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.properties;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/** The property which describes a string parameter for {@code ActionCapability}. */
+@SuppressWarnings("AutoValueImmutableFields")
+@AutoValue
+public abstract class StringProperty extends ParamProperty<StringProperty.PossibleValue> {
+    /** A default StringProperty instance. This property will accept any string value. */
+    public static final StringProperty EMPTY = StringProperty.newBuilder().build();
+
+    /**
+     * Using StringProperty.PROHIBITED will ensure no Argument containing a value for this field
+     * will
+     * be received by this capability.
+     */
+    public static final StringProperty PROHIBITED =
+            StringProperty.newBuilder().setIsProhibited(true).build();
+
+    /** Returns a Builder instance for StringProperty. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    // TODO (b/260137899)
+
+    /** Represents a single possible value for StringProperty. */
+    @AutoValue
+    public abstract static class PossibleValue {
+
+        /** Creates a new StringProperty.PossibleValue instance. */
+        @NonNull
+        public static final PossibleValue of(@NonNull String name,
+                @NonNull String... alternateNames) {
+            return new AutoValue_StringProperty_PossibleValue(
+                    name, Collections.unmodifiableList(Arrays.asList(alternateNames)));
+        }
+
+        /** The name of this possible value. */
+        @NonNull
+        public abstract String getName();
+
+        /** The alternate names of this possible value. */
+        @NonNull
+        public abstract List<String> getAlternateNames();
+    }
+
+    /** Builder for {@link StringProperty}. */
+    public static class Builder {
+
+        private final ArrayList<PossibleValue> mPossibleValueList = new ArrayList<>();
+        private boolean mIsRequired;
+        private boolean mEntityMatchRequired;
+        private boolean mIsProhibited;
+
+        private Builder() {
+        }
+
+        /**
+         * Adds a possible string value for this property.
+         *
+         * @param name           the possible string value.
+         * @param alternateNames the alternate names for this value.
+         */
+        @NonNull
+        public Builder addPossibleValue(@NonNull String name, @NonNull String... alternateNames) {
+            mPossibleValueList.add(PossibleValue.of(name, alternateNames));
+            return this;
+        }
+
+        /** Sets whether or not this property requires a value for fulfillment. */
+        @NonNull
+        public Builder setIsRequired(boolean isRequired) {
+            this.mIsRequired = isRequired;
+            return this;
+        }
+
+        /**
+         * Sets whether or not this property requires that the value for this property must match
+         * one of
+         * the string values in the defined possible values.
+         */
+        @NonNull
+        public Builder setValueMatchRequired(boolean valueMatchRequired) {
+            this.mEntityMatchRequired = valueMatchRequired;
+            return this;
+        }
+
+        /**
+         * Sets whether the String property is prohibited in the response.
+         *
+         * @param isProhibited Whether this property is prohibited in the response.
+         */
+        @NonNull
+        public Builder setIsProhibited(boolean isProhibited) {
+            this.mIsProhibited = isProhibited;
+            return this;
+        }
+
+        /** Builds the property for this string parameter. */
+        @NonNull
+        public StringProperty build() {
+            return new AutoValue_StringProperty(
+                    Collections.unmodifiableList(mPossibleValueList),
+                    mIsRequired,
+                    mEntityMatchRequired,
+                    mIsProhibited);
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/AppEntityListResolver.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/AppEntityListResolver.java
new file mode 100644
index 0000000..920c369
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/AppEntityListResolver.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.values.SearchAction;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.List;
+
+/**
+ * Similar to ValueListener, but also need to handle grounding of ungrounded values.
+ *
+ * @param <T>
+ */
+public interface AppEntityListResolver<T> extends ValueListener<List<T>> {
+    /**
+     * Given a search criteria, looks up the inventory during runtime, renders the search result
+     * within the app's own UI and then returns it to the Assistant so that the task can be kept in
+     * sync with the app UI.
+     */
+    @NonNull
+    ListenableFuture<EntitySearchResult<T>> lookupAndRender(@NonNull SearchAction<T> searchAction);
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/AppEntityResolver.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/AppEntityResolver.java
new file mode 100644
index 0000000..8cfa324
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/AppEntityResolver.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.values.SearchAction;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * Similar to ValueListener, but also need to handle grounding of ungrounded values.
+ *
+ * @param <T>
+ */
+public interface AppEntityResolver<T> extends ValueListener<T> {
+    /**
+     * Given a search criteria, looks up the inventory during runtime, renders the search result
+     * within the app's own UI and then returns it to the Assistant so that the task can be kept in
+     * sync with the app UI.
+     */
+    @NonNull
+    ListenableFuture<EntitySearchResult<T>> lookupAndRender(@NonNull SearchAction<T> searchAction);
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/EntitySearchResult.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/EntitySearchResult.java
new file mode 100644
index 0000000..98666ad
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/EntitySearchResult.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents results from grounding an ungrounded value.
+ *
+ * @param <V>
+ */
+@SuppressWarnings("AutoValueImmutableFields")
+@AutoValue
+public abstract class EntitySearchResult<V> {
+
+    /** Builds a EntitySearchResult with no possible values. */
+    @NonNull
+    public static <V> EntitySearchResult<V> empty() {
+        return EntitySearchResult.<V>newBuilder().build();
+    }
+
+    @NonNull
+    public static <V> Builder<V> newBuilder() {
+        return new AutoValue_EntitySearchResult.Builder<>();
+    }
+
+    /**
+     * The possible entity values for grounding. Returning exactly 1 result means the value will be
+     * immediately accepted by the task. Returning multiple values will leave the argument in a
+     * disambiguation state, and Assistant should ask for clarification from the user.
+     */
+    @NonNull
+    public abstract List<V> possibleValues();
+
+    /**
+     * Builder for the EntitySearchResult.
+     *
+     * @param <V>
+     */
+    @AutoValue.Builder
+    public abstract static class Builder<V> {
+        private final List<V> mPossibleValues = new ArrayList<>();
+
+        @NonNull
+        abstract Builder<V> setPossibleValues(@NonNull List<V> possibleValues);
+
+        @NonNull
+        public final Builder<V> addPossibleValue(@NonNull V value) {
+            mPossibleValues.add(value);
+            return this;
+        }
+
+        abstract EntitySearchResult<V> autoBuild();
+
+        @NonNull
+        public EntitySearchResult<V> build() {
+            setPossibleValues(mPossibleValues);
+            return autoBuild();
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InvalidTaskException.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InvalidTaskException.java
new file mode 100644
index 0000000..c277143
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InvalidTaskException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Thrown when the task is not configured correctly for a particular capability instance. One
+ * example is when a capability requires user confirmation support, but the capability instance does
+ * not set a value for the {@code OnReadyToConfirmListener}.
+ */
+public final class InvalidTaskException extends RuntimeException {
+
+    public InvalidTaskException(@NonNull String message) {
+        super(message);
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InventoryListResolver.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InventoryListResolver.java
new file mode 100644
index 0000000..b152a17
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InventoryListResolver.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task;
+
+import androidx.annotation.NonNull;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.List;
+
+/**
+ * Repeated version of {@code InventoryResolver}.
+ *
+ * @param <T>
+ */
+public interface InventoryListResolver<T> extends ValueListener<List<T>> {
+    /**
+     * Renders the provided entities in the app UI for dismabiguation.
+     *
+     * <p>The app should not modify the entity contents or their orders during the rendering.
+     * Otherwise, the Assistant task will be out of sync with the app UI.
+     */
+    @NonNull
+    ListenableFuture<Void> renderChoices(@NonNull List<String> entityIDs);
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InventoryResolver.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InventoryResolver.java
new file mode 100644
index 0000000..47674c1
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InventoryResolver.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task;
+
+import androidx.annotation.NonNull;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.List;
+
+/**
+ * Similar to ValueListener, but also need to handle entity rendering.
+ *
+ * @param <T>
+ */
+public interface InventoryResolver<T> extends ValueListener<T> {
+    /**
+     * Renders the provided entities in the app UI for dismabiguation.
+     *
+     * <p>The app should not modify the entity contents or their orders during the rendering.
+     * Otherwise, the Assistant task will be out of sync with the app UI.
+     */
+    @NonNull
+    ListenableFuture<Void> renderChoices(@NonNull List<String> entityIDs);
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/OnDialogFinishListener.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/OnDialogFinishListener.java
new file mode 100644
index 0000000..d79a2e1
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/OnDialogFinishListener.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.ExecutionResult;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * Listener for when a task reaches a valid argument state, and can finish fulfillment.
+ *
+ * @param <ArgumentT>
+ * @param <OutputT>
+ */
+public interface OnDialogFinishListener<ArgumentT, OutputT> {
+
+    /** Called when a task finishes, with the final Argument instance. */
+    @NonNull
+    ListenableFuture<ExecutionResult<OutputT>> onFinish(@NonNull ArgumentT finalArgument);
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/OnInitListener.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/OnInitListener.java
new file mode 100644
index 0000000..ff04662
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/OnInitListener.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task;
+
+import androidx.annotation.NonNull;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * Listener for when a task is initialized.
+ *
+ * @param <TaskUpdaterT>
+ */
+public interface OnInitListener<TaskUpdaterT> {
+
+    /**
+     * Called when a task is initiated. This method should perform initialization if necessary, and
+     * return a future that represents when initialization is finished.
+     *
+     * @param arg depending on the BII, the SDK may pass a utility object to this method, such as
+     *            TaskUpdater.
+     */
+    @NonNull
+    ListenableFuture<Void> onInit(TaskUpdaterT arg);
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/OnReadyToConfirmListener.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/OnReadyToConfirmListener.java
new file mode 100644
index 0000000..eb6c3bd9
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/OnReadyToConfirmListener.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.ConfirmationOutput;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * Listener for when a task reaches a valid argument state, and can confirm fulfillment.
+ *
+ * @param <ArgumentT>
+ * @param <ConfirmationT>
+ */
+public interface OnReadyToConfirmListener<ArgumentT, ConfirmationT> {
+
+    /** Called when a task is ready to confirm, with the final Argument instance. */
+    @NonNull
+    ListenableFuture<ConfirmationOutput<ConfirmationT>> onReadyToConfirm(ArgumentT finalArgument);
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/ValidationResult.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/ValidationResult.java
new file mode 100644
index 0000000..e80d9a7
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/ValidationResult.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.value.AutoOneOf;
+
+/** Result from validating a single argument value. */
+@AutoOneOf(ValidationResult.Kind.class)
+public abstract class ValidationResult {
+    /** Creates a new ACCEPTED ValidationResult. */
+    @NonNull
+    public static ValidationResult newAccepted() {
+        return AutoOneOf_ValidationResult.accepted("accepted");
+    }
+
+    /** Creates a new REJECTED ValidationResult. */
+    @NonNull
+    public static ValidationResult newRejected() {
+        return AutoOneOf_ValidationResult.rejected("rejected");
+    }
+
+    @NonNull
+    public abstract Kind getKind();
+
+    abstract String accepted();
+
+    abstract String rejected();
+
+    /** The state of the argument value after performing validation. */
+    public enum Kind {
+        ACCEPTED,
+        REJECTED,
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/ValueListListener.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/ValueListListener.java
new file mode 100644
index 0000000..f1b6af7
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/ValueListListener.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task;
+
+import java.util.List;
+
+/**
+ * Similar to {@code ValueListener} but for repeated fields.
+ *
+ * @param <T>
+ */
+public interface ValueListListener<T> extends ValueListener<List<T>> {
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/ValueListener.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/ValueListener.java
new file mode 100644
index 0000000..884a95a
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/ValueListener.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task;
+
+import androidx.annotation.NonNull;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * Provides a mechanism for the app to listen to argument updates from Assistant.
+ *
+ * @param <T>
+ */
+public interface ValueListener<T> {
+    /**
+     * Invoked when Assistant reports that an argument value has changed. This method should be
+     * idempotent, as it may be called multiple times with the same input value, not only on the
+     * initial value change.
+     *
+     * <p>This method should:
+     *
+     * <ul>
+     *   <li>1. validate the given argument value(s).
+     *   <li>2. If the given values are valid, update app UI state if applicable.
+     * </ul>
+     *
+     * <p>Returns a ListenableFuture that resolves to the ValidationResult.
+     */
+    @NonNull
+    ListenableFuture<ValidationResult> onReceived(T value);
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/AbstractTaskUpdater.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/AbstractTaskUpdater.java
new file mode 100644
index 0000000..43c3882
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/AbstractTaskUpdater.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import java.util.List;
+import java.util.Map;
+
+/** Base class for BII-specific TaskUpdater for manual input. */
+public abstract class AbstractTaskUpdater {
+
+    private TaskUpdateHandler mTaskUpdateHandler;
+    private boolean mIsDestroyed = false;
+
+    /** Will fail silently if task processing is in the middle of another SYNC request. */
+    protected void updateParamValues(@NonNull Map<String, List<ParamValue>> paramValuesMap) {
+        if (mIsDestroyed || mTaskUpdateHandler == null) {
+            return;
+        }
+        this.mTaskUpdateHandler.updateParamValues(paramValuesMap);
+    }
+
+    void init(@NonNull TaskUpdateHandler taskUpdateHandler) {
+        this.mTaskUpdateHandler = taskUpdateHandler;
+    }
+
+    void destroy() {
+        this.mIsDestroyed = true;
+    }
+
+    /** Returns true if the applicable task is terminated. */
+    public boolean isDestroyed() {
+        return mIsDestroyed;
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/AppGroundingResult.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/AppGroundingResult.java
new file mode 100644
index 0000000..ef14c02
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/AppGroundingResult.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+import androidx.appactions.interaction.proto.CurrentValue;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import com.google.auto.value.AutoOneOf;
+
+/**
+ * Helper object to express if the grounding step of argument processing was successul for a single
+ * ParamValue.
+ */
+@AutoOneOf(AppGroundingResult.Kind.class)
+abstract class AppGroundingResult {
+    static AppGroundingResult ofSuccess(ParamValue paramValue) {
+        return AutoOneOf_AppGroundingResult.success(paramValue);
+    }
+
+    static AppGroundingResult ofFailure(CurrentValue currentValue) {
+        return AutoOneOf_AppGroundingResult.failure(currentValue);
+    }
+
+    public abstract Kind getKind();
+
+    abstract ParamValue success();
+
+    abstract CurrentValue failure();
+
+    enum Kind {
+        SUCCESS,
+        FAILURE
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/AssistantUpdateRequest.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/AssistantUpdateRequest.java
new file mode 100644
index 0000000..31570a2
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/AssistantUpdateRequest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+import androidx.appactions.interaction.capabilities.core.impl.ArgumentsWrapper;
+import androidx.appactions.interaction.capabilities.core.impl.CallbackInternal;
+
+import com.google.auto.value.AutoValue;
+
+/** Represents a fulfillment request coming from Assistant. */
+@AutoValue
+abstract class AssistantUpdateRequest {
+
+    static AssistantUpdateRequest create(
+            ArgumentsWrapper argumentsWrapper, CallbackInternal callbackInternal) {
+        return new AutoValue_AssistantUpdateRequest(argumentsWrapper, callbackInternal);
+    }
+
+    /** The fulfillment request data. */
+    abstract ArgumentsWrapper argumentsWrapper();
+
+    /*
+     * The callback to be report results from handling this request.
+     */
+    abstract CallbackInternal callbackInternal();
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/EmptyTaskUpdater.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/EmptyTaskUpdater.java
new file mode 100644
index 0000000..4155e60
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/EmptyTaskUpdater.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+/** Useful for capabilities that do not support a TaskUpdater. */
+public final class EmptyTaskUpdater extends AbstractTaskUpdater {
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.java
new file mode 100644
index 0000000..2f512a5
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter;
+import androidx.appactions.interaction.capabilities.core.impl.converters.SlotTypeConverter;
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+import androidx.appactions.interaction.capabilities.core.task.AppEntityListResolver;
+import androidx.appactions.interaction.capabilities.core.task.AppEntityResolver;
+import androidx.appactions.interaction.capabilities.core.task.EntitySearchResult;
+import androidx.appactions.interaction.capabilities.core.task.InventoryListResolver;
+import androidx.appactions.interaction.capabilities.core.task.InventoryResolver;
+import androidx.appactions.interaction.capabilities.core.task.ValidationResult;
+import androidx.appactions.interaction.capabilities.core.task.ValueListListener;
+import androidx.appactions.interaction.capabilities.core.task.ValueListener;
+import androidx.appactions.interaction.capabilities.core.task.impl.exceptions.InvalidResolverException;
+import androidx.appactions.interaction.capabilities.core.values.SearchAction;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import com.google.auto.value.AutoOneOf;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.List;
+
+/**
+ * A wrapper around all types of slot resolvers (value listeners + disambig resolvers).
+ *
+ * <p>This allows one type of resolver to be bound for each slot, and abstracts the details of the
+ * individual resolvers. It is also the place where repeated fields are handled.
+ *
+ * @param <ValueTypeT>
+ */
+@AutoOneOf(GenericResolverInternal.Kind.class)
+public abstract class GenericResolverInternal<ValueTypeT> {
+    @NonNull
+    public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromValueListener(
+            @NonNull ValueListener<ValueTypeT> valueListener) {
+        return AutoOneOf_GenericResolverInternal.value(valueListener);
+    }
+
+    @NonNull
+    public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromValueListListener(
+            @NonNull ValueListListener<ValueTypeT> valueListListener) {
+        return AutoOneOf_GenericResolverInternal.valueList(valueListListener);
+    }
+
+    @NonNull
+    public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromAppEntityResolver(
+            @NonNull AppEntityResolver<ValueTypeT> appEntityResolver) {
+        return AutoOneOf_GenericResolverInternal.appEntityResolver(appEntityResolver);
+    }
+
+    @NonNull
+    public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromAppEntityListResolver(
+            @NonNull AppEntityListResolver<ValueTypeT> appEntityListResolver) {
+        return AutoOneOf_GenericResolverInternal.appEntityListResolver(appEntityListResolver);
+    }
+
+    @NonNull
+    public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromInventoryResolver(
+            @NonNull InventoryResolver<ValueTypeT> inventoryResolver) {
+        return AutoOneOf_GenericResolverInternal.inventoryResolver(inventoryResolver);
+    }
+
+    @NonNull
+    public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromInventoryListResolver(
+            @NonNull InventoryListResolver<ValueTypeT> inventoryListResolverListener) {
+        return AutoOneOf_GenericResolverInternal.inventoryListResolver(
+                inventoryListResolverListener);
+    }
+
+    /** Returns the Kind of this resolver */
+    @NonNull
+    public abstract Kind getKind();
+
+    abstract ValueListener<ValueTypeT> value();
+
+    abstract ValueListListener<ValueTypeT> valueList();
+
+    abstract AppEntityResolver<ValueTypeT> appEntityResolver();
+
+    abstract AppEntityListResolver<ValueTypeT> appEntityListResolver();
+
+    abstract InventoryResolver<ValueTypeT> inventoryResolver();
+
+    abstract InventoryListResolver<ValueTypeT> inventoryListResolver();
+
+    /** Wrapper which should invoke the `lookupAndRender` provided by the developer. */
+    @NonNull
+    public ListenableFuture<EntitySearchResult<ValueTypeT>> invokeLookup(
+            @NonNull SearchAction<ValueTypeT> searchAction) throws InvalidResolverException {
+        switch (getKind()) {
+            case APP_ENTITY_RESOLVER:
+                return appEntityResolver().lookupAndRender(searchAction);
+            case APP_ENTITY_LIST_RESOLVER:
+                return appEntityListResolver().lookupAndRender(searchAction);
+            default:
+                throw new InvalidResolverException(
+                        String.format(
+                                "invokeLookup is not supported on this resolver of type %s",
+                                getKind().name()));
+        }
+    }
+
+    /**
+     * Wrapper which should invoke the EntityRender#renderEntities method when the Assistant is
+     * prompting for disambiguation.
+     */
+    @NonNull
+    public ListenableFuture<Void> invokeEntityRender(@NonNull List<String> entityIds)
+            throws InvalidResolverException {
+        switch (getKind()) {
+            case INVENTORY_RESOLVER:
+                return inventoryResolver().renderChoices(entityIds);
+            case INVENTORY_LIST_RESOLVER:
+                return inventoryListResolver().renderChoices(entityIds);
+            default:
+                throw new InvalidResolverException(
+                        String.format(
+                                "invokeEntityRender is not supported on this resolver of type %s",
+                                getKind().name()));
+        }
+    }
+
+    /**
+     * Notifies the app that a new value for this argument has been set by Assistant. This method
+     * should only be called with completely grounded values.
+     */
+    @NonNull
+    public ListenableFuture<ValidationResult> notifyValueChange(
+            @NonNull List<ParamValue> paramValues,
+            @NonNull ParamValueConverter<ValueTypeT> converter)
+            throws StructConversionException {
+        SlotTypeConverter<ValueTypeT> singularConverter = SlotTypeConverter.ofSingular(converter);
+        SlotTypeConverter<List<ValueTypeT>> repeatedConverter = SlotTypeConverter.ofRepeated(
+                converter);
+
+        switch (getKind()) {
+            case VALUE:
+                return value().onReceived(singularConverter.convert(paramValues));
+            case VALUE_LIST:
+                return valueList().onReceived(repeatedConverter.convert(paramValues));
+            case APP_ENTITY_RESOLVER:
+                return appEntityResolver().onReceived(singularConverter.convert(paramValues));
+            case APP_ENTITY_LIST_RESOLVER:
+                return appEntityListResolver().onReceived(repeatedConverter.convert(paramValues));
+            case INVENTORY_RESOLVER:
+                return inventoryResolver().onReceived(singularConverter.convert(paramValues));
+            case INVENTORY_LIST_RESOLVER:
+                return inventoryListResolver().onReceived(repeatedConverter.convert(paramValues));
+        }
+        throw new IllegalStateException("unreachable");
+    }
+
+    /** The kind of resolver. */
+    public enum Kind {
+        VALUE,
+        VALUE_LIST,
+        APP_ENTITY_RESOLVER,
+        APP_ENTITY_LIST_RESOLVER,
+        INVENTORY_RESOLVER,
+        INVENTORY_LIST_RESOLVER
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/ManualInputUpdateRequest.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/ManualInputUpdateRequest.java
new file mode 100644
index 0000000..a869f9b
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/ManualInputUpdateRequest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+import androidx.appactions.interaction.proto.ParamValue;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Represents a fulfillment request coming from user tap. */
+@SuppressWarnings("AutoValueImmutableFields")
+@AutoValue
+abstract class TouchEventUpdateRequest {
+
+    static TouchEventUpdateRequest create(Map<String, List<ParamValue>> paramValuesMap) {
+        return new AutoValue_TouchEventUpdateRequest(paramValuesMap);
+    }
+
+    /**
+     * merge two TouchEventUpdateRequest instances. Map entries in newRequest will take priority in
+     * case of conflict.
+     */
+    static TouchEventUpdateRequest merge(
+            TouchEventUpdateRequest oldRequest, TouchEventUpdateRequest newRequest) {
+        Map<String, List<ParamValue>> mergedParamValuesMap = new HashMap<>(
+                oldRequest.paramValuesMap());
+        mergedParamValuesMap.putAll(newRequest.paramValuesMap());
+        return TouchEventUpdateRequest.create(Collections.unmodifiableMap(mergedParamValuesMap));
+    }
+
+    /* the param values from manual input. */
+    abstract Map<String, List<ParamValue>> paramValuesMap();
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/OnReadyToConfirmListenerInternal.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/OnReadyToConfirmListenerInternal.java
new file mode 100644
index 0000000..e4a9047
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/OnReadyToConfirmListenerInternal.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.ConfirmationOutput;
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+import androidx.appactions.interaction.capabilities.core.task.impl.exceptions.MissingRequiredArgException;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Generic onReadyToConfirm listener for a task capability. This is the entry point to specific
+ * onReadyToCOnfirm listeners. For example, Search/Update sub-BIIs factories may invoke specific
+ * onReadyToConfirm listeners for that BII.
+ *
+ * @param <ConfirmationT>
+ */
+public interface OnReadyToConfirmListenerInternal<ConfirmationT> {
+
+    /** onReadyToConfirm callback for a task capability. */
+    @NonNull
+    ListenableFuture<ConfirmationOutput<ConfirmationT>> onReadyToConfirm(
+            @NonNull Map<String, List<ParamValue>> args)
+            throws StructConversionException, MissingRequiredArgException;
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/OperationType.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/OperationType.java
new file mode 100644
index 0000000..157202d
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/OperationType.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+/**
+ * Represents different operations possible in the Search/Update protocol.
+ */
+public enum OperationType {
+    /** Supports adding to a field of the target object, for example adding to a list. */
+    ADD("Add");
+
+    private final String mOperationType;
+
+    OperationType(String operationType) {
+        this.mOperationType = operationType;
+    }
+
+    @Override
+    public String toString() {
+        return mOperationType;
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/SlotProcessingResult.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/SlotProcessingResult.java
new file mode 100644
index 0000000..6fa70e6
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/SlotProcessingResult.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+import androidx.appactions.interaction.proto.CurrentValue;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.List;
+
+@SuppressWarnings("AutoValueImmutableFields")
+@AutoValue
+abstract class SlotProcessingResult {
+    static SlotProcessingResult create(Boolean isSuccessful, List<CurrentValue> processedValues) {
+        return new AutoValue_SlotProcessingResult(isSuccessful, processedValues);
+    }
+
+    /**
+     * Whether or not the next slot should be processed.
+     *
+     * <p>This is true if the following conditions were met during processing.
+     *
+     * <ul>
+     *   <li>there are no ungroundedValues remaining (either rejected or disambig)
+     *   <li>listener#onReceived returned ACCEPTED for all grounded values (which could be empty
+     *   list)
+     * </ul>
+     */
+    abstract Boolean isSuccessful();
+
+    /** Processed CurrentValue objects. */
+    abstract List<CurrentValue> processedValues();
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImpl.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImpl.java
new file mode 100644
index 0000000..6c7ecd1
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImpl.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appactions.interaction.capabilities.core.impl.ActionCapabilityInternal;
+import androidx.appactions.interaction.capabilities.core.impl.ArgumentsWrapper;
+import androidx.appactions.interaction.capabilities.core.impl.CallbackInternal;
+import androidx.appactions.interaction.capabilities.core.impl.ErrorStatusInternal;
+import androidx.appactions.interaction.capabilities.core.impl.TouchEventCallback;
+import androidx.appactions.interaction.capabilities.core.impl.concurrent.FutureCallback;
+import androidx.appactions.interaction.capabilities.core.impl.concurrent.Futures;
+import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec;
+import androidx.appactions.interaction.capabilities.core.task.OnDialogFinishListener;
+import androidx.appactions.interaction.capabilities.core.task.OnInitListener;
+import androidx.appactions.interaction.proto.AppActionsContext.AppAction;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.Executor;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Stateful horizontal task orchestrator to manage business logic for the task.
+ *
+ * @param <PropertyT>
+ * @param <ArgumentT>
+ * @param <OutputT>
+ * @param <ConfirmationT>
+ * @param <TaskUpdaterT>
+ */
+public final class TaskCapabilityImpl<
+                PropertyT,
+                ArgumentT,
+                OutputT,
+                ConfirmationT,
+                TaskUpdaterT extends AbstractTaskUpdater>
+        implements ActionCapabilityInternal, TaskUpdateHandler {
+
+    private final String mIdentifier;
+    private final Executor mExecutor;
+    private final TaskOrchestrator<PropertyT, ArgumentT, OutputT, ConfirmationT, TaskUpdaterT>
+            mTaskOrchestrator;
+
+    private final Object mAssistantUpdateLock = new Object();
+
+    @GuardedBy("mAssistantUpdateLock")
+    @Nullable
+    private AssistantUpdateRequest mPendingAssistantRequest = null;
+
+    @GuardedBy("mAssistantUpdateLock")
+    @Nullable
+    private TouchEventUpdateRequest mPendingTouchEventRequest = null;
+
+    public TaskCapabilityImpl(
+            @NonNull String identifier,
+            @NonNull ActionSpec<PropertyT, ArgumentT, OutputT> actionSpec,
+            PropertyT property,
+            @NonNull TaskParamRegistry paramRegistry,
+            @NonNull Optional<OnInitListener<TaskUpdaterT>> onInitListener,
+            @NonNull
+                    Optional<OnReadyToConfirmListenerInternal<ConfirmationT>>
+                            onReadyToConfirmListener,
+            @NonNull OnDialogFinishListener<ArgumentT, OutputT> onFinishListener,
+            @NonNull
+                    Map<String, Function<ConfirmationT, List<ParamValue>>>
+                            confirmationOutputBindings,
+            @NonNull Map<String, Function<OutputT, List<ParamValue>>> executionOutputBindings,
+            @NonNull Executor executor) {
+        this.mIdentifier = identifier;
+        this.mExecutor = executor;
+        this.mTaskOrchestrator =
+                new TaskOrchestrator<>(
+                        identifier,
+                        actionSpec,
+                        property,
+                        paramRegistry,
+                        onInitListener,
+                        onReadyToConfirmListener,
+                        onFinishListener,
+                        confirmationOutputBindings,
+                        executionOutputBindings,
+                        executor);
+    }
+
+    @NonNull
+    @Override
+    public Optional<String> getId() {
+        return Optional.of(mIdentifier);
+    }
+
+    public void setTaskUpdaterSupplier(@NonNull Supplier<TaskUpdaterT> taskUpdaterSupplier) {
+        this.mTaskOrchestrator.setTaskUpdaterSupplier(
+                () -> {
+                    TaskUpdaterT taskUpdater = taskUpdaterSupplier.get();
+                    taskUpdater.init(this);
+                    return taskUpdater;
+                });
+    }
+
+    @Override
+    public void setTouchEventCallback(@NonNull TouchEventCallback touchEventCallback) {
+        mTaskOrchestrator.setTouchEventCallback(touchEventCallback);
+    }
+
+    @NonNull
+    @Override
+    public AppAction getAppAction() {
+        return this.mTaskOrchestrator.getAppAction();
+    }
+
+    /**
+     * If there is a pendingAssistantRequest, we will overwrite that request (and send CANCELLED
+     * response to that request).
+     *
+     * <p>This is done because assistant requests contain the full state, so we can safely ignore
+     * existing requests if a new one arrives.
+     */
+    private void enqueueAssistantRequest(@NonNull AssistantUpdateRequest request) {
+        synchronized (mAssistantUpdateLock) {
+            if (mPendingAssistantRequest != null) {
+                mPendingAssistantRequest.callbackInternal().onError(ErrorStatusInternal.CANCELLED);
+            }
+            mPendingAssistantRequest = request;
+            dispatchPendingRequestIfIdle();
+        }
+    }
+
+    private void enqueueTouchEventRequest(@NonNull TouchEventUpdateRequest request) {
+        synchronized (mAssistantUpdateLock) {
+            if (mPendingTouchEventRequest == null) {
+                mPendingTouchEventRequest = request;
+            } else {
+                mPendingTouchEventRequest =
+                        TouchEventUpdateRequest.merge(mPendingTouchEventRequest, request);
+            }
+            dispatchPendingRequestIfIdle();
+        }
+    }
+
+    /**
+     * If taskOrchestrator is idle, select the next request to dispatch to taskOrchestrator (if
+     * there are any pending requests).
+     *
+     * <p>If taskOrchestrator is not idle, do nothing, since this method will automatically be
+     * called when the current request finishes.
+     */
+    void dispatchPendingRequestIfIdle() {
+        synchronized (mAssistantUpdateLock) {
+            if (!mTaskOrchestrator.isIdle()) {
+                return;
+            }
+            UpdateRequest nextRequest = null;
+            if (mPendingAssistantRequest != null) {
+                nextRequest = UpdateRequest.of(mPendingAssistantRequest);
+                mPendingAssistantRequest = null;
+            } else if (mPendingTouchEventRequest != null) {
+                nextRequest = UpdateRequest.of(mPendingTouchEventRequest);
+                mPendingTouchEventRequest = null;
+            }
+            if (nextRequest != null) {
+                Futures.addCallback(
+                        mTaskOrchestrator.processUpdateRequest(nextRequest),
+                        new FutureCallback<Void>() {
+                            @Override
+                            public void onSuccess(Void unused) {
+                                dispatchPendingRequestIfIdle();
+                            }
+
+                            /**
+                             * A fatal exception has occurred, cause by one of the following:
+                             *
+                             * <ul>
+                             *   <li>1. The developer listener threw some runtime exception
+                             *   <li>2. The SDK encountered some uncaught internal exception
+                             * </ul>
+                             *
+                             * <p>In both cases, this exception will be rethrown which will crash
+                             * the app.
+                             */
+                            @Override
+                            public void onFailure(@NonNull Throwable t) {
+                                throw new IllegalStateException(
+                                        "unhandled exception in request processing", t);
+                            }
+                        },
+                        mExecutor);
+            }
+        }
+    }
+
+    @Override
+    public void execute(
+            @NonNull ArgumentsWrapper argumentsWrapper, @NonNull CallbackInternal callback) {
+        enqueueAssistantRequest(AssistantUpdateRequest.create(argumentsWrapper, callback));
+    }
+
+    /** Method for attempting to manually update the param values. */
+    @Override
+    public void updateParamValues(@NonNull Map<String, List<ParamValue>> paramValuesMap) {
+        enqueueTouchEventRequest(TouchEventUpdateRequest.create(paramValuesMap));
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtils.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtils.java
new file mode 100644
index 0000000..41fe7f6
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtils.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+import static androidx.appactions.interaction.capabilities.core.impl.utils.ImmutableCollectors.toImmutableList;
+import static androidx.appactions.interaction.capabilities.core.impl.utils.ImmutableCollectors.toImmutableMap;
+import static androidx.appactions.interaction.capabilities.core.impl.utils.ImmutableCollectors.toImmutableSet;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.task.impl.exceptions.MissingRequiredArgException;
+import androidx.appactions.interaction.proto.AppActionsContext.IntentParameter;
+import androidx.appactions.interaction.proto.CurrentValue;
+import androidx.appactions.interaction.proto.CurrentValue.Status;
+import androidx.appactions.interaction.proto.DisambiguationData;
+import androidx.appactions.interaction.proto.Entity;
+import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.FulfillmentValue;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import com.google.protobuf.Struct;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.IntStream;
+
+/** Utility methods used for implementing Task Capabilities. */
+public final class TaskCapabilityUtils {
+    private TaskCapabilityUtils() {
+    }
+
+    /** Uses Property to detect if all required arguments are present. */
+    static boolean isSlotFillingComplete(
+            Map<String, List<ParamValue>> finalArguments, List<IntentParameter> paramsList) {
+        Set<String> requiredParams =
+                paramsList.stream()
+                        .filter(IntentParameter::getIsRequired)
+                        .map(IntentParameter::getName)
+                        .collect(toImmutableSet());
+        for (String paramName : requiredParams) {
+            if (!finalArguments.containsKey(paramName)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    static List<CurrentValue> paramValuesToCurrentValue(
+            List<ParamValue> paramValueList, Status status) {
+        return paramValueList.stream()
+                .map(paramValue -> toCurrentValue(paramValue, status))
+                .collect(toImmutableList());
+    }
+
+    static List<FulfillmentValue> paramValuesToFulfillmentValues(List<ParamValue> paramValueList) {
+        return paramValueList.stream()
+                .map(paramValue -> FulfillmentValue.newBuilder().setValue(paramValue).build())
+                .collect(toImmutableList());
+    }
+
+    static Map<String, List<FulfillmentValue>> paramValuesMapToFulfillmentValuesMap(
+            Map<String, List<ParamValue>> paramValueMap) {
+        return paramValueMap.entrySet().stream()
+                .collect(
+                        toImmutableMap(
+                                Map.Entry::getKey,
+                                (entry) -> paramValuesToFulfillmentValues(entry.getValue())));
+    }
+
+    static List<CurrentValue> fulfillmentValuesToCurrentValues(
+            List<FulfillmentValue> fulfillmentValueList, Status status) {
+        return fulfillmentValueList.stream()
+                .map(fulfillmentValue -> toCurrentValue(fulfillmentValue, status))
+                .collect(toImmutableList());
+    }
+
+    static CurrentValue toCurrentValue(ParamValue paramValue, Status status) {
+        return CurrentValue.newBuilder().setValue(paramValue).setStatus(status).build();
+    }
+
+    static CurrentValue toCurrentValue(FulfillmentValue fulfillmentValue, Status status) {
+        CurrentValue.Builder result = CurrentValue.newBuilder();
+        if (fulfillmentValue.hasValue()) {
+            result.setValue(fulfillmentValue.getValue());
+        }
+        if (fulfillmentValue.hasDisambigData()) {
+            result.setDisambiguationData(fulfillmentValue.getDisambigData());
+        }
+        return result.setStatus(status).build();
+    }
+
+    static ParamValue groundedValueToParamValue(Entity groundedEntity) {
+        if (groundedEntity.hasValue()) {
+            return ParamValue.newBuilder()
+                    .setIdentifier(groundedEntity.getIdentifier())
+                    .setStructValue(groundedEntity.getValue())
+                    .build();
+        } else {
+            return ParamValue.newBuilder()
+                    .setIdentifier(groundedEntity.getIdentifier())
+                    .setStringValue(groundedEntity.getName())
+                    .build();
+        }
+    }
+
+    /** Create a CurrentValue based on Disambugation result for a ParamValue. */
+    static CurrentValue getCurrentValueForDisambiguation(
+            ParamValue paramValue, List<Entity> disambiguationEntities) {
+        return CurrentValue.newBuilder()
+                .setValue(paramValue)
+                .setStatus(Status.DISAMBIG)
+                .setDisambiguationData(
+                        DisambiguationData.newBuilder().addAllEntities(disambiguationEntities))
+                .build();
+    }
+
+    /** Convenience method to be used in onFinishListeners. */
+    @NonNull
+    public static List<ParamValue> checkRequiredArg(
+            @NonNull Map<String, List<ParamValue>> args, @NonNull String argName)
+            throws MissingRequiredArgException {
+        List<ParamValue> result = args.get(argName);
+        if (result == null) {
+            throw new MissingRequiredArgException(
+                    String.format(
+                            "'%s' is a required argument but is missing from the final arguments "
+                                    + "map.",
+                            argName));
+        }
+        return result;
+    }
+
+    /** Compares two ParamValue, returns false if they are equivalent, true otherwise. */
+    private static boolean hasParamValueDiff(ParamValue oldArg, ParamValue newArg) {
+        if (oldArg.getValueCase().getNumber() != newArg.getValueCase().getNumber()) {
+            return true;
+        }
+        if (!oldArg.getIdentifier().equals(newArg.getIdentifier())) {
+            return true;
+        }
+        switch (oldArg.getValueCase()) {
+            case VALUE_NOT_SET:
+                return false;
+            case STRING_VALUE:
+                return !oldArg.getStringValue().equals(newArg.getStringValue());
+            case BOOL_VALUE:
+                return oldArg.getBoolValue() != newArg.getBoolValue();
+            case NUMBER_VALUE:
+                return oldArg.getNumberValue() != newArg.getNumberValue();
+            case STRUCT_VALUE:
+                return !Arrays.equals(
+                        oldArg.getStructValue().toByteArray(),
+                        newArg.getStructValue().toByteArray());
+        }
+        return true;
+    }
+
+    /**
+     * Returns true if we can skip processing of new FulfillmentValues for a slot.
+     *
+     * <p>There are two required conditions for skipping processing:
+     *
+     * <ul>
+     *   <li>1. currentValues are all ACCEPTED.
+     *   <li>2. there are no differences between the ParamValues in currentValues and
+     *       fulfillmentValues.
+     * </ul>
+     */
+    static boolean canSkipSlotProcessing(
+            List<CurrentValue> currentValues, List<FulfillmentValue> fulfillmentValues) {
+        if (currentValues.stream()
+                .allMatch(currentValue -> currentValue.getStatus().equals(Status.ACCEPTED))) {
+            if (currentValues.size() == fulfillmentValues.size()) {
+                return IntStream.range(0, fulfillmentValues.size())
+                        .allMatch(
+                                i ->
+                                        !TaskCapabilityUtils.hasParamValueDiff(
+                                                currentValues.get(i).getValue(),
+                                                fulfillmentValues.get(i).getValue()));
+            }
+        }
+        return false;
+    }
+
+    /** Given a {@code List<CurrentValue>} find all the Struct in them as a Map. */
+    private static Map<String, Struct> getStructsFromCurrentValues(
+            List<CurrentValue> currentValues) {
+        Map<String, Struct> candidates = new HashMap<>();
+        for (CurrentValue currentValue : currentValues) {
+            if (currentValue.getStatus() == CurrentValue.Status.ACCEPTED
+                    && currentValue.getValue().hasStructValue()) {
+                candidates.put(
+                        currentValue.getValue().getIdentifier(),
+                        currentValue.getValue().getStructValue());
+            } else if (currentValue.getStatus() == CurrentValue.Status.DISAMBIG) {
+                for (Entity entity : currentValue.getDisambiguationData().getEntitiesList()) {
+                    if (entity.hasValue()) {
+                        candidates.put(entity.getIdentifier(), entity.getValue());
+                    }
+                }
+            }
+        }
+        return Collections.unmodifiableMap(candidates);
+    }
+
+    /**
+     * Grounded values for donated inventory slots are sent as identifier only, so find matching
+     * Struct from previous turn and add them to the fulfillment values.
+     */
+    static List<FulfillmentValue> getMaybeModifiedSlotValues(
+            List<CurrentValue> currentValues, List<FulfillmentValue> newSlotValues) {
+        Map<String, Struct> candidates = getStructsFromCurrentValues(currentValues);
+        if (candidates.isEmpty()) {
+            return newSlotValues;
+        }
+        return newSlotValues.stream()
+                .map(
+                        fulfillmentValue -> {
+                            ParamValue paramValue = fulfillmentValue.getValue();
+                            if (paramValue.hasIdentifier()
+                                    && !paramValue.hasStructValue()
+                                    && candidates.containsKey(paramValue.getIdentifier())) {
+                                // TODO(b/243944366) throw error if struct filling fails for an
+                                //  inventory slot.
+                                return fulfillmentValue.toBuilder()
+                                        .setValue(
+                                                paramValue.toBuilder()
+                                                        .setStructValue(candidates.get(
+                                                                paramValue.getIdentifier())))
+                                        .build();
+                            }
+                            return fulfillmentValue;
+                        })
+                .collect(toImmutableList());
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.java
new file mode 100644
index 0000000..4edee50
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.java
@@ -0,0 +1,713 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+import static androidx.appactions.interaction.capabilities.core.impl.utils.ImmutableCollectors.toImmutableList;
+import static androidx.appactions.interaction.capabilities.core.impl.utils.ImmutableCollectors.toImmutableMap;
+import static androidx.appactions.interaction.capabilities.core.impl.utils.ImmutableCollectors.toImmutableSet;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appactions.interaction.capabilities.core.ConfirmationOutput;
+import androidx.appactions.interaction.capabilities.core.ExecutionResult;
+import androidx.appactions.interaction.capabilities.core.impl.ArgumentsWrapper;
+import androidx.appactions.interaction.capabilities.core.impl.CallbackInternal;
+import androidx.appactions.interaction.capabilities.core.impl.ErrorStatusInternal;
+import androidx.appactions.interaction.capabilities.core.impl.TouchEventCallback;
+import androidx.appactions.interaction.capabilities.core.impl.concurrent.FutureCallback;
+import androidx.appactions.interaction.capabilities.core.impl.concurrent.Futures;
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec;
+import androidx.appactions.interaction.capabilities.core.impl.utils.CapabilityLogger;
+import androidx.appactions.interaction.capabilities.core.impl.utils.LoggerInternal;
+import androidx.appactions.interaction.capabilities.core.task.OnDialogFinishListener;
+import androidx.appactions.interaction.capabilities.core.task.OnInitListener;
+import androidx.appactions.interaction.capabilities.core.task.impl.exceptions.MissingRequiredArgException;
+import androidx.appactions.interaction.proto.AppActionsContext.AppAction;
+import androidx.appactions.interaction.proto.AppActionsContext.IntentParameter;
+import androidx.appactions.interaction.proto.CurrentValue;
+import androidx.appactions.interaction.proto.CurrentValue.Status;
+import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment;
+import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.FulfillmentValue;
+import androidx.appactions.interaction.proto.FulfillmentResponse;
+import androidx.appactions.interaction.proto.FulfillmentResponse.StructuredOutput;
+import androidx.appactions.interaction.proto.ParamValue;
+import androidx.appactions.interaction.proto.TaskInfo;
+import androidx.appactions.interaction.proto.TouchEventMetadata;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * TaskOrchestrator is responsible for holding task state, and processing assistant / manual input
+ * updates to update task state.
+ *
+ * <p>TaskOrchestrator is also responsible to communicating state updates to developer provided
+ * listeners.
+ *
+ * <p>Only one request can be processed at a time.
+ */
+final class TaskOrchestrator<
+        PropertyT, ArgumentT, OutputT, ConfirmationT, TaskUpdaterT extends AbstractTaskUpdater> {
+
+    private static final String LOG_TAG = "TaskOrchestrator";
+    private final String mIdentifier;
+    private final ActionSpec<PropertyT, ArgumentT, OutputT> mActionSpec;
+    private final PropertyT mProperty;
+    private final TaskParamRegistry mParamRegistry;
+    private final Optional<OnInitListener<TaskUpdaterT>> mOnInitListener;
+    private final Optional<OnReadyToConfirmListenerInternal<ConfirmationT>>
+            mOnReadyToConfirmListener;
+
+    private final OnDialogFinishListener<ArgumentT, OutputT> mOnFinishListener;
+    private final Executor mExecutor;
+
+    /**
+     * Map of argument name to the {@link CurrentValue} which wraps the argument name and status
+     * .
+     */
+    private final Map<String, List<CurrentValue>> mCurrentValuesMap;
+    /**
+     * Map of confirmation data name to a function that converts confirmation data to ParamValue
+     * .
+     */
+    private final Map<String, Function<ConfirmationT, List<ParamValue>>>
+            mConfirmationOutputBindings;
+    /** Map of execution output name to a function that converts execution output to ParamValue. */
+    private final Map<String, Function<OutputT, List<ParamValue>>> mExecutionOutputBindings;
+    /**
+     * Internal lock to enable synchronization while processing update requests. Also used for
+     * synchronization of Task orchestrator state. ie indicate whether it is idle or not
+     */
+    private final Object mTaskOrchestratorLock = new Object();
+    /**
+     * The callback that should be invoked when manual input processing finishes. This sends the
+     * processing results to the AppInteraction SDKs. Note, this field is not provided on
+     * construction
+     * because the callback is not available at the time when the developer creates the capability.
+     */
+    @Nullable
+    TouchEventCallback mTouchEventCallback;
+    /** Current status of the overall task (i.e. status of the task). */
+    private TaskStatus mTaskStatus;
+    /** Supplies new instances of TaskUpdaterT to give to onInitListener. */
+    private Supplier<TaskUpdaterT> mTaskUpdaterSupplier;
+
+    /**
+     * The current TaskUpdaterT instance. Should only be non-null when taskStatus is IN_PROGRESS.
+     */
+    @Nullable
+    private TaskUpdaterT mTaskUpdater;
+    /** True if an UpdateRequest is currently being processed, false otherwise. */
+    @GuardedBy("mTaskOrchestratorLock")
+    private boolean mIsIdle = true;
+
+    TaskOrchestrator(
+            String identifier,
+            ActionSpec<PropertyT, ArgumentT, OutputT> actionSpec,
+            PropertyT property,
+            TaskParamRegistry paramRegistry,
+            Optional<OnInitListener<TaskUpdaterT>> onInitListener,
+            Optional<OnReadyToConfirmListenerInternal<ConfirmationT>> onReadyToConfirmListener,
+            OnDialogFinishListener<ArgumentT, OutputT> onFinishListener,
+            Map<String, Function<ConfirmationT, List<ParamValue>>> confirmationOutputBindings,
+            Map<String, Function<OutputT, List<ParamValue>>> executionOutputBindings,
+            Executor executor) {
+        this.mIdentifier = identifier;
+        this.mActionSpec = actionSpec;
+        this.mProperty = property;
+        this.mParamRegistry = paramRegistry;
+        this.mOnInitListener = onInitListener;
+        this.mOnReadyToConfirmListener = onReadyToConfirmListener;
+        this.mOnFinishListener = onFinishListener;
+        this.mConfirmationOutputBindings = confirmationOutputBindings;
+        this.mExecutionOutputBindings = executionOutputBindings;
+        this.mExecutor = executor;
+
+        this.mCurrentValuesMap = Collections.synchronizedMap(new HashMap<>());
+        this.mTaskStatus = TaskStatus.UNINITIATED;
+        this.mTaskUpdater = null;
+    }
+
+    void setTaskUpdaterSupplier(Supplier<TaskUpdaterT> taskUpdaterSupplier) {
+        this.mTaskUpdaterSupplier = taskUpdaterSupplier;
+    }
+
+    // Set a TouchEventCallback instance. This callback is invoked when state changes from manual
+    // input.
+    void setTouchEventCallback(@Nullable TouchEventCallback touchEventCallback) {
+        this.mTouchEventCallback = touchEventCallback;
+    }
+
+    /** Returns whether or not a request is currently being processed */
+    boolean isIdle() {
+        synchronized (mTaskOrchestratorLock) {
+            return mIsIdle;
+        }
+    }
+
+    /**
+     * processes the provided UpdateRequest asynchronously.
+     *
+     * <p>Returns a {@code ListenableFuture<Void>} that is completed when the request handling is
+     * completed.
+     *
+     * <p>An unhandled exception when handling an UpdateRequest will cause all future update
+     * requests
+     * to fail.
+     *
+     * <p>This method should never be called when isIdle() returns false.
+     */
+    ListenableFuture<Void> processUpdateRequest(UpdateRequest updateRequest) {
+        synchronized (mTaskOrchestratorLock) {
+            if (!mIsIdle) {
+                throw new IllegalStateException(
+                        "processUpdateRequest should never be called when isIdle is false.");
+            }
+            mIsIdle = false;
+            ListenableFuture<Void> requestProcessingFuture;
+            switch (updateRequest.getKind()) {
+                case ASSISTANT:
+                    requestProcessingFuture = processAssistantUpdateRequest(
+                            updateRequest.assistant());
+                    break;
+                case TOUCH_EVENT:
+                    requestProcessingFuture = processTouchEventUpdateRequest(
+                            updateRequest.touchEvent());
+                    break;
+                default:
+                    throw new IllegalArgumentException("unknown UpdateRequest type");
+            }
+            return Futures.transform(
+                    requestProcessingFuture,
+                    unused -> {
+                        synchronized (mTaskOrchestratorLock) {
+                            mIsIdle = true;
+                            return null;
+                        }
+                    },
+                    mExecutor,
+                    "set isIdle");
+        }
+    }
+
+    /** Processes an assistant update request. */
+    private ListenableFuture<Void> processAssistantUpdateRequest(
+            AssistantUpdateRequest assistantUpdateRequest) {
+        ArgumentsWrapper argumentsWrapper = assistantUpdateRequest.argumentsWrapper();
+        CallbackInternal callback = assistantUpdateRequest.callbackInternal();
+
+        if (!argumentsWrapper.requestMetadata().isPresent()) {
+            callback.onError(ErrorStatusInternal.INVALID_REQUEST_TYPE);
+            return Futures.immediateVoidFuture();
+        }
+        Fulfillment.Type requestType = argumentsWrapper.requestMetadata().get().requestType();
+        switch (requestType) {
+            case UNRECOGNIZED:
+            case UNKNOWN_TYPE:
+                callback.onError(ErrorStatusInternal.INVALID_REQUEST_TYPE);
+                break;
+            case SYNC:
+                return handleSync(argumentsWrapper, callback);
+            case CONFIRM:
+                return handleConfirm(callback);
+            case CANCEL:
+            case TERMINATE:
+                clearState();
+                callback.onSuccess(FulfillmentResponse.getDefaultInstance());
+                break;
+        }
+        return Futures.immediateVoidFuture();
+    }
+
+    public ListenableFuture<Void> processTouchEventUpdateRequest(
+            TouchEventUpdateRequest touchEventUpdateRequest) {
+        Map<String, List<ParamValue>> paramValuesMap = touchEventUpdateRequest.paramValuesMap();
+        if (mTouchEventCallback == null
+                || paramValuesMap.isEmpty()
+                || mTaskStatus != TaskStatus.IN_PROGRESS) {
+            return Futures.immediateVoidFuture();
+        }
+        for (Map.Entry<String, List<ParamValue>> entry : paramValuesMap.entrySet()) {
+            String argName = entry.getKey();
+            mCurrentValuesMap.put(
+                    argName,
+                    entry.getValue().stream()
+                            .map(paramValue -> TaskCapabilityUtils.toCurrentValue(paramValue,
+                                    Status.ACCEPTED))
+                            .collect(toImmutableList()));
+        }
+        ListenableFuture<Void> argumentsProcessingFuture;
+        if (anyParamsOfStatus(Status.DISAMBIG)) {
+            argumentsProcessingFuture = Futures.immediateVoidFuture();
+        } else {
+            Map<String, List<FulfillmentValue>> fulfillmentValuesMap =
+                    TaskCapabilityUtils.paramValuesMapToFulfillmentValuesMap(
+                            getCurrentPendingArguments());
+            argumentsProcessingFuture = processFulfillmentValues(fulfillmentValuesMap);
+        }
+
+        ListenableFuture<FulfillmentResponse> fulfillmentResponseFuture =
+                Futures.transformAsync(
+                        argumentsProcessingFuture,
+                        (unused) -> maybeConfirmOrFinish(),
+                        mExecutor,
+                        "maybeConfirmOrFinish");
+
+        return invokeTouchEventCallback(fulfillmentResponseFuture);
+    }
+
+    private ListenableFuture<Void> invokeTouchEventCallback(
+            ListenableFuture<FulfillmentResponse> fulfillmentResponseFuture) {
+        return CallbackToFutureAdapter.getFuture(
+                completer -> {
+                    Futures.addCallback(
+                            fulfillmentResponseFuture,
+                            new FutureCallback<FulfillmentResponse>() {
+                                @Override
+                                public void onSuccess(FulfillmentResponse fulfillmentResponse) {
+                                    LoggerInternal.log(
+                                            CapabilityLogger.LogLevel.INFO, LOG_TAG,
+                                            "Manual input success");
+                                    if (mTouchEventCallback != null) {
+                                        mTouchEventCallback.onSuccess(
+                                                fulfillmentResponse,
+                                                TouchEventMetadata.getDefaultInstance());
+                                    } else {
+                                        LoggerInternal.log(
+                                                CapabilityLogger.LogLevel.ERROR, LOG_TAG,
+                                                "Manual input null callback");
+                                    }
+                                    completer.set(null);
+                                }
+
+                                @Override
+                                public void onFailure(@NonNull Throwable t) {
+                                    LoggerInternal.log(CapabilityLogger.LogLevel.ERROR, LOG_TAG,
+                                            "Manual input fail");
+                                    if (mTouchEventCallback != null) {
+                                        mTouchEventCallback.onError(
+                                                ErrorStatusInternal.TOUCH_EVENT_REQUEST_FAILURE);
+                                    } else {
+                                        LoggerInternal.log(
+                                                CapabilityLogger.LogLevel.ERROR, LOG_TAG,
+                                                "Manual input null callback");
+                                    }
+                                    completer.set(null);
+                                }
+                            },
+                            mExecutor);
+                    return "handle fulfillmentResponseFuture for manual input";
+                });
+    }
+
+    /** Remove any state that may affect the #getAppAction() call. */
+    private void clearState() {
+        if (this.mTaskUpdater != null) {
+            this.mTaskUpdater.destroy();
+            this.mTaskUpdater = null;
+        }
+        this.mCurrentValuesMap.clear();
+        this.mTaskStatus = TaskStatus.UNINITIATED;
+    }
+
+    /**
+     * If slot filling is incomplete, the future contains default FulfillmentResponse.
+     *
+     * <p>Otherwise, the future contains a FulfillmentResponse containing BIC or BIO data.
+     */
+    private ListenableFuture<FulfillmentResponse> maybeConfirmOrFinish() {
+        Map<String, List<ParamValue>> finalArguments = getCurrentAcceptedArguments();
+        AppAction appAction = mActionSpec.convertPropertyToProto(mProperty);
+        if (anyParamsOfStatus(Status.REJECTED)
+                || !TaskCapabilityUtils.isSlotFillingComplete(finalArguments,
+                appAction.getParamsList())) {
+            return Futures.immediateFuture(FulfillmentResponse.getDefaultInstance());
+        }
+        if (mOnReadyToConfirmListener.isPresent()) {
+            return getFulfillmentResponseForConfirmation(finalArguments);
+        }
+        return getFulfillmentResponseForExecution(finalArguments);
+    }
+
+    private ListenableFuture<Void> maybeInitializeTask() {
+        if (this.mTaskStatus == TaskStatus.UNINITIATED && mOnInitListener.isPresent()) {
+            this.mTaskUpdater = mTaskUpdaterSupplier.get();
+            this.mTaskStatus = TaskStatus.IN_PROGRESS;
+            return mOnInitListener.get().onInit(this.mTaskUpdater);
+        }
+        this.mTaskStatus = TaskStatus.IN_PROGRESS;
+        return Futures.immediateVoidFuture();
+    }
+
+    /**
+     * Handles a SYNC request from assistant.
+     *
+     * <p>Control-flow logic for a single task turn. Note, a task may start and finish in the same
+     * turn, so the logic should include onEnter, arg validation, and onExit.
+     */
+    private ListenableFuture<Void> handleSync(
+            ArgumentsWrapper argumentsWrapper, CallbackInternal callback) {
+        ListenableFuture<Void> onInitFuture = maybeInitializeTask();
+
+        clearMissingArgs(argumentsWrapper);
+        ListenableFuture<Void> argResolutionFuture =
+                Futures.transformAsync(
+                        onInitFuture,
+                        unused -> processFulfillmentValues(argumentsWrapper.paramValues()),
+                        mExecutor,
+                        "processFulfillmentValues");
+
+        ListenableFuture<FulfillmentResponse> fulfillmentResponseFuture =
+                Futures.transformAsync(
+                        argResolutionFuture,
+                        unused -> maybeConfirmOrFinish(),
+                        mExecutor,
+                        "maybeConfirmOrFinish");
+        return CallbackToFutureAdapter.getFuture(
+                completer -> {
+                    Futures.addCallback(
+                            fulfillmentResponseFuture,
+                            new FutureCallback<FulfillmentResponse>() {
+                                @Override
+                                public void onSuccess(FulfillmentResponse fulfillmentResponse) {
+                                    LoggerInternal.log(CapabilityLogger.LogLevel.INFO, LOG_TAG,
+                                            "Task sync success");
+                                    callback.onSuccess(fulfillmentResponse);
+                                    completer.set(null);
+                                }
+
+                                @Override
+                                public void onFailure(@NonNull Throwable t) {
+                                    LoggerInternal.log(CapabilityLogger.LogLevel.ERROR, LOG_TAG,
+                                            "Task sync fail", t);
+                                    callback.onError(ErrorStatusInternal.SYNC_REQUEST_FAILURE);
+                                    completer.set(null);
+                                }
+                            },
+                            mExecutor);
+                    return "handle fulfillmentResponseFuture for SYNC";
+                });
+    }
+
+    /**
+     * Control-flow logic for a single task turn in which the user has confirmed in the previous
+     * turn.
+     */
+    private ListenableFuture<Void> handleConfirm(CallbackInternal callback) {
+        Map<String, List<ParamValue>> finalArguments = getCurrentAcceptedArguments();
+        return CallbackToFutureAdapter.getFuture(
+                completer -> {
+                    Futures.addCallback(
+                            getFulfillmentResponseForExecution(finalArguments),
+                            new FutureCallback<FulfillmentResponse>() {
+                                @Override
+                                public void onSuccess(FulfillmentResponse fulfillmentResponse) {
+                                    LoggerInternal.log(
+                                            CapabilityLogger.LogLevel.INFO, LOG_TAG,
+                                            "Task confirm success");
+                                    callback.onSuccess(fulfillmentResponse);
+                                    completer.set(null);
+                                }
+
+                                @Override
+                                public void onFailure(@NonNull Throwable t) {
+                                    LoggerInternal.log(CapabilityLogger.LogLevel.ERROR, LOG_TAG,
+                                            "Task confirm fail");
+                                    callback.onError(
+                                            ErrorStatusInternal.CONFIRMATION_REQUEST_FAILURE);
+                                    completer.set(null);
+                                }
+                            },
+                            mExecutor);
+                    return "handle fulfillmentResponseFuture for CONFIRM";
+                });
+    }
+
+    private void clearMissingArgs(ArgumentsWrapper assistantArgs) {
+        Set<String> argsCleared =
+                mCurrentValuesMap.keySet().stream()
+                        .filter(argName -> !assistantArgs.paramValues().containsKey(argName))
+                        .collect(toImmutableSet());
+        for (String arg : argsCleared) {
+            mCurrentValuesMap.remove(arg);
+            // TODO(b/234170829): notify listener#onReceived of the cleared arguments
+        }
+    }
+
+    /**
+     * Main processing chain for both assistant requests and manual input requests. All pending
+     * parameters contained in fulfillmentValuesMap are chained together in a serial fassion. We use
+     * Futures here to make sure long running app processing (such as argument grounding or argument
+     * validation) are executed asynchronously.
+     */
+    private ListenableFuture<Void> processFulfillmentValues(
+            Map<String, List<FulfillmentValue>> fulfillmentValuesMap) {
+        ListenableFuture<SlotProcessingResult> currentFuture =
+                Futures.immediateFuture(SlotProcessingResult.create(true, Collections.emptyList()));
+        for (Map.Entry<String, List<FulfillmentValue>> entry : fulfillmentValuesMap.entrySet()) {
+            String name = entry.getKey();
+            List<FulfillmentValue> fulfillmentValues = entry.getValue();
+            currentFuture =
+                    Futures.transformAsync(
+                            currentFuture,
+                            (previousResult) ->
+                                    maybeProcessSlotAndUpdateCurrentValues(previousResult, name,
+                                            fulfillmentValues),
+                            mExecutor,
+                            "maybeProcessSlotAndUpdateCurrentValues");
+        }
+        // Transform the final Boolean future to a void one.
+        return Futures.transform(currentFuture, (unused) -> null, mExecutor, "return null");
+    }
+
+    private ListenableFuture<SlotProcessingResult> maybeProcessSlotAndUpdateCurrentValues(
+            SlotProcessingResult previousResult, String slotKey,
+            List<FulfillmentValue> newSlotValues) {
+        List<CurrentValue> currentSlotValues =
+                mCurrentValuesMap.getOrDefault(slotKey, Collections.emptyList());
+        List<FulfillmentValue> modifiedSlotValues =
+                TaskCapabilityUtils.getMaybeModifiedSlotValues(currentSlotValues, newSlotValues);
+        if (TaskCapabilityUtils.canSkipSlotProcessing(currentSlotValues, modifiedSlotValues)) {
+            return Futures.immediateFuture(previousResult);
+        }
+        List<CurrentValue> pendingArgs =
+                TaskCapabilityUtils.fulfillmentValuesToCurrentValues(modifiedSlotValues,
+                        Status.PENDING);
+        return Futures.transform(
+                processSlot(slotKey, previousResult, pendingArgs),
+                currentResult -> {
+                    mCurrentValuesMap.put(slotKey, currentResult.processedValues());
+                    return currentResult;
+                },
+                mExecutor,
+                "update currentValuesMap");
+    }
+
+    /**
+     * Process pending param values for a slot.
+     *
+     * <p>If the previous slot was accepted, go through grounding/validation with TaskSlotProcessor,
+     * otherwise just return the pending values as is.
+     */
+    private ListenableFuture<SlotProcessingResult> processSlot(
+            String name, SlotProcessingResult previousResult, List<CurrentValue> pendingArgs) {
+        if (!previousResult.isSuccessful()) {
+            return Futures.immediateFuture(SlotProcessingResult.create(false, pendingArgs));
+        }
+        return TaskSlotProcessor.processSlot(name, pendingArgs, mParamRegistry, mExecutor);
+    }
+
+    /**
+     * Retrieve all ParamValue from accepted slots in currentValuesMap.
+     *
+     * <p>A slot is considered accepted if all CurrentValues in the slot has ACCEPTED status.
+     */
+    private Map<String, List<ParamValue>> getCurrentAcceptedArguments() {
+        return mCurrentValuesMap.entrySet().stream()
+                .filter(
+                        entry ->
+                                entry.getValue().stream()
+                                        .allMatch(currentValue -> currentValue.getStatus()
+                                                == Status.ACCEPTED))
+                .collect(
+                        toImmutableMap(
+                                Map.Entry::getKey,
+                                entry ->
+                                        entry.getValue().stream()
+                                                .map(CurrentValue::getValue)
+                                                .collect(toImmutableList())));
+    }
+
+    /**
+     * Retrieve all ParamValue from pending slots in currentValuesMap.
+     *
+     * <p>A slot is considered pending if any CurrentValues in the slot has PENDING status.
+     */
+    private Map<String, List<ParamValue>> getCurrentPendingArguments() {
+        return mCurrentValuesMap.entrySet().stream()
+                .filter(
+                        entry ->
+                                entry.getValue().stream()
+                                        .anyMatch(currentValue -> currentValue.getStatus()
+                                                == Status.PENDING))
+                .collect(
+                        toImmutableMap(
+                                Map.Entry::getKey,
+                                entry ->
+                                        entry.getValue().stream()
+                                                .map(CurrentValue::getValue)
+                                                .collect(toImmutableList())));
+    }
+
+    /** Returns true if any CurrentValue in currentValuesMap has the given Status. */
+    private boolean anyParamsOfStatus(Status status) {
+        return mCurrentValuesMap.entrySet().stream()
+                .anyMatch(
+                        entry ->
+                                entry.getValue().stream()
+                                        .anyMatch(currentValue -> currentValue.getStatus()
+                                                == status));
+    }
+
+    private ListenableFuture<ConfirmationOutput<ConfirmationT>> executeOnTaskReadyToConfirm(
+            Map<String, List<ParamValue>> finalArguments) {
+        try {
+            return mOnReadyToConfirmListener.get().onReadyToConfirm(finalArguments);
+        } catch (StructConversionException | MissingRequiredArgException e) {
+            return Futures.immediateFailedFuture(e);
+        }
+    }
+
+    private ListenableFuture<ExecutionResult<OutputT>> executeOnTaskFinish(
+            Map<String, List<ParamValue>> finalArguments) {
+        ListenableFuture<ExecutionResult<OutputT>> finishListener;
+        try {
+            finishListener = mOnFinishListener.onFinish(mActionSpec.buildArgument(finalArguments));
+        } catch (StructConversionException e) {
+            return Futures.immediateFailedFuture(e);
+        }
+        return Futures.transform(
+                finishListener,
+                executionResult -> {
+                    this.mTaskStatus = TaskStatus.COMPLETED;
+                    return executionResult;
+                },
+                mExecutor,
+                "set taskStatus to COMPLETED");
+    }
+
+    private ListenableFuture<FulfillmentResponse> getFulfillmentResponseForConfirmation(
+            Map<String, List<ParamValue>> finalArguments) {
+        return Futures.transform(
+                executeOnTaskReadyToConfirm(finalArguments),
+                result -> {
+                    FulfillmentResponse.Builder fulfillmentResponse =
+                            FulfillmentResponse.newBuilder();
+                    convertToConfirmationOutput(result).ifPresent(
+                            fulfillmentResponse::setConfirmationData);
+                    return fulfillmentResponse.build();
+                },
+                mExecutor,
+                "create FulfillmentResponse from ConfirmationOutput");
+    }
+
+    private ListenableFuture<FulfillmentResponse> getFulfillmentResponseForExecution(
+            Map<String, List<ParamValue>> finalArguments) {
+        return Futures.transform(
+                executeOnTaskFinish(finalArguments),
+                result -> {
+                    FulfillmentResponse.Builder fulfillmentResponse =
+                            FulfillmentResponse.newBuilder();
+                    if (mTaskStatus == TaskStatus.COMPLETED) {
+                        convertToExecutionOutput(result).ifPresent(
+                                fulfillmentResponse::setExecutionOutput);
+                    }
+                    return fulfillmentResponse.build();
+                },
+                mExecutor,
+                "create FulfillmentResponse from ExecutionResult");
+    }
+
+    private List<IntentParameter> addStateToParamsContext(List<IntentParameter> params) {
+        List<IntentParameter> updatedList = new ArrayList<>();
+        params.stream()
+                .forEach(
+                        param -> {
+                            List<CurrentValue> vals = mCurrentValuesMap.get(param.getName());
+                            if (vals != null) {
+                                updatedList.add(
+                                        param.toBuilder().clearCurrentValue().addAllCurrentValue(
+                                                vals).build());
+                            } else {
+                                updatedList.add(param);
+                            }
+                        });
+        return updatedList;
+    }
+
+    AppAction getAppAction() {
+        AppAction appActionWithoutState = mActionSpec.convertPropertyToProto(mProperty);
+        return appActionWithoutState.toBuilder()
+                .clearParams()
+                .addAllParams(addStateToParamsContext(appActionWithoutState.getParamsList()))
+                .setTaskInfo(TaskInfo.newBuilder().setSupportsPartialFulfillment(true))
+                .setIdentifier(mIdentifier)
+                .build();
+    }
+
+    /** Convert from java capabilities {@link ExecutionResult} to {@link StructuredOutput} proto. */
+    private Optional<StructuredOutput> convertToExecutionOutput(
+            ExecutionResult<OutputT> executionResult) {
+        OutputT output = executionResult.getOutput();
+        if (output == null || output instanceof Void) {
+            return Optional.empty();
+        }
+
+        StructuredOutput.Builder executionOutputBuilder = StructuredOutput.newBuilder();
+        for (Map.Entry<String, Function<OutputT, List<ParamValue>>> entry :
+                mExecutionOutputBindings.entrySet()) {
+            executionOutputBuilder.addOutputValues(
+                    StructuredOutput.OutputValue.newBuilder()
+                            .setName(entry.getKey())
+                            .addAllValues(entry.getValue().apply(output))
+                            .build());
+        }
+        return Optional.of(executionOutputBuilder.build());
+    }
+
+    /**
+     * Convert from java capabilities {@link ConfirmationOutput} to {@link StructuredOutput} proto.
+     */
+    private Optional<StructuredOutput> convertToConfirmationOutput(
+            ConfirmationOutput<ConfirmationT> confirmationOutput) {
+        ConfirmationT confirmation = confirmationOutput.getConfirmation();
+        if (confirmation == null || confirmation instanceof Void) {
+            return Optional.empty();
+        }
+
+        StructuredOutput.Builder confirmationOutputBuilder = StructuredOutput.newBuilder();
+        for (Map.Entry<String, Function<ConfirmationT, List<ParamValue>>> entry :
+                mConfirmationOutputBindings.entrySet()) {
+            confirmationOutputBuilder.addOutputValues(
+                    StructuredOutput.OutputValue.newBuilder()
+                            .setName(entry.getKey())
+                            .addAllValues(entry.getValue().apply(confirmation))
+                            .build());
+        }
+        return Optional.of(confirmationOutputBuilder.build());
+    }
+
+    /** State of the task internal to this capability. */
+    private enum TaskStatus {
+        UNINITIATED,
+        IN_PROGRESS,
+        COMPLETED
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskParamBinding.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskParamBinding.java
new file mode 100644
index 0000000..ef18dea
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskParamBinding.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.converters.DisambigEntityConverter;
+import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter;
+import androidx.appactions.interaction.capabilities.core.impl.converters.SearchActionConverter;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Optional;
+import java.util.function.Predicate;
+
+/**
+ * A binding between a parameter and its Property converter / Argument setter.
+ *
+ * @param <ValueTypeT>
+ */
+@AutoValue
+public abstract class TaskParamBinding<ValueTypeT> {
+
+    /** Create a TaskParamBinding for a slot. */
+    static <ValueTypeT> TaskParamBinding<ValueTypeT> create(
+            String name,
+            Predicate<ParamValue> groundingPredicate,
+            GenericResolverInternal<ValueTypeT> resolver,
+            ParamValueConverter<ValueTypeT> converter,
+            Optional<DisambigEntityConverter<ValueTypeT>> entityConverter,
+            Optional<SearchActionConverter<ValueTypeT>> searchActionConverter) {
+        return new AutoValue_TaskParamBinding<>(
+                name, groundingPredicate, resolver, converter, entityConverter,
+                searchActionConverter);
+    }
+
+    /** Returns the name of this param. */
+    @NonNull
+    public abstract String name();
+
+    /** Tests whether the ParamValue requires app-driven grounding or not. */
+    @NonNull
+    public abstract Predicate<ParamValue> groundingPredicate();
+
+    /** Stores concrete resolver for the slot. */
+    @NonNull
+    public abstract GenericResolverInternal<ValueTypeT> resolver();
+
+    /** Converts from internal {@code ParamValue} proto to public {@code ValueTypeT}. */
+    @NonNull
+    public abstract ParamValueConverter<ValueTypeT> converter();
+
+    /** Converts from the {@code ValueTypeT} to app-driven disambig entities i.e. {@code Entity}. */
+    @NonNull
+    public abstract Optional<DisambigEntityConverter<ValueTypeT>> entityConverter();
+
+    /** Converts an ungrounded {@code ParamValue} to a {@code SearchAction} object. */
+    @NonNull
+    public abstract Optional<SearchActionConverter<ValueTypeT>> searchActionConverter();
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskParamRegistry.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskParamRegistry.java
new file mode 100644
index 0000000..4db5a2b
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskParamRegistry.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.converters.DisambigEntityConverter;
+import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter;
+import androidx.appactions.interaction.capabilities.core.impl.converters.SearchActionConverter;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+/** Bindings for grounding and resolving arguments. */
+@SuppressWarnings("AutoValueImmutableFields")
+@AutoValue
+public abstract class TaskParamRegistry {
+
+    @NonNull
+    public static Builder builder() {
+        return new AutoValue_TaskParamRegistry.Builder();
+    }
+
+    /** Map of argument name to param binding. */
+    @NonNull
+    public abstract Map<String, TaskParamBinding<?>> bindings();
+
+    /** Builder for the TaskParamRegistry. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+        private final Map<String, TaskParamBinding<?>> mBindings = new HashMap<>();
+
+        abstract Builder setBindings(Map<String, TaskParamBinding<?>> bindings);
+
+        /**
+         * Register some slot related objects and method references.
+         *
+         * @param paramName          the slot name.
+         * @param groundingPredicate a function that returns true if ParamValue needs grounding,
+         *                           false
+         *                           otherwise.
+         * @param resolver           the GenericResolverInternal instance wrapping developer's slot
+         *                           listener
+         * @param entityConverter    a function that converts developer provided grounded objects
+         *                           to Entity proto
+         * @param searchActionConverter
+         * @param typeConverter      a function that converts a single ParamValue to some
+         *                           developer-facing object type
+         * @return
+         * @param <T>
+         */
+        @NonNull
+        public final <T> Builder addTaskParameter(
+                @NonNull String paramName,
+                @NonNull Predicate<ParamValue> groundingPredicate,
+                @NonNull GenericResolverInternal<T> resolver,
+                @NonNull Optional<DisambigEntityConverter<T>> entityConverter,
+                @NonNull Optional<SearchActionConverter<T>> searchActionConverter,
+                @NonNull ParamValueConverter<T> typeConverter) {
+            mBindings.put(
+                    paramName,
+                    TaskParamBinding.create(
+                            paramName,
+                            groundingPredicate,
+                            resolver,
+                            typeConverter,
+                            entityConverter,
+                            searchActionConverter));
+            return this;
+        }
+
+        abstract TaskParamRegistry autoBuild();
+
+        @NonNull
+        public TaskParamRegistry build() {
+            setBindings(Collections.unmodifiableMap(mBindings));
+            return autoBuild();
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessor.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessor.java
new file mode 100644
index 0000000..841d860
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessor.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+import static androidx.appactions.interaction.capabilities.core.impl.utils.ImmutableCollectors.toImmutableList;
+
+import androidx.appactions.interaction.capabilities.core.impl.concurrent.Futures;
+import androidx.appactions.interaction.capabilities.core.impl.converters.DisambigEntityConverter;
+import androidx.appactions.interaction.capabilities.core.impl.converters.SearchActionConverter;
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+import androidx.appactions.interaction.capabilities.core.task.EntitySearchResult;
+import androidx.appactions.interaction.capabilities.core.task.ValidationResult;
+import androidx.appactions.interaction.capabilities.core.task.impl.exceptions.InvalidResolverException;
+import androidx.appactions.interaction.capabilities.core.task.impl.exceptions.MissingEntityConverterException;
+import androidx.appactions.interaction.capabilities.core.task.impl.exceptions.MissingSearchActionConverterException;
+import androidx.appactions.interaction.capabilities.core.values.SearchAction;
+import androidx.appactions.interaction.proto.CurrentValue;
+import androidx.appactions.interaction.proto.CurrentValue.Status;
+import androidx.appactions.interaction.proto.DisambiguationData;
+import androidx.appactions.interaction.proto.Entity;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Contains static utility methods that handles processing argument slots for TaskCapabilityImpl.
+ */
+final class TaskSlotProcessor {
+
+    private TaskSlotProcessor() {
+    }
+
+    /** perform an in-app search for an ungrounded ParamValue */
+    private static <T> ListenableFuture<AppGroundingResult> ground(
+            ParamValue ungroundedParamValue, TaskParamBinding<T> binding, Executor executor) {
+        GenericResolverInternal<T> fieldResolver = binding.resolver();
+        if (!binding.entityConverter().isPresent()) {
+            return Futures.immediateFailedFuture(
+                    new MissingEntityConverterException(
+                            "No entity converter found in the binding."));
+        }
+        if (!binding.searchActionConverter().isPresent()) {
+            return Futures.immediateFailedFuture(
+                    new MissingSearchActionConverterException(
+                            "No search action converter found in the binding."));
+        }
+        DisambigEntityConverter<T> entityConverter = binding.entityConverter().get();
+        SearchActionConverter<T> searchActionConverter = binding.searchActionConverter().get();
+        try {
+            SearchAction<T> searchAction = searchActionConverter.toSearchAction(
+                    ungroundedParamValue);
+            // Note, transformAsync is needed to catch checked exceptions. See
+            // https://yaqs.corp.google.com/eng/q/2565415714299052032.
+            return Futures.transformAsync(
+                    fieldResolver.invokeLookup(searchAction),
+                    (entitySearchResult) -> {
+                        try {
+                            return Futures.immediateFuture(
+                                    processEntitySearchResult(
+                                            entitySearchResult, entityConverter,
+                                            ungroundedParamValue));
+                        } catch (StructConversionException e) {
+                            return Futures.immediateFailedFuture(e);
+                        }
+                    },
+                    executor,
+                    "processEntitySearchResult");
+        } catch (InvalidResolverException | StructConversionException e) {
+            return Futures.immediateFailedFuture(e);
+        }
+    }
+
+    /**
+     * Applies "wildcard capture" technique. For more details see
+     * https://docs.oracle.com/javase/tutorial/java/generics/capture.html
+     */
+    private static <T> ListenableFuture<ValidationResult> invokeValueChange(
+            List<ParamValue> updatedValue, TaskParamBinding<T> binding) {
+        try {
+            return binding.resolver().notifyValueChange(updatedValue, binding.converter());
+        } catch (StructConversionException e) {
+            return Futures.immediateFailedFuture(e);
+        }
+    }
+
+    /**
+     * Processes all ParamValue for a single slot.
+     *
+     * @return a {@code ListenableFuture<SlotProcessingResult>} object.
+     */
+    static ListenableFuture<SlotProcessingResult> processSlot(
+            String name,
+            List<CurrentValue> pendingArgs,
+            TaskParamRegistry taskParamRegistry,
+            Executor executor) {
+        TaskParamBinding<?> taskParamBinding = taskParamRegistry.bindings().get(name);
+        if (taskParamBinding == null) {
+            // TODO(b/234655571) use slot metadata to ensure that we never auto accept values for
+            // reference slots.
+            return Futures.immediateFuture(
+                    SlotProcessingResult.create(
+                            Boolean.TRUE,
+                            pendingArgs.stream()
+                                    .map(
+                                            pendingArg ->
+                                                    TaskCapabilityUtils.toCurrentValue(
+                                                            pendingArg.getValue(), Status.ACCEPTED))
+                                    .collect(toImmutableList())));
+        }
+        List<ParamValue> groundedValues = Collections.synchronizedList(new ArrayList<>());
+        List<CurrentValue> ungroundedValues = Collections.synchronizedList(new ArrayList<>());
+
+        ListenableFuture<AppGroundingResult> groundingFuture =
+                Futures.immediateFuture(
+                        AppGroundingResult.ofSuccess(ParamValue.getDefaultInstance()));
+
+        for (CurrentValue pendingValue : pendingArgs) {
+            if (pendingValue.hasDisambiguationData()) {
+                // assistant-driven disambiguation
+                groundingFuture =
+                        consumeGroundingResult(
+                                chainAssistantGrounding(groundingFuture, pendingValue,
+                                        taskParamBinding, executor),
+                                groundedValues,
+                                ungroundedValues,
+                                executor);
+            } else if (taskParamBinding.groundingPredicate().test(pendingValue.getValue())) {
+                // app-driven disambiguation
+                groundingFuture =
+                        consumeGroundingResult(
+                                chainAppGrounding(groundingFuture, pendingValue, taskParamBinding,
+                                        executor),
+                                groundedValues,
+                                ungroundedValues,
+                                executor);
+            } else {
+                groundedValues.add(pendingValue.getValue());
+            }
+        }
+        return Futures.transformAsync(
+                groundingFuture,
+                (unused) -> {
+                    if (groundedValues.isEmpty()) {
+                        return Futures.immediateFuture(
+                                SlotProcessingResult.create(
+                                        /** isSuccessful= */
+                                        false, Collections.unmodifiableList(ungroundedValues)));
+                    }
+                    return Futures.transform(
+                            invokeValueChange(groundedValues, taskParamBinding),
+                            validationResult ->
+                                    processValidationResult(validationResult, groundedValues,
+                                            ungroundedValues),
+                            executor,
+                            "validation");
+                },
+                executor,
+                "slot processing result");
+    }
+
+    /**
+     * Consumes the result of grounding.
+     *
+     * <p>If grounding was successful (app-driven with 1 returned result) the grounded ParamValue is
+     * added to groundedValues.
+     *
+     * <p>otherwise the ungrounded CurrentValue is added to ungroundedValues.
+     */
+    static ListenableFuture<AppGroundingResult> consumeGroundingResult(
+            ListenableFuture<AppGroundingResult> resultFuture,
+            List<ParamValue> groundedValues,
+            List<CurrentValue> ungroundedValues,
+            Executor executor) {
+        return Futures.transform(
+                resultFuture,
+                appGroundingResult -> {
+                    switch (appGroundingResult.getKind()) {
+                        case SUCCESS:
+                            groundedValues.add(appGroundingResult.success());
+                            break;
+                        case FAILURE:
+                            ungroundedValues.add(appGroundingResult.failure());
+                    }
+                    return appGroundingResult;
+                },
+                executor,
+                "consume grounding result");
+    }
+
+    /** enqueues processing of a pending value that requires assistant-driven grounding. */
+    static ListenableFuture<AppGroundingResult> chainAssistantGrounding(
+            ListenableFuture<AppGroundingResult> groundingFuture,
+            CurrentValue pendingValue,
+            TaskParamBinding<?> taskParamBinding,
+            Executor executor) {
+        return Futures.transformAsync(
+                groundingFuture,
+                previousResult -> {
+                    switch (previousResult.getKind()) {
+                        case SUCCESS:
+                            return Futures.transform(
+                                    renderAssistantDisambigData(
+                                            pendingValue.getDisambiguationData(), taskParamBinding),
+                                    unused ->
+                                            AppGroundingResult.ofFailure(
+                                                    CurrentValue.newBuilder(pendingValue).setStatus(
+                                                            Status.DISAMBIG).build()),
+                                    executor,
+                                    "renderAssistantDisambigData");
+                        case FAILURE:
+                            return Futures.immediateFuture(
+                                    AppGroundingResult.ofFailure(pendingValue));
+                    }
+                    throw new IllegalStateException("unreachable");
+                },
+                executor,
+                "assistant grounding");
+    }
+
+    /** enqueues processing of a pending value that requires app-driven grounding. */
+    static ListenableFuture<AppGroundingResult> chainAppGrounding(
+            ListenableFuture<AppGroundingResult> groundingFuture,
+            CurrentValue pendingValue,
+            TaskParamBinding<?> taskParamBinding,
+            Executor executor) {
+        return Futures.transformAsync(
+                groundingFuture,
+                previousResult -> {
+                    switch (previousResult.getKind()) {
+                        case SUCCESS:
+                            return ground(pendingValue.getValue(), taskParamBinding, executor);
+                        case FAILURE:
+                            return Futures.immediateFuture(
+                                    AppGroundingResult.ofFailure(pendingValue));
+                    }
+                    throw new IllegalStateException("unreachable");
+                },
+                executor,
+                "app grounding");
+    }
+
+    /**
+     * Processes the EntitySearchResult from performing an entity search.
+     *
+     * @param entitySearchResult the EntitySearchResult returned from the app resolver.
+     * @param ungroundedValue    the original ungrounded ParamValue.
+     */
+    private static <T> AppGroundingResult processEntitySearchResult(
+            EntitySearchResult<T> entitySearchResult,
+            DisambigEntityConverter<T> entityConverter,
+            ParamValue ungroundedValue)
+            throws StructConversionException {
+        switch (entitySearchResult.possibleValues().size()) {
+            case 0:
+                return AppGroundingResult.ofFailure(
+                        TaskCapabilityUtils.toCurrentValue(ungroundedValue, Status.REJECTED));
+            case 1:
+                Entity groundedEntity =
+                        entityConverter.convert(
+                                Objects.requireNonNull(entitySearchResult.possibleValues().get(0)));
+                return AppGroundingResult.ofSuccess(
+                        TaskCapabilityUtils.groundedValueToParamValue(groundedEntity));
+            default:
+                List<Entity> disambigEntities =
+                        getDisambigEntities(entitySearchResult.possibleValues(), entityConverter);
+                return AppGroundingResult.ofFailure(
+                        TaskCapabilityUtils.getCurrentValueForDisambiguation(
+                                ungroundedValue, disambigEntities));
+        }
+    }
+
+    private static <T> List<Entity> getDisambigEntities(
+            List<T> possibleValues, DisambigEntityConverter<T> entityConverter)
+            throws StructConversionException {
+        List<Entity> disambigEntities = new ArrayList<>();
+        for (T entity : possibleValues) {
+            disambigEntities.add(entityConverter.convert(Objects.requireNonNull(entity)));
+        }
+        return Collections.unmodifiableList(disambigEntities);
+    }
+
+    /**
+     * Processes the ValidationResult from sending argument updates to onReceived.
+     *
+     * @param validationResult the ValidationResult returned from value listener.
+     * @param groundedValues   a List of all grounded ParamValue.
+     * @param ungroundedValues a List of all ungrounded CurrentValue.
+     */
+    private static SlotProcessingResult processValidationResult(
+            ValidationResult validationResult,
+            List<ParamValue> groundedValues,
+            List<CurrentValue> ungroundedValues) {
+        List<CurrentValue> combinedValues = new ArrayList<>();
+        switch (validationResult.getKind()) {
+            case ACCEPTED:
+                combinedValues.addAll(
+                        TaskCapabilityUtils.paramValuesToCurrentValue(groundedValues,
+                                Status.ACCEPTED));
+                break;
+            case REJECTED:
+                combinedValues.addAll(
+                        TaskCapabilityUtils.paramValuesToCurrentValue(groundedValues,
+                                Status.REJECTED));
+                break;
+        }
+        combinedValues.addAll(ungroundedValues);
+        return SlotProcessingResult.create(
+                /* isSuccessful= */ ungroundedValues.isEmpty()
+                        && (validationResult.getKind() == ValidationResult.Kind.ACCEPTED),
+                Collections.unmodifiableList(combinedValues));
+    }
+
+    private static ListenableFuture<Void> renderAssistantDisambigData(
+            DisambiguationData disambiguationData, TaskParamBinding<?> binding) {
+        List<String> entityIds =
+                disambiguationData.getEntitiesList().stream()
+                        .map(Entity::getIdentifier)
+                        .collect(toImmutableList());
+        try {
+            return binding.resolver().invokeEntityRender(entityIds);
+        } catch (InvalidResolverException e) {
+            return Futures.immediateFailedFuture(e);
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskUpdateHandler.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskUpdateHandler.java
new file mode 100644
index 0000000..d769e619
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskUpdateHandler.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+import androidx.appactions.interaction.proto.ParamValue;
+
+import java.util.List;
+import java.util.Map;
+
+/** Implemented by TaskCapabilityImpl to handle manual input updates and BIC input. */
+interface TaskUpdateHandler {
+    void updateParamValues(Map<String, List<ParamValue>> paramValuesMap);
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/UpdateRequest.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/UpdateRequest.java
new file mode 100644
index 0000000..b9bf730
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/UpdateRequest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+import com.google.auto.value.AutoOneOf;
+
+/** Contains either an AssistantUpdateRequest or a TouchEventUpdateRequest */
+@AutoOneOf(UpdateRequest.Kind.class)
+abstract class UpdateRequest {
+    static UpdateRequest of(AssistantUpdateRequest request) {
+        return AutoOneOf_UpdateRequest.assistant(request);
+    }
+
+    static UpdateRequest of(TouchEventUpdateRequest request) {
+        return AutoOneOf_UpdateRequest.touchEvent(request);
+    }
+
+    abstract Kind getKind();
+
+    abstract AssistantUpdateRequest assistant();
+
+    abstract TouchEventUpdateRequest touchEvent();
+
+    enum Kind {
+        ASSISTANT,
+        TOUCH_EVENT
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/exceptions/DisambigStateException.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/exceptions/DisambigStateException.java
new file mode 100644
index 0000000..b993114
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/exceptions/DisambigStateException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl.exceptions;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Represents an internal issue with the state sync between the SDK and Assistant. One example is
+ * when the SDK places an argument in dismabig state, but then Assistant sends the same argument
+ * data again without any grounding.
+ */
+public final class DisambigStateException extends Exception {
+
+    public DisambigStateException(@NonNull String message) {
+        super(message);
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/exceptions/InvalidResolverException.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/exceptions/InvalidResolverException.java
new file mode 100644
index 0000000..24e4e77
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/exceptions/InvalidResolverException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl.exceptions;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Represents an internal issue with Resolvers, such as an "app-driven" method being invoked on a
+ * "assistant-driven" resolver.
+ */
+public final class InvalidResolverException extends Exception {
+
+    public InvalidResolverException(@NonNull String message) {
+        super(message);
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/exceptions/MissingEntityConverterException.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/exceptions/MissingEntityConverterException.java
new file mode 100644
index 0000000..6f7808a
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/exceptions/MissingEntityConverterException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl.exceptions;
+
+import androidx.annotation.NonNull;
+
+/** No entity converter is present in the {@code TaskParamBinding}. */
+public final class MissingEntityConverterException extends Exception {
+
+    public MissingEntityConverterException(@NonNull String message) {
+        super(message);
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/exceptions/MissingRequiredArgException.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/exceptions/MissingRequiredArgException.java
new file mode 100644
index 0000000..c2af79c
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/exceptions/MissingRequiredArgException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl.exceptions;
+
+import androidx.annotation.NonNull;
+
+/**
+ * During the onFinishListener handling, all required params should be present in the Map sent to
+ * the listener. If they are not for some reason, this is an internal error.
+ */
+public final class MissingRequiredArgException extends Exception {
+
+    public MissingRequiredArgException(@NonNull String message) {
+        super(message);
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/exceptions/MissingSearchActionConverterException.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/exceptions/MissingSearchActionConverterException.java
new file mode 100644
index 0000000..9d8b690
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/exceptions/MissingSearchActionConverterException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl.exceptions;
+
+import androidx.annotation.NonNull;
+
+/** No SearchAction converter is present in the {@code DialogParamBinding}. */
+public final class MissingSearchActionConverterException extends Exception {
+
+    public MissingSearchActionConverterException(@NonNull String message) {
+        super(message);
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Alarm.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Alarm.java
new file mode 100644
index 0000000..ca545f1
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Alarm.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+
+import com.google.auto.value.AutoValue;
+
+/** Represents a single item in an item list. */
+@AutoValue
+public abstract class Alarm extends Thing {
+
+    /** Create a new Alarm instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_Alarm.Builder();
+    }
+
+    /** Builder class for Alarm entities. */
+    @AutoValue.Builder
+    public abstract static class Builder extends Thing.Builder<Builder> implements
+            BuilderOf<Alarm> {
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/CalendarEvent.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/CalendarEvent.java
new file mode 100644
index 0000000..d35dcf1
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/CalendarEvent.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+import androidx.appactions.interaction.capabilities.core.values.properties.Attendee;
+
+import com.google.auto.value.AutoValue;
+
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/** Represents a CalendarEvent. */
+@SuppressWarnings("AutoValueImmutableFields")
+@AutoValue
+public abstract class CalendarEvent extends Thing {
+    /** Create a new CalendarEvent.Builder instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_CalendarEvent.Builder();
+    }
+
+    /** Returns the start date. */
+    @NonNull
+    public abstract Optional<ZonedDateTime> getStartDate();
+
+    /** Returns the end date. */
+    @NonNull
+    public abstract Optional<ZonedDateTime> getEndDate();
+
+    /** Returns the {@link Attendee}s in the CalendarEvent. */
+    @NonNull
+    public abstract List<Attendee> getAttendeeList();
+
+    /** Builder class for building a CalendarEvent. */
+    @AutoValue.Builder
+    public abstract static class Builder extends Thing.Builder<Builder>
+            implements BuilderOf<CalendarEvent> {
+
+        private final List<Attendee> mAttendeeList = new ArrayList<>();
+
+        /** Sets start date. */
+        @NonNull
+        public abstract Builder setStartDate(@NonNull ZonedDateTime startDate);
+
+        /** Sets end date. */
+        @NonNull
+        public abstract Builder setEndDate(@NonNull ZonedDateTime endDate);
+
+        /** Adds a person. */
+        @NonNull
+        public final Builder addAttendee(@NonNull Person person) {
+            mAttendeeList.add(new Attendee(person));
+            return this;
+        }
+
+        /** Adds a Attendee. */
+        @NonNull
+        public final Builder addAttendee(@NonNull Attendee attendee) {
+            mAttendeeList.add(attendee);
+            return this;
+        }
+
+        /** Add a list of attendees. */
+        @NonNull
+        public final Builder addAllAttendee(@NonNull Iterable<Attendee> attendees) {
+            for (Attendee attendee : attendees) {
+                mAttendeeList.add(attendee);
+            }
+            return this;
+        }
+
+        /** Add a list of persons. */
+        @NonNull
+        public final Builder addAllPerson(@NonNull Iterable<Person> persons) {
+            for (Person person : persons) {
+                mAttendeeList.add(new Attendee(person));
+            }
+            return this;
+        }
+
+        /** Builds and returns the CalendarEvent instance. */
+        @Override
+        @NonNull
+        public final CalendarEvent build() {
+            setAttendeeList(mAttendeeList);
+            return autoBuild();
+        }
+
+        /** Sets the attendees of the CalendarEvent. */
+        @NonNull
+        abstract Builder setAttendeeList(@NonNull List<Attendee> attendeeList);
+
+        @NonNull
+        abstract CalendarEvent autoBuild();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Call.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Call.java
new file mode 100644
index 0000000..5698873
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Call.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+import androidx.appactions.interaction.capabilities.core.values.properties.Participant;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/** Represents a Call. */
+@AutoValue
+public abstract class Call extends Thing {
+    /** Create a new Call.Builder instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_Call.Builder();
+    }
+
+    /** Returns the call format, e.g. video or audio. */
+    @NonNull
+    public abstract Optional<CallFormat> getCallFormat();
+
+    /** Returns the {@link Participant}s in the call. */
+    @NonNull
+    @SuppressWarnings("AutoValueImmutableFields")
+    public abstract List<Participant> getParticipantList();
+
+    /** Format of the call. */
+    public enum CallFormat {
+        AUDIO("Audio"),
+        VIDEO("Video");
+
+        private final String mCallFormat;
+
+        CallFormat(String callFormat) {
+            this.mCallFormat = callFormat;
+        }
+
+        @Override
+        public String toString() {
+            return mCallFormat;
+        }
+    }
+
+    /** Builder class for building a Call. */
+    @AutoValue.Builder
+    public abstract static class Builder extends Thing.Builder<Builder> implements BuilderOf<Call> {
+
+        private final List<Participant> mParticipantList = new ArrayList<>();
+
+        /** Sets call format. */
+        @NonNull
+        public abstract Builder setCallFormat(@NonNull CallFormat callFormat);
+
+        /** Adds a person. */
+        @NonNull
+        public final Builder addParticipant(@NonNull Person person) {
+            mParticipantList.add(new Participant(person));
+            return this;
+        }
+
+        /** Adds a Participant. */
+        @NonNull
+        public final Builder addParticipant(@NonNull Participant participant) {
+            mParticipantList.add(participant);
+            return this;
+        }
+
+        /** Add a list of participants. */
+        @NonNull
+        public final Builder addAllParticipant(@NonNull Iterable<Participant> participants) {
+            for (Participant participant : participants) {
+                mParticipantList.add(participant);
+            }
+            return this;
+        }
+
+        /** Add a list of persons. */
+        @NonNull
+        public final Builder addAllPerson(@NonNull Iterable<Person> persons) {
+            for (Person person : persons) {
+                mParticipantList.add(new Participant(person));
+            }
+            return this;
+        }
+
+        /** Builds and returns the Call instance. */
+        @Override
+        @NonNull
+        public final Call build() {
+            setParticipantList(mParticipantList);
+            return autoBuild();
+        }
+
+        /** Sets the participants of the call. */
+        @NonNull
+        abstract Builder setParticipantList(@NonNull List<Participant> participantList);
+
+        @NonNull
+        abstract Call autoBuild();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/EntityValue.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/EntityValue.java
new file mode 100644
index 0000000..690c591
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/EntityValue.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Optional;
+
+/**
+ * Represents an entity value for {@code ActionCapability} which includes a value and optionally an
+ * id.
+ */
+@AutoValue
+public abstract class EntityValue {
+
+    /** Returns a new Builder to build a EntityValue. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_EntityValue.Builder();
+    }
+
+    /** Returns a EntityValue that has both its id and value set to the given identifier. */
+    @NonNull
+    public static EntityValue ofId(@NonNull String id) {
+        return EntityValue.newBuilder().setId(id).setValue(id).build();
+    }
+
+    /** Returns a EntityValue that has the given value and no id. */
+    @NonNull
+    public static EntityValue ofValue(@NonNull String value) {
+        return EntityValue.newBuilder().setValue(value).build();
+    }
+
+    /** Returns the id of the EntityValue. */
+    @NonNull
+    public abstract Optional<String> getId();
+
+    /** Returns the value of the EntityValue. */
+    @NonNull
+    public abstract String getValue();
+
+    /** Builder for {@link EntityValue}. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+        /** Sets the identifier of the EntityValue to be built. */
+        @NonNull
+        public abstract Builder setId(@NonNull String id);
+
+        /** Sets The value of the EntityValue to be built. */
+        @NonNull
+        public abstract Builder setValue(@NonNull String value);
+
+        /** Builds and returns the EntityValue. */
+        @NonNull
+        public abstract EntityValue build();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/GenericErrorStatus.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/GenericErrorStatus.java
new file mode 100644
index 0000000..a7c0713
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/GenericErrorStatus.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+
+import com.google.auto.value.AutoValue;
+
+/** GenericErrorStatus for ExecutionStatus. */
+@AutoValue
+public abstract class GenericErrorStatus extends Thing {
+
+    /** Create a new GenericErrorStatus instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_GenericErrorStatus.Builder();
+    }
+
+    /** Create a new default instance. */
+    @NonNull
+    public static GenericErrorStatus getDefaultInstance() {
+        return new AutoValue_GenericErrorStatus.Builder().build();
+    }
+
+    @Override
+    public final String toString() {
+        return "GenericErrorStatus";
+    }
+
+    /** Builder class for GenericErrorStatus status. */
+    @AutoValue.Builder
+    public abstract static class Builder extends Thing.Builder<Builder>
+            implements BuilderOf<GenericErrorStatus> {
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/ItemList.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/ItemList.java
new file mode 100644
index 0000000..041663d
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/ItemList.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Represents an ItemList object. */
+@SuppressWarnings("AutoValueImmutableFields")
+@AutoValue
+public abstract class ItemList extends Thing {
+
+    /** Create a new ItemList.Builder instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_ItemList.Builder();
+    }
+
+    /** Returns the optional list items in this ItemList. */
+    @NonNull
+    public abstract List<ListItem> getListItems();
+
+    /** Builder class for building item lists with items. */
+    @AutoValue.Builder
+    public abstract static class Builder extends Thing.Builder<Builder>
+            implements BuilderOf<ItemList> {
+
+        private final List<ListItem> mListItemsToBuild = new ArrayList<>();
+
+        /** Add one or more ListItem to the ItemList to be built. */
+        @NonNull
+        public final Builder addListItem(@NonNull ListItem... listItems) {
+            Collections.addAll(mListItemsToBuild, listItems);
+            return this;
+        }
+
+        /** Add a list of ListItem to the ItemList to be built. */
+        @NonNull
+        public final Builder addAllListItems(@NonNull List<ListItem> listItems) {
+            return addListItem(listItems.toArray(ListItem[]::new));
+        }
+
+        abstract Builder setListItems(List<ListItem> listItems);
+
+        /** Builds and returns the ItemList. */
+        @Override
+        @NonNull
+        public final ItemList build() {
+            setListItems(Collections.unmodifiableList(mListItemsToBuild));
+            return autoBuild();
+        }
+
+        @NonNull
+        abstract ItemList autoBuild();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/ListItem.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/ListItem.java
new file mode 100644
index 0000000..e79e99f
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/ListItem.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+
+import com.google.auto.value.AutoValue;
+
+/** Represents a single item in an item list. */
+@AutoValue
+public abstract class ListItem extends Thing {
+
+    /** Creates a ListItem given its name. */
+    @NonNull
+    public static ListItem create(@NonNull String id, @NonNull String name) {
+        return newBuilder().setId(id).setName(name).build();
+    }
+
+    /** Create a new ListItem.Builder instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_ListItem.Builder();
+    }
+
+    /** Builder class for building item lists with items. */
+    @AutoValue.Builder
+    public abstract static class Builder extends Thing.Builder<Builder>
+            implements BuilderOf<ListItem> {
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Message.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Message.java
new file mode 100644
index 0000000..7e5d875
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Message.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+import androidx.appactions.interaction.capabilities.core.values.properties.Recipient;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/** Represents an message object. */
+@SuppressWarnings("AutoValueImmutableFields")
+@AutoValue
+public abstract class Message extends Thing {
+
+    /** Create a new Message.Builder instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_Message.Builder();
+    }
+
+    /** Returns the recipients of the message. */
+    @NonNull
+    public abstract List<Recipient> getRecipientList();
+
+    /** Returns the message text. */
+    @NonNull
+    public abstract Optional<String> getMessageText();
+
+    /** Builder class for building an Message. */
+    @AutoValue.Builder
+    public abstract static class Builder extends Thing.Builder<Builder>
+            implements BuilderOf<Message> {
+
+        private final List<Recipient> mRecipientList = new ArrayList<>();
+
+        /** Adds a {@link Person}. */
+        @NonNull
+        public final Builder addRecipient(@NonNull Person person) {
+            mRecipientList.add(new Recipient(person));
+            return this;
+        }
+
+        /** Adds a {@link Recipient}. */
+        @NonNull
+        public final Builder addRecipient(@NonNull Recipient recipient) {
+            mRecipientList.add(recipient);
+            return this;
+        }
+
+        /** Adds a list of {@link Recipient}s. */
+        @NonNull
+        public final Builder addAllRecipient(@NonNull Iterable<Recipient> recipients) {
+            for (Recipient recipient : recipients) {
+                mRecipientList.add(recipient);
+            }
+            return this;
+        }
+
+        /** Sets the message text. */
+        @NonNull
+        public abstract Builder setMessageText(@NonNull String messageText);
+
+        /** Builds and returns the Message instance. */
+        @Override
+        @NonNull
+        public final Message build() {
+            setRecipientList(mRecipientList);
+            return autoBuild();
+        }
+
+        /** Sets the recipients of the message. */
+        @NonNull
+        abstract Builder setRecipientList(@NonNull List<Recipient> recipientList);
+
+        @NonNull
+        abstract Message autoBuild();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Order.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Order.java
new file mode 100644
index 0000000..2f44202
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Order.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+
+import com.google.auto.value.AutoValue;
+
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/** Represents an order object. */
+@SuppressWarnings("AutoValueImmutableFields")
+@AutoValue
+public abstract class Order extends Thing {
+
+    /** Create a new Order.Builder instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_Order.Builder();
+    }
+
+    /** Returns the date the order was placed. */
+    @NonNull
+    public abstract Optional<ZonedDateTime> getOrderDate();
+
+    /** Returns the {@link OrderItem}s in the order. */
+    @NonNull
+    public abstract List<OrderItem> getOrderedItems();
+
+    /** Returns the current status of the order. */
+    @NonNull
+    public abstract Optional<OrderStatus> getOrderStatus();
+
+    /** Returns the name of the seller. */
+    @NonNull
+    public abstract Optional<Organization> getSeller();
+
+    /** Returns the delivery information. */
+    @NonNull
+    public abstract Optional<ParcelDelivery> getOrderDelivery();
+
+    /** Status of the order. */
+    public enum OrderStatus {
+        ORDER_CANCELED("OrderCanceled"),
+        ORDER_DELIVERED("OrderDelivered"),
+        ORDER_IN_TRANSIT("OrderInTransit"),
+        ORDER_PAYMENT_DUE("OrderPaymentDue"),
+        ORDER_PICKUP_AVAILABLE("OrderPickupAvailable"),
+        ORDER_PROBLEM("OrderProblem"),
+        ORDER_PROCESSING("OrderProcessing"),
+        ORDER_RETURNED("OrderReturned");
+
+        private final String mStringValue;
+
+        OrderStatus(String stringValue) {
+            this.mStringValue = stringValue;
+        }
+
+        @Override
+        public String toString() {
+            return mStringValue;
+        }
+    }
+
+    /** Builder class for building an Order. */
+    @AutoValue.Builder
+    public abstract static class Builder extends Thing.Builder<Builder> implements
+            BuilderOf<Order> {
+
+        /** Order items to build. */
+        private final List<OrderItem> mOrderItems = new ArrayList<>();
+
+        /** Sets the date the order was placed. */
+        @NonNull
+        public abstract Builder setOrderDate(@NonNull ZonedDateTime orderDate);
+
+        /** Sets the ordered items. */
+        @NonNull
+        abstract Builder setOrderedItems(@NonNull List<OrderItem> orderItems);
+
+        /** Adds an item to the order. */
+        @NonNull
+        public final Builder addOrderedItem(@NonNull OrderItem orderItem) {
+            mOrderItems.add(orderItem);
+            return this;
+        }
+
+        /** Add a list of OrderItem. */
+        @NonNull
+        public final Builder addAllOrderedItems(@NonNull List<OrderItem> orderItems) {
+            this.mOrderItems.addAll(orderItems);
+            return this;
+        }
+
+        /** Sets the current order status. */
+        @NonNull
+        public abstract Builder setOrderStatus(@NonNull OrderStatus orderStatus);
+
+        /** Sets the name of the seller. */
+        @NonNull
+        public abstract Builder setSeller(@NonNull Organization seller);
+
+        /** Sets the order delivery. */
+        @NonNull
+        public abstract Builder setOrderDelivery(@NonNull ParcelDelivery parcelDelivery);
+
+        /** Builds and returns the Order instance. */
+        @Override
+        @NonNull
+        public final Order build() {
+            setOrderedItems(mOrderItems);
+            return autoBuild();
+        }
+
+        @NonNull
+        abstract Order autoBuild();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/OrderItem.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/OrderItem.java
new file mode 100644
index 0000000..0475268
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/OrderItem.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+
+import com.google.auto.value.AutoValue;
+
+/** Represents an item in an order. */
+@AutoValue
+public abstract class OrderItem extends Thing {
+
+    /** Create a new OrderItem.Builder instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_OrderItem.Builder();
+    }
+
+    /** Builder class for building an OrderItem. */
+    @AutoValue.Builder
+    public abstract static class Builder extends Thing.Builder<Builder>
+            implements BuilderOf<OrderItem> {
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Organization.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Organization.java
new file mode 100644
index 0000000..9f5171a
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Organization.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+
+import com.google.auto.value.AutoValue;
+
+/** Represents an organization. */
+@AutoValue
+public abstract class Organization extends Thing {
+
+    /** Create a new Organization.Builder instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_Organization.Builder();
+    }
+
+    /** Builder class for building an Organization. */
+    @AutoValue.Builder
+    public abstract static class Builder extends Thing.Builder<Builder>
+            implements BuilderOf<Organization> {
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/ParcelDelivery.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/ParcelDelivery.java
new file mode 100644
index 0000000..a2437cd
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/ParcelDelivery.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+
+import com.google.auto.value.AutoValue;
+
+import java.time.ZonedDateTime;
+import java.util.Optional;
+
+/** The delivery of a parcel. */
+@AutoValue
+public abstract class ParcelDelivery extends Thing {
+
+    /** Create a new ParcelDelivery.Builder instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_ParcelDelivery.Builder();
+    }
+
+    /** Returns the delivery address. */
+    @NonNull
+    public abstract Optional<String> getDeliveryAddress();
+
+    /** Returns the method used for delivery or shipping. */
+    @NonNull
+    public abstract Optional<String> getDeliveryMethod();
+
+    /** Returns the earliest date the package may arrive. */
+    @NonNull
+    public abstract Optional<ZonedDateTime> getExpectedArrivalFrom();
+
+    /** Returns the latest date the package may arrive. */
+    @NonNull
+    public abstract Optional<ZonedDateTime> getExpectedArrivalUntil();
+
+    /** Returns the tracking number. */
+    @NonNull
+    public abstract Optional<String> getTrackingNumber();
+
+    /** Returns the tracking URL. */
+    @NonNull
+    public abstract Optional<String> getTrackingUrl();
+
+    /** Builder class for building ParcelDelivery. */
+    @AutoValue.Builder
+    public abstract static class Builder extends Thing.Builder<Builder>
+            implements BuilderOf<ParcelDelivery> {
+
+        /** Sets the delivery address. */
+        @NonNull
+        public abstract Builder setDeliveryAddress(@NonNull String deliveryAddress);
+
+        /** Sets the delivery method. */
+        @NonNull
+        public abstract Builder setDeliveryMethod(@NonNull String deliveryMethod);
+
+        /** Sets the earliest date the package may arrive. */
+        @NonNull
+        public abstract Builder setExpectedArrivalFrom(@NonNull ZonedDateTime expectedArrivalFrom);
+
+        /** Sets the latest date the package may arrive. */
+        @NonNull
+        public abstract Builder setExpectedArrivalUntil(
+                @NonNull ZonedDateTime expectedArrivalUntil);
+
+        /** Sets the tracking number. */
+        @NonNull
+        public abstract Builder setTrackingNumber(@NonNull String trackingNumber);
+
+        /** Sets the tracking URL. */
+        @NonNull
+        public abstract Builder setTrackingUrl(@NonNull String trackingUrl);
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Person.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Person.java
new file mode 100644
index 0000000..a54bb59
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Person.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Optional;
+
+/** Represents a person. */
+@AutoValue
+public abstract class Person extends Thing {
+    /** Create a new Person.Builder instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_Person.Builder();
+    }
+
+    /** Returns the email. */
+    @NonNull
+    public abstract Optional<String> getEmail();
+
+    /** Returns the telephone. */
+    @NonNull
+    public abstract Optional<String> getTelephone();
+
+    /** Builder class for building a Person. */
+    @AutoValue.Builder
+    public abstract static class Builder extends Thing.Builder<Builder> implements
+            BuilderOf<Person> {
+        /** Sets the email. */
+        @NonNull
+        public abstract Builder setEmail(@NonNull String email);
+
+        /** Sets the telephone. */
+        @NonNull
+        public abstract Builder setTelephone(@NonNull String telephone);
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/SafetyCheck.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/SafetyCheck.java
new file mode 100644
index 0000000..afe240c
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/SafetyCheck.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+
+import com.google.auto.value.AutoValue;
+
+import java.time.Duration;
+import java.time.ZonedDateTime;
+import java.util.Optional;
+
+/** Represents a SafetyCheck. */
+@AutoValue
+public abstract class SafetyCheck extends Thing {
+    /** Create a new SafetyCheck instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_SafetyCheck.Builder();
+    }
+
+    /** Returns the duration. */
+    @NonNull
+    public abstract Optional<Duration> getDuration();
+
+    /** Returns the check-in time. */
+    @NonNull
+    public abstract Optional<ZonedDateTime> getCheckinTime();
+
+    /** Builder class for building a SafetyCheck. */
+    @AutoValue.Builder
+    public abstract static class Builder extends Thing.Builder<Builder>
+            implements BuilderOf<SafetyCheck> {
+        @NonNull
+        public abstract Builder setDuration(@NonNull Duration duration);
+
+        @NonNull
+        public abstract Builder setCheckinTime(@NonNull ZonedDateTime checkinTime);
+
+        /** Builds and returns the SafetyCheck instance. */
+        @Override
+        @NonNull
+        public final SafetyCheck build() {
+            return autoBuild();
+        }
+
+        @NonNull
+        abstract SafetyCheck autoBuild();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/SearchAction.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/SearchAction.java
new file mode 100644
index 0000000..1e472320
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/SearchAction.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Optional;
+
+/**
+ * Represents a request to perform search for some in-app entities.
+ *
+ * @param <T>
+ */
+@AutoValue
+public abstract class SearchAction<T> {
+
+    /** Returns a new Builder instance for SearchAction. */
+    @NonNull
+    public static <T> Builder<T> newBuilder() {
+        return new AutoValue_SearchAction.Builder<>();
+    }
+
+    /** The String query of this SearchAction. */
+    @NonNull
+    public abstract Optional<String> getQuery();
+
+    /** The object to search for of this SearchAction. */
+    @NonNull
+    public abstract Optional<T> getObject();
+
+    /**
+     * Builder class for SearchAction.
+     *
+     * @param <T>
+     */
+    @AutoValue.Builder
+    public abstract static class Builder<T> implements BuilderOf<SearchAction<T>> {
+        /** Sets the String query of this SearchAction. */
+        @NonNull
+        public abstract Builder<T> setQuery(@NonNull String query);
+
+        /** Sets the Object query of this SearchAction. */
+        @NonNull
+        public abstract Builder<T> setObject(T object);
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/StringOrEnumValue.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/StringOrEnumValue.java
new file mode 100644
index 0000000..2c41ecb
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/StringOrEnumValue.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.value.AutoOneOf;
+
+/**
+ * Represents a string or enum argument value for {@code ActionCapability}.
+ *
+ * @param <EnumT>
+ */
+@AutoOneOf(StringOrEnumValue.Kind.class)
+public abstract class StringOrEnumValue<EnumT extends Enum<EnumT>> {
+    /** Creates a StringOrEnumValue instance with the given String. */
+    @NonNull
+    public static <EnumT extends Enum<EnumT>> StringOrEnumValue<EnumT> ofStringValue(
+            @NonNull String s) {
+        return AutoOneOf_StringOrEnumValue.stringValue(s);
+    }
+
+    /** Creates a StringOrEnumValue instance with the given Enum value. */
+    @NonNull
+    public static <EnumT extends Enum<EnumT>> StringOrEnumValue<EnumT> ofEnumValue(
+            @NonNull EnumT enumValue) {
+        return AutoOneOf_StringOrEnumValue.enumValue(enumValue);
+    }
+
+    /** The Kind of this StringOrEnumValue. */
+    @NonNull
+    public abstract Kind getKind();
+
+    /** The String value of this StringOrEnumValue, for Kind.STRING_VALUE. */
+    @NonNull
+    public abstract String stringValue();
+
+    /** The Enum value of this StringOrEnumValue, for Kind.ENUM_VALUE. */
+    @NonNull
+    public abstract EnumT enumValue();
+
+    /** Possible argument type. */
+    public enum Kind {
+        STRING_VALUE,
+        ENUM_VALUE,
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/SuccessStatus.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/SuccessStatus.java
new file mode 100644
index 0000000..af4469a
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/SuccessStatus.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+
+import com.google.auto.value.AutoValue;
+
+/** SuccessStatus for ExecutionStatus. */
+@AutoValue
+public abstract class SuccessStatus extends Thing {
+
+    /** Create a new SuccessStatus instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_SuccessStatus.Builder();
+    }
+
+    /** Create a new default instance. */
+    @NonNull
+    public static SuccessStatus getDefaultInstance() {
+        return new AutoValue_SuccessStatus.Builder().build();
+    }
+
+    @Override
+    public final String toString() {
+        return "SuccessStatus";
+    }
+
+    /** Builder class for SuccessStatus status. */
+    @AutoValue.Builder
+    public abstract static class Builder extends Thing.Builder<Builder>
+            implements BuilderOf<SuccessStatus> {
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Thing.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Thing.java
new file mode 100644
index 0000000..7d897f0
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Thing.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+
+import java.util.Optional;
+
+/** Common interface for structured entity. */
+public abstract class Thing {
+    /** Returns the id of this thing. */
+    @NonNull
+    public abstract Optional<String> getId();
+
+    /** Returns the name of this thing. */
+    @NonNull
+    public abstract Optional<String> getName();
+
+    /**
+     * Base builder class that can be extended to build objects that extend Thing.
+     *
+     * @param <T>
+     */
+    public abstract static class Builder<T extends Builder<T>> {
+        /** Sets the id of the Thing to be built. */
+        @NonNull
+        public abstract T setId(@NonNull String id);
+
+        /** Sets the name of the Thing to be built. */
+        @NonNull
+        public abstract T setName(@NonNull String name);
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Timer.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Timer.java
new file mode 100644
index 0000000..80627b4
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/Timer.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+
+import com.google.auto.value.AutoValue;
+
+/** Represents a Timer. */
+@AutoValue
+public abstract class Timer extends Thing {
+
+    /** Create a new Timer instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_Timer.Builder();
+    }
+
+    /** Builder class for Timer. */
+    @AutoValue.Builder
+    public abstract static class Builder extends Thing.Builder<Builder> implements
+            BuilderOf<Timer> {
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/executionstatus/ActionAlreadyInProgress.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/executionstatus/ActionAlreadyInProgress.java
new file mode 100644
index 0000000..57fe650
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/executionstatus/ActionAlreadyInProgress.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values.executionstatus;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+import androidx.appactions.interaction.capabilities.core.values.Thing;
+
+import com.google.auto.value.AutoValue;
+
+/** Error status for execution failure due to the action being in progress. */
+@AutoValue
+public abstract class ActionAlreadyInProgress extends Thing {
+
+    /** Create a new ActionAlreadyInProgress instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_ActionAlreadyInProgress.Builder();
+    }
+
+    /** Create a new default instance. */
+    @NonNull
+    public static ActionAlreadyInProgress getDefaultInstance() {
+        return new AutoValue_ActionAlreadyInProgress.Builder().build();
+    }
+
+    @Override
+    public final String toString() {
+        return "ActionAlreadyInProgress";
+    }
+
+    /** Builder class for ActionAlreadyInProgress status. */
+    @AutoValue.Builder
+    public abstract static class Builder extends Thing.Builder<Builder>
+            implements BuilderOf<ActionAlreadyInProgress> {
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/executionstatus/ActionNotInProgress.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/executionstatus/ActionNotInProgress.java
new file mode 100644
index 0000000..4b342a6
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/executionstatus/ActionNotInProgress.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values.executionstatus;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+import androidx.appactions.interaction.capabilities.core.values.Thing;
+
+import com.google.auto.value.AutoValue;
+
+/** Error status for execution failure due to the action not being in progress. */
+@AutoValue
+public abstract class ActionNotInProgress extends Thing {
+
+    /** Create a new ActionNotInProgress instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_ActionNotInProgress.Builder();
+    }
+
+    /** Create a new default instance. */
+    @NonNull
+    public static ActionNotInProgress getDefaultInstance() {
+        return new AutoValue_ActionNotInProgress.Builder().build();
+    }
+
+    @Override
+    public final String toString() {
+        return "ActionNotInProgress";
+    }
+
+    /** Builder class for ActionNotInProgress status. */
+    @AutoValue.Builder
+    public abstract static class Builder extends Thing.Builder<Builder>
+            implements BuilderOf<ActionNotInProgress> {
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/executionstatus/NoInternetConnection.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/executionstatus/NoInternetConnection.java
new file mode 100644
index 0000000..3a3bb5b
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/executionstatus/NoInternetConnection.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values.executionstatus;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+import androidx.appactions.interaction.capabilities.core.values.Thing;
+
+import com.google.auto.value.AutoValue;
+
+/** Error status for execution failure due to no internet connection. */
+@AutoValue
+public abstract class NoInternetConnection extends Thing {
+
+    /** Create a new NoInternetConnection instance. */
+    @NonNull
+    public static Builder newBuilder() {
+        return new AutoValue_NoInternetConnection.Builder();
+    }
+
+    /** Create a new default instance. */
+    @NonNull
+    public static NoInternetConnection getDefaultInstance() {
+        return new AutoValue_NoInternetConnection.Builder().build();
+    }
+
+    @Override
+    public final String toString() {
+        return "NoInternetConnection";
+    }
+
+    /** Builder class for NoInternetConnection status. */
+    @AutoValue.Builder
+    public abstract static class Builder extends Thing.Builder<Builder>
+            implements BuilderOf<NoInternetConnection> {
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/properties/Attendee.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/properties/Attendee.java
new file mode 100644
index 0000000..1c5ef74
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/properties/Attendee.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values.properties;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appactions.interaction.capabilities.core.values.Person;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Optional;
+
+/**
+ * Represents the value of the union property: http://schema.org/attendee, currently it only can
+ * contain {@link Person}.
+ */
+public class Attendee {
+    private final Value mValue;
+
+    public Attendee(@NonNull Person person) {
+        mValue = new AutoValue_Attendee_Value(Optional.of(person));
+    }
+
+    @NonNull
+    public Optional<Person> asPerson() {
+        return mValue.person();
+    }
+
+    @Override
+    public int hashCode() {
+        return mValue.hashCode();
+    }
+
+    @Override
+    public boolean equals(@Nullable Object object) {
+        if (object instanceof Attendee) {
+            Attendee that = (Attendee) object;
+            return this.mValue.equals(that.mValue);
+        }
+        return false;
+    }
+
+    /** Represents the value in the this wrapper class. */
+    @AutoValue
+    abstract static class Value {
+        abstract Optional<Person> person();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/properties/Participant.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/properties/Participant.java
new file mode 100644
index 0000000..02cbc7b
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/properties/Participant.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values.properties;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appactions.interaction.capabilities.core.values.Person;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Optional;
+
+/**
+ * Represents the value of the union property: http://schema.org/participant, currently it only can
+ * contain {@link Person}.
+ */
+public class Participant {
+    private final Value mValue;
+
+    public Participant(@NonNull Person person) {
+        mValue = new AutoValue_Participant_Value(Optional.of(person));
+    }
+
+    @NonNull
+    public Optional<Person> asPerson() {
+        return mValue.person();
+    }
+
+    @Override
+    public int hashCode() {
+        return mValue.hashCode();
+    }
+
+    @Override
+    public boolean equals(@Nullable Object object) {
+        if (object instanceof Participant) {
+            Participant that = (Participant) object;
+            return this.mValue.equals(that.mValue);
+        }
+        return false;
+    }
+
+    /** Represents the value in the this wrapper class. */
+    @AutoValue
+    abstract static class Value {
+        abstract Optional<Person> person();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/properties/Recipient.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/properties/Recipient.java
new file mode 100644
index 0000000..56d2021
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/values/properties/Recipient.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.values.properties;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appactions.interaction.capabilities.core.values.Person;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Optional;
+
+/**
+ * Represents the value of the union property: http://schema.org/recipient, currently it only can
+ * contain {@link Person}.
+ */
+public class Recipient {
+    private final Value mValue;
+
+    public Recipient(@NonNull Person person) {
+        mValue = new AutoValue_Recipient_Value(Optional.of(person));
+    }
+
+    @NonNull
+    public Optional<Person> asPerson() {
+        return mValue.person();
+    }
+
+    @Override
+    public int hashCode() {
+        return mValue.hashCode();
+    }
+
+    @Override
+    public boolean equals(@Nullable Object object) {
+        if (object instanceof Recipient) {
+            Recipient that = (Recipient) object;
+            return this.mValue.equals(that.mValue);
+        }
+        return false;
+    }
+
+    /** Represents the value in the this wrapper class. */
+    @AutoValue
+    abstract static class Value {
+        abstract Optional<Person> person();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/FuturesTest.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/FuturesTest.java
new file mode 100644
index 0000000..0016b7e
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/FuturesTest.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.concurrent;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.testing.spec.SettableFutureWrapper;
+import androidx.test.filters.SdkSuppress;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.concurrent.ExecutionException;
+import java.util.function.Function;
+
+@RunWith(JUnit4.class)
+public final class FuturesTest {
+
+    @SdkSuppress(minSdkVersion = 24)
+    @Test
+    public void addCallback_onSuccess() throws Exception {
+        SettableFutureWrapper<Integer> integerFutureWrapper = new SettableFutureWrapper<>();
+        SettableFutureWrapper<Boolean> testFutureWrapper = new SettableFutureWrapper<>();
+        Futures.addCallback(
+                integerFutureWrapper.getFuture(),
+                new FutureCallback<Integer>() {
+                    @Override
+                    public void onSuccess(Integer value) {
+                        assertThat(value).isEqualTo(25);
+                        testFutureWrapper.set(true);
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable t) {
+                        testFutureWrapper.set(false);
+                    }
+                },
+                Runnable::run);
+
+        assertThat(testFutureWrapper.getFuture().isDone()).isFalse();
+        integerFutureWrapper.set(25);
+        assertThat(testFutureWrapper.getFuture().get()).isTrue();
+    }
+
+    @SdkSuppress(minSdkVersion = 24)
+    @Test
+    public void addCallback_onFailure() throws Exception {
+        SettableFutureWrapper<Object> objectFutureWrapper = new SettableFutureWrapper<>();
+        SettableFutureWrapper<Boolean> testFutureWrapper = new SettableFutureWrapper<>();
+        Futures.addCallback(
+                objectFutureWrapper.getFuture(),
+                new FutureCallback<Object>() {
+                    @Override
+                    public void onSuccess(Object value) {
+                        testFutureWrapper.set(false);
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable t) {
+                        assertThat(t).isInstanceOf(IllegalStateException.class);
+                        testFutureWrapper.set(true);
+                    }
+                },
+                Runnable::run);
+
+        assertThat(testFutureWrapper.getFuture().isDone()).isFalse();
+        objectFutureWrapper.setException(new IllegalStateException());
+        assertThat(testFutureWrapper.getFuture().get()).isTrue();
+    }
+
+    @SdkSuppress(minSdkVersion = 24)
+    @Test
+    public void transform_success() throws Exception {
+        SettableFutureWrapper<Integer> integerFutureWrapper = new SettableFutureWrapper<>();
+        ListenableFuture<Integer> transformedFuture =
+                Futures.transform(integerFutureWrapper.getFuture(), (x) -> x + 10, Runnable::run,
+                        "add 10");
+
+        assertThat(transformedFuture.isDone()).isFalse();
+        integerFutureWrapper.set(25);
+        assertThat(transformedFuture.get()).isEqualTo(35);
+    }
+
+    @SdkSuppress(minSdkVersion = 24)
+    @Test
+    public void transformAsync_success() throws Exception {
+        SettableFutureWrapper<Integer> integerFutureWrapper = new SettableFutureWrapper<>();
+        ListenableFuture<Integer> transformedFuture =
+                Futures.transformAsync(
+                        integerFutureWrapper.getFuture(),
+                        (x) -> {
+                            SettableFutureWrapper<Integer> transformFuture =
+                                    new SettableFutureWrapper<>();
+                            transformFuture.set(x + 10);
+                            return transformFuture.getFuture();
+                        },
+                        Runnable::run,
+                        "add 10 async");
+
+        assertThat(transformedFuture.isDone()).isFalse();
+        integerFutureWrapper.set(25);
+        assertThat(transformedFuture.get()).isEqualTo(35);
+    }
+
+    @SdkSuppress(minSdkVersion = 24)
+    @Test
+    public void immediateFuture_success() throws Exception {
+        ListenableFuture<Integer> immediateFuture = Futures.immediateFuture(25);
+        immediateFuture.cancel(true);
+        assertThat(immediateFuture.isCancelled()).isFalse();
+        assertThat(immediateFuture.isDone()).isTrue();
+        assertThat(immediateFuture.get()).isEqualTo(25);
+    }
+
+    @SdkSuppress(minSdkVersion = 24)
+    @Test
+    public void immediateVoidFuture_success() throws Exception {
+        ListenableFuture<Void> immediateVoidFuture = Futures.immediateVoidFuture();
+        immediateVoidFuture.cancel(true);
+        assertThat(immediateVoidFuture.isCancelled()).isFalse();
+        assertThat(immediateVoidFuture.isDone()).isTrue();
+        assertThat(immediateVoidFuture.get()).isEqualTo(null);
+    }
+
+    @SdkSuppress(minSdkVersion = 24)
+    @Test
+    public void immediateFailedFuture_failure() throws Exception {
+        ListenableFuture<Object> immediateFailedFuture =
+                Futures.immediateFailedFuture(new CustomException());
+        ListenableFuture<Object> transformedFuture =
+                Futures.transform(immediateFailedFuture, Function.identity(), Runnable::run,
+                        "test");
+
+        assertThat(transformedFuture.isDone()).isTrue();
+        ExecutionException e = assertThrows(ExecutionException.class, transformedFuture::get);
+        assertThat(e).hasCauseThat().isInstanceOf(CustomException.class);
+    }
+
+    @SdkSuppress(minSdkVersion = 24)
+    @Test
+    public void transform_synchronousExceptionPropagated() throws Exception {
+        Function<Integer, Integer> badTransform =
+                (unused) -> {
+                    throw new IllegalStateException();
+                };
+        ListenableFuture<Integer> transformedFuture =
+                Futures.transform(Futures.immediateFuture(25), badTransform, Runnable::run,
+                        "badTransform");
+        assertThat(transformedFuture.isDone()).isTrue();
+
+        SettableFutureWrapper<Throwable> errorContainer = new SettableFutureWrapper<>();
+        Futures.addCallback(
+                transformedFuture,
+                new FutureCallback<Integer>() {
+                    @Override
+                    public void onSuccess(Integer value) {
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable t) {
+                        errorContainer.set(t);
+                    }
+                },
+                Runnable::run);
+        assertThat(errorContainer.getFuture().get()).isInstanceOf(IllegalStateException.class);
+        ExecutionException e = assertThrows(ExecutionException.class, transformedFuture::get);
+        assertThat(e).hasCauseThat().isInstanceOf(IllegalStateException.class);
+    }
+
+    @SdkSuppress(minSdkVersion = 24)
+    @Test
+    public void transformAsync_synchronousExceptionPropagated() throws Exception {
+        Function<Integer, ListenableFuture<Integer>> badAsyncTransform =
+                (unused) -> {
+                    throw new IllegalStateException();
+                };
+        ListenableFuture<Integer> transformedFuture =
+                Futures.transformAsync(
+                        Futures.immediateFuture(25), badAsyncTransform, Runnable::run,
+                        "badAsyncTransform");
+        assertThat(transformedFuture.isDone()).isTrue();
+
+        SettableFutureWrapper<Throwable> errorContainer = new SettableFutureWrapper<>();
+        Futures.addCallback(
+                transformedFuture,
+                new FutureCallback<Integer>() {
+                    @Override
+                    public void onSuccess(Integer value) {
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable t) {
+                        errorContainer.set(t);
+                    }
+                },
+                Runnable::run);
+        assertThat(errorContainer.getFuture().get()).isInstanceOf(IllegalStateException.class);
+        ExecutionException e = assertThrows(ExecutionException.class, transformedFuture::get);
+        assertThat(e).hasCauseThat().isInstanceOf(IllegalStateException.class);
+    }
+
+    private static class CustomException extends Exception {
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConvertersTest.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConvertersTest.java
new file mode 100644
index 0000000..2721638
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConvertersTest.java
@@ -0,0 +1,869 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.converters;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+import androidx.appactions.interaction.capabilities.core.values.Alarm;
+import androidx.appactions.interaction.capabilities.core.values.CalendarEvent;
+import androidx.appactions.interaction.capabilities.core.values.Call;
+import androidx.appactions.interaction.capabilities.core.values.EntityValue;
+import androidx.appactions.interaction.capabilities.core.values.ItemList;
+import androidx.appactions.interaction.capabilities.core.values.ListItem;
+import androidx.appactions.interaction.capabilities.core.values.Message;
+import androidx.appactions.interaction.capabilities.core.values.Order;
+import androidx.appactions.interaction.capabilities.core.values.OrderItem;
+import androidx.appactions.interaction.capabilities.core.values.Organization;
+import androidx.appactions.interaction.capabilities.core.values.ParcelDelivery;
+import androidx.appactions.interaction.capabilities.core.values.Person;
+import androidx.appactions.interaction.capabilities.core.values.SafetyCheck;
+import androidx.appactions.interaction.capabilities.core.values.SearchAction;
+import androidx.appactions.interaction.capabilities.core.values.Timer;
+import androidx.appactions.interaction.capabilities.core.values.properties.Participant;
+import androidx.appactions.interaction.capabilities.core.values.properties.Recipient;
+import androidx.appactions.interaction.proto.Entity;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import com.google.protobuf.ListValue;
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.zone.ZoneRulesException;
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public final class TypeConvertersTest {
+
+    private static final Order ORDER_JAVA_THING =
+            Order.newBuilder()
+                    .setId("id")
+                    .setName("name")
+                    .addOrderedItem(OrderItem.newBuilder().setName("apples").build())
+                    .addOrderedItem(OrderItem.newBuilder().setName("oranges").build())
+                    .setSeller(Organization.newBuilder().setName("Google").build())
+                    .setOrderDate(ZonedDateTime.of(2022, 1, 1, 8, 0, 0, 0, ZoneOffset.UTC))
+                    .setOrderStatus(Order.OrderStatus.ORDER_DELIVERED)
+                    .setOrderDelivery(
+                            ParcelDelivery.newBuilder()
+                                    .setDeliveryAddress("test address")
+                                    .setDeliveryMethod("UPS")
+                                    .setTrackingNumber("A12345")
+                                    .setTrackingUrl("https://")
+                                    .build())
+                    .build();
+    private static final Person PERSON_JAVA_THING =
+            Person.newBuilder()
+                    .setName("name")
+                    .setEmail("email")
+                    .setTelephone("telephone")
+                    .setId("id")
+                    .build();
+    private static final Person PERSON_JAVA_THING_2 = Person.newBuilder().setId("id2").build();
+    private static final CalendarEvent CALENDAR_EVENT_JAVA_THING =
+            CalendarEvent.newBuilder()
+                    .setStartDate(ZonedDateTime.of(2022, 1, 1, 8, 0, 0, 0, ZoneOffset.UTC))
+                    .setEndDate(ZonedDateTime.of(2023, 1, 1, 8, 0, 0, 0, ZoneOffset.UTC))
+                    .addAttendee(PERSON_JAVA_THING)
+                    .addAttendee(PERSON_JAVA_THING_2)
+                    .build();
+    private static final Call CALL_JAVA_THING =
+            Call.newBuilder()
+                    .setId("id")
+                    .setCallFormat(Call.CallFormat.AUDIO)
+                    .addParticipant(PERSON_JAVA_THING)
+                    .build();
+    private static final Message MESSAGE_JAVA_THING =
+            Message.newBuilder()
+                    .setId("id")
+                    .addRecipient(PERSON_JAVA_THING)
+                    .setMessageText("hello")
+                    .build();
+    private static final SafetyCheck SAFETY_CHECK_JAVA_THING =
+            SafetyCheck.newBuilder()
+                    .setId("id")
+                    .setDuration(Duration.ofMinutes(5))
+                    .setCheckinTime(ZonedDateTime.of(2023, 01, 10, 10, 0, 0, 0, ZoneOffset.UTC))
+                    .build();
+    private static final ListValue ORDER_ITEMS_STRUCT =
+            ListValue.newBuilder()
+                    .addValues(
+                            Value.newBuilder()
+                                    .setStructValue(
+                                            Struct.newBuilder()
+                                                    .putFields(
+                                                            "@type",
+                                                            Value.newBuilder()
+                                                                    .setStringValue("OrderItem")
+                                                                    .build())
+                                                    .putFields(
+                                                            "name",
+                                                            Value.newBuilder()
+                                                                    .setStringValue("apples")
+                                                                    .build()))
+                                    .build())
+                    .addValues(
+                            Value.newBuilder()
+                                    .setStructValue(
+                                            Struct.newBuilder()
+                                                    .putFields(
+                                                            "@type",
+                                                            Value.newBuilder()
+                                                                    .setStringValue("OrderItem")
+                                                                    .build())
+                                                    .putFields(
+                                                            "name",
+                                                            Value.newBuilder()
+                                                                    .setStringValue("oranges")
+                                                                    .build()))
+                                    .build())
+                    .build();
+    private static final Struct PARCEL_DELIVERY_STRUCT =
+            Struct.newBuilder()
+                    .putFields("@type", Value.newBuilder().setStringValue("ParcelDelivery").build())
+                    .putFields(
+                            "deliveryAddress",
+                            Value.newBuilder().setStringValue("test address").build())
+                    .putFields(
+                            "hasDeliveryMethod", Value.newBuilder().setStringValue("UPS").build())
+                    .putFields(
+                            "trackingNumber", Value.newBuilder().setStringValue("A12345").build())
+                    .putFields("trackingUrl", Value.newBuilder().setStringValue("https://").build())
+                    .build();
+    private static final Struct ORGANIZATION_STRUCT =
+            Struct.newBuilder()
+                    .putFields("@type", Value.newBuilder().setStringValue("Organization").build())
+                    .putFields("name", Value.newBuilder().setStringValue("Google").build())
+                    .build();
+    private static final Struct ORDER_STRUCT =
+            Struct.newBuilder()
+                    .putFields("@type", Value.newBuilder().setStringValue("Order").build())
+                    .putFields("identifier", Value.newBuilder().setStringValue("id").build())
+                    .putFields("name", Value.newBuilder().setStringValue("name").build())
+                    .putFields(
+                            "orderDate",
+                            Value.newBuilder().setStringValue("2022-01-01T08:00Z").build())
+                    .putFields(
+                            "orderDelivery",
+                            Value.newBuilder().setStructValue(PARCEL_DELIVERY_STRUCT).build())
+                    .putFields(
+                            "orderedItem",
+                            Value.newBuilder().setListValue(ORDER_ITEMS_STRUCT).build())
+                    .putFields(
+                            "orderStatus",
+                            Value.newBuilder().setStringValue("OrderDelivered").build())
+                    .putFields(
+                            "seller",
+                            Value.newBuilder().setStructValue(ORGANIZATION_STRUCT).build())
+                    .build();
+    private static final Struct PERSON_STRUCT =
+            Struct.newBuilder()
+                    .putFields("@type", Value.newBuilder().setStringValue("Person").build())
+                    .putFields("identifier", Value.newBuilder().setStringValue("id").build())
+                    .putFields("name", Value.newBuilder().setStringValue("name").build())
+                    .putFields("email", Value.newBuilder().setStringValue("email").build())
+                    .putFields("telephone", Value.newBuilder().setStringValue("telephone").build())
+                    .build();
+    private static final Struct PERSON_STRUCT_2 =
+            Struct.newBuilder()
+                    .putFields("@type", Value.newBuilder().setStringValue("Person").build())
+                    .putFields("identifier", Value.newBuilder().setStringValue("id2").build())
+                    .build();
+    private static final Struct CALENDAR_EVENT_STRUCT =
+            Struct.newBuilder()
+                    .putFields("@type", Value.newBuilder().setStringValue("CalendarEvent").build())
+                    .putFields(
+                            "startDate",
+                            Value.newBuilder().setStringValue("2022-01-01T08:00Z").build())
+                    .putFields(
+                            "endDate",
+                            Value.newBuilder().setStringValue("2023-01-01T08:00Z").build())
+                    .putFields(
+                            "attendee",
+                            Value.newBuilder()
+                                    .setListValue(
+                                            ListValue.newBuilder()
+                                                    .addValues(
+                                                            Value.newBuilder()
+                                                                    .setStructValue(PERSON_STRUCT)
+                                                                    .build())
+                                                    .addValues(
+                                                            Value.newBuilder()
+                                                                    .setStructValue(PERSON_STRUCT_2)
+                                                                    .build()))
+                                    .build())
+                    .build();
+    private static final Struct CALL_STRUCT =
+            Struct.newBuilder()
+                    .putFields("@type", Value.newBuilder().setStringValue("Call").build())
+                    .putFields("callFormat", Value.newBuilder().setStringValue("Audio").build())
+                    .putFields(
+                            "participant",
+                            Value.newBuilder()
+                                    .setListValue(
+                                            ListValue.newBuilder()
+                                                    .addValues(
+                                                            Value.newBuilder()
+                                                                    .setStructValue(PERSON_STRUCT)))
+                                    .build())
+                    .build();
+    private static final Struct MESSAGE_STRUCT =
+            Struct.newBuilder()
+                    .putFields("@type", Value.newBuilder().setStringValue("Message").build())
+                    .putFields(
+                            "recipient",
+                            Value.newBuilder()
+                                    .setListValue(
+                                            ListValue.newBuilder()
+                                                    .addValues(
+                                                            Value.newBuilder()
+                                                                    .setStructValue(PERSON_STRUCT))
+                                                    .build())
+                                    .build())
+                    .putFields("text", Value.newBuilder().setStringValue("hello").build())
+                    .build();
+    private static final Struct SAFETY_CHECK_STRUCT =
+            Struct.newBuilder()
+                    .putFields("@type", Value.newBuilder().setStringValue("SafetyCheck").build())
+                    .putFields("identifier", Value.newBuilder().setStringValue("id").build())
+                    .putFields("duration", Value.newBuilder().setStringValue("PT5M").build())
+                    .putFields(
+                            "checkinTime",
+                            Value.newBuilder().setStringValue("2023-01-10T10:00Z").build())
+                    .build();
+
+    private static ParamValue toParamValue(Struct struct, String identifier) {
+        return ParamValue.newBuilder().setIdentifier(identifier).setStructValue(struct).build();
+    }
+
+    private static Entity toEntity(Struct struct) {
+        return Entity.newBuilder().setIdentifier("id").setValue(struct).build();
+    }
+
+    @Test
+    public void toEntityValue() throws Exception {
+        List<ParamValue> input =
+                Collections.singletonList(
+                        ParamValue.newBuilder()
+                                .setIdentifier("entity-id")
+                                .setStringValue("string-val")
+                                .build());
+
+        assertThat(SlotTypeConverter.ofSingular(TypeConverters::toEntityValue).convert(input))
+                .isEqualTo(
+                        EntityValue.newBuilder().setId("entity-id").setValue("string-val").build());
+    }
+
+    @Test
+    public void toIntegerValue() throws Exception {
+        List<ParamValue> input =
+                Collections.singletonList(ParamValue.newBuilder().setNumberValue(5).build());
+
+        assertThat(SlotTypeConverter.ofSingular(TypeConverters::toIntegerValue).convert(input))
+                .isEqualTo(5);
+    }
+
+    @Test
+    public void toStringValue_fromList() throws Exception {
+        List<ParamValue> input =
+                Collections.singletonList(
+                        ParamValue.newBuilder().setStringValue("hello world").build());
+
+        assertThat(SlotTypeConverter.ofSingular(TypeConverters::toStringValue).convert(input))
+                .isEqualTo("hello world");
+    }
+
+    @Test
+    public void toStringValue_withIdentifier() throws Exception {
+        List<ParamValue> input =
+                Collections.singletonList(
+                        ParamValue.newBuilder()
+                                .setIdentifier("id1")
+                                .setStringValue("hello world")
+                                .build());
+
+        assertThat(SlotTypeConverter.ofSingular(TypeConverters::toStringValue).convert(input))
+                .isEqualTo("id1");
+    }
+
+    @Test
+    public void toStringValue_fromSingleParam() {
+        ParamValue input = ParamValue.newBuilder().setStringValue("hello world").build();
+
+        assertThat(TypeConverters.toStringValue(input)).isEqualTo("hello world");
+    }
+
+    @Test
+    public void alarm_conversions_matchesExpected() throws Exception {
+        Alarm alarm = Alarm.newBuilder().setId("id").build();
+
+        assertThat(
+                        TypeConverters.toAssistantAlarm(
+                                ParamValue.newBuilder().setIdentifier("id").build()))
+                .isEqualTo(alarm);
+    }
+
+    @Test
+    public void listItem_conversions_matchesExpected() throws Exception {
+        ListItem listItem = ListItem.create("itemId", "Test Item");
+        Struct listItemStruct =
+                Struct.newBuilder()
+                        .putFields("@type", Value.newBuilder().setStringValue("ListItem").build())
+                        .putFields(
+                                "identifier", Value.newBuilder().setStringValue("itemId").build())
+                        .putFields("name", Value.newBuilder().setStringValue("Test Item").build())
+                        .build();
+        Entity listItemProto =
+                Entity.newBuilder().setIdentifier("itemId").setValue(listItemStruct).build();
+
+        assertThat(TypeConverters.toEntity(listItem)).isEqualTo(listItemProto);
+        assertThat(TypeConverters.toListItem(toParamValue(listItemStruct, "itemId")))
+                .isEqualTo(listItem);
+    }
+
+    @Test
+    public void itemList_conversions_matchesExpected() throws Exception {
+        ItemList itemList =
+                ItemList.newBuilder()
+                        .setId("testList")
+                        .setName("Test List")
+                        .addListItem(
+                                ListItem.create("item1", "apple"),
+                                ListItem.create("item2", "banana"))
+                        .build();
+        Struct itemListStruct =
+                Struct.newBuilder()
+                        .putFields("@type", Value.newBuilder().setStringValue("ItemList").build())
+                        .putFields(
+                                "identifier", Value.newBuilder().setStringValue("testList").build())
+                        .putFields("name", Value.newBuilder().setStringValue("Test List").build())
+                        .putFields(
+                                "itemListElement",
+                                Value.newBuilder()
+                                        .setListValue(
+                                                ListValue.newBuilder()
+                                                        .addValues(
+                                                                Value.newBuilder()
+                                                                        .setStructValue(
+                                                                                Struct.newBuilder()
+                                                                                        .putFields(
+                                                                                                "@type",
+                                                                                                Value
+                                                                                                        .newBuilder()
+                                                                                                        .setStringValue(
+                                                                                                                "ListItem")
+                                                                                                        .build())
+                                                                                        .putFields(
+                                                                                                "identifier",
+                                                                                                Value
+                                                                                                        .newBuilder()
+                                                                                                        .setStringValue(
+                                                                                                                "item1")
+                                                                                                        .build())
+                                                                                        .putFields(
+                                                                                                "name",
+                                                                                                Value
+                                                                                                        .newBuilder()
+                                                                                                        .setStringValue(
+                                                                                                                "apple")
+                                                                                                        .build())
+                                                                                        .build())
+                                                                        .build())
+                                                        .addValues(
+                                                                Value.newBuilder()
+                                                                        .setStructValue(
+                                                                                Struct.newBuilder()
+                                                                                        .putFields(
+                                                                                                "@type",
+                                                                                                Value
+                                                                                                        .newBuilder()
+                                                                                                        .setStringValue(
+                                                                                                                "ListItem")
+                                                                                                        .build())
+                                                                                        .putFields(
+                                                                                                "identifier",
+                                                                                                Value
+                                                                                                        .newBuilder()
+                                                                                                        .setStringValue(
+                                                                                                                "item2")
+                                                                                                        .build())
+                                                                                        .putFields(
+                                                                                                "name",
+                                                                                                Value
+                                                                                                        .newBuilder()
+                                                                                                        .setStringValue(
+                                                                                                                "banana")
+                                                                                                        .build())
+                                                                                        .build())
+                                                                        .build())
+                                                        .build())
+                                        .build())
+                        .build();
+        Entity itemListProto =
+                Entity.newBuilder().setIdentifier("testList").setValue(itemListStruct).build();
+
+        assertThat(TypeConverters.toEntity(itemList)).isEqualTo(itemListProto);
+        assertThat(TypeConverters.toItemList(toParamValue(itemListStruct, "testList")))
+                .isEqualTo(itemList);
+    }
+
+    @Test
+    public void order_conversions_matchesExpected() throws Exception {
+        assertThat(TypeConverters.toParamValue(ORDER_JAVA_THING))
+                .isEqualTo(toParamValue(ORDER_STRUCT, "id"));
+        assertThat(TypeConverters.toOrder(toParamValue(ORDER_STRUCT, "id")))
+                .isEqualTo(ORDER_JAVA_THING);
+        assertThat(TypeConverters.toEntity(ORDER_JAVA_THING)).isEqualTo(toEntity(ORDER_STRUCT));
+    }
+
+    @Test
+    public void participant_conversions_matchesExpected() throws Exception {
+        ParamValue paramValue =
+                ParamValue.newBuilder()
+                        .setIdentifier(PERSON_JAVA_THING.getId().orElse("id"))
+                        .setStructValue(PERSON_STRUCT)
+                        .build();
+        Participant participant = new Participant(PERSON_JAVA_THING);
+
+        assertThat(TypeConverters.toParamValue(participant)).isEqualTo(paramValue);
+        assertThat(TypeConverters.toParticipant(paramValue)).isEqualTo(participant);
+    }
+
+    @Test
+    public void calendarEvent_conversions_matchesExpected() throws Exception {
+        assertThat(TypeConverters.CALENDAR_EVENT_TYPE_SPEC.toStruct(CALENDAR_EVENT_JAVA_THING))
+                .isEqualTo(CALENDAR_EVENT_STRUCT);
+        assertThat(TypeConverters.CALENDAR_EVENT_TYPE_SPEC.fromStruct(CALENDAR_EVENT_STRUCT))
+                .isEqualTo(CALENDAR_EVENT_JAVA_THING);
+    }
+
+    @Test
+    public void recipient_conversions_matchesExpected() throws Exception {
+        ParamValue paramValue =
+                ParamValue.newBuilder()
+                        .setIdentifier(PERSON_JAVA_THING.getId().orElse("id"))
+                        .setStructValue(PERSON_STRUCT)
+                        .build();
+        Recipient recipient = new Recipient(PERSON_JAVA_THING);
+
+        assertThat(TypeConverters.toParamValue(recipient)).isEqualTo(paramValue);
+        assertThat(TypeConverters.toRecipient(paramValue)).isEqualTo(recipient);
+    }
+
+    @Test
+    public void toParticipant_unexpectedType_throwsException() {
+        Struct malformedStruct =
+                Struct.newBuilder()
+                        .putFields("@type", Value.newBuilder().setStringValue("Malformed").build())
+                        .build();
+
+        assertThrows(
+                StructConversionException.class,
+                () -> TypeConverters.toParticipant(toParamValue(malformedStruct, "id")));
+    }
+
+    @Test
+    public void toRecipient_unexpectedType_throwsException() {
+        Struct malformedStruct =
+                Struct.newBuilder()
+                        .putFields("@type", Value.newBuilder().setStringValue("Malformed").build())
+                        .build();
+
+        assertThrows(
+                StructConversionException.class,
+                () -> TypeConverters.toRecipient(toParamValue(malformedStruct, "id")));
+    }
+
+    @Test
+    public void itemList_malformedStruct_throwsException() {
+        Struct malformedStruct =
+                Struct.newBuilder()
+                        .putFields("@type", Value.newBuilder().setStringValue("Malformed").build())
+                        .putFields("name", Value.newBuilder().setStringValue("List Name").build())
+                        .putFields("identifier", Value.newBuilder().setStringValue("list1").build())
+                        .build();
+
+        assertThrows(
+                StructConversionException.class,
+                () -> TypeConverters.toItemList(toParamValue(malformedStruct, "list1")));
+    }
+
+    @Test
+    public void listItem_malformedStruct_throwsException() throws Exception {
+        Struct malformedStruct =
+                Struct.newBuilder()
+                        .putFields("@type", Value.newBuilder().setStringValue("Malformed").build())
+                        .putFields("name", Value.newBuilder().setStringValue("Apple").build())
+                        .putFields("identifier", Value.newBuilder().setStringValue("item1").build())
+                        .build();
+
+        assertThrows(
+                StructConversionException.class,
+                () -> TypeConverters.toListItem(toParamValue(malformedStruct, "item1")));
+    }
+
+    @Test
+    public void toBoolean_success() throws Exception {
+        List<ParamValue> input =
+                Collections.singletonList(ParamValue.newBuilder().setBoolValue(false).build());
+
+        assertThat(SlotTypeConverter.ofSingular(TypeConverters::toBooleanValue).convert(input))
+                .isFalse();
+    }
+
+    @Test
+    public void toBoolean_throwsException() {
+        List<ParamValue> input = Collections.singletonList(ParamValue.getDefaultInstance());
+
+        StructConversionException thrown =
+                assertThrows(
+                        StructConversionException.class,
+                        () ->
+                                SlotTypeConverter.ofSingular(TypeConverters::toBooleanValue)
+                                        .convert(input));
+        assertThat(thrown)
+                .hasMessageThat()
+                .isEqualTo("Cannot parse boolean because bool_value is missing from ParamValue.");
+    }
+
+    @Test
+    public void toLocalDate_success() throws Exception {
+        List<ParamValue> input =
+                Collections.singletonList(
+                        ParamValue.newBuilder().setStringValue("2018-06-17").build());
+
+        assertThat(SlotTypeConverter.ofSingular(TypeConverters::toLocalDate).convert(input))
+                .isEqualTo(LocalDate.of(2018, 6, 17));
+    }
+
+    @Test
+    public void toLocalDate_throwsException() {
+        List<ParamValue> input =
+                Collections.singletonList(
+                        ParamValue.newBuilder().setStringValue("2018-0617").build());
+
+        StructConversionException thrown =
+                assertThrows(
+                        StructConversionException.class,
+                        () ->
+                                SlotTypeConverter.ofSingular(TypeConverters::toLocalDate)
+                                        .convert(input));
+        assertThat(thrown)
+                .hasMessageThat()
+                .isEqualTo("Failed to parse ISO 8601 string to LocalDate");
+    }
+
+    @Test
+    public void toLocalDateMissingValue_throwsException() {
+        List<ParamValue> input = Collections.singletonList(ParamValue.getDefaultInstance());
+
+        StructConversionException thrown =
+                assertThrows(
+                        StructConversionException.class,
+                        () ->
+                                SlotTypeConverter.ofSingular(TypeConverters::toLocalDate)
+                                        .convert(input));
+        assertThat(thrown)
+                .hasMessageThat()
+                .isEqualTo("Cannot parse date because string_value is missing from ParamValue.");
+    }
+
+    @Test
+    public void toLocalTime_success() throws Exception {
+        List<ParamValue> input =
+                Collections.singletonList(
+                        ParamValue.newBuilder().setStringValue("15:10:05").build());
+
+        assertThat(SlotTypeConverter.ofSingular(TypeConverters::toLocalTime).convert(input))
+                .isEqualTo(LocalTime.of(15, 10, 5));
+    }
+
+    @Test
+    public void toLocalTime_throwsException() {
+        List<ParamValue> input =
+                Collections.singletonList(ParamValue.newBuilder().setStringValue("15:1:5").build());
+
+        StructConversionException thrown =
+                assertThrows(
+                        StructConversionException.class,
+                        () ->
+                                SlotTypeConverter.ofSingular(TypeConverters::toLocalTime)
+                                        .convert(input));
+        assertThat(thrown)
+                .hasMessageThat()
+                .isEqualTo("Failed to parse ISO 8601 string to LocalTime");
+    }
+
+    @Test
+    public void toLocalTimeMissingValue_throwsException() {
+        List<ParamValue> input = Collections.singletonList(ParamValue.getDefaultInstance());
+
+        StructConversionException thrown =
+                assertThrows(
+                        StructConversionException.class,
+                        () ->
+                                SlotTypeConverter.ofSingular(TypeConverters::toLocalTime)
+                                        .convert(input));
+        assertThat(thrown)
+                .hasMessageThat()
+                .isEqualTo("Cannot parse time because string_value is missing from ParamValue.");
+    }
+
+    @Test
+    public void toZoneId_success() throws Exception {
+        List<ParamValue> input =
+                Collections.singletonList(
+                        ParamValue.newBuilder().setStringValue("America/New_York").build());
+
+        assertThat(SlotTypeConverter.ofSingular(TypeConverters::toZoneId).convert(input))
+                .isEqualTo(ZoneId.of("America/New_York"));
+    }
+
+    @Test
+    public void toZoneId_throwsException() {
+        List<ParamValue> input =
+                Collections.singletonList(
+                        ParamValue.newBuilder().setStringValue("America/New_Yo").build());
+
+        ZoneRulesException thrown =
+                assertThrows(
+                        ZoneRulesException.class,
+                        () ->
+                                SlotTypeConverter.ofSingular(TypeConverters::toZoneId)
+                                        .convert(input));
+        assertThat(thrown).hasMessageThat().isEqualTo("Unknown time-zone ID: America/New_Yo");
+    }
+
+    @Test
+    public void toZoneIdMissingValue_throwsException() {
+        List<ParamValue> input = Collections.singletonList(ParamValue.getDefaultInstance());
+
+        StructConversionException thrown =
+                assertThrows(
+                        StructConversionException.class,
+                        () ->
+                                SlotTypeConverter.ofSingular(TypeConverters::toZoneId)
+                                        .convert(input));
+        assertThat(thrown)
+                .hasMessageThat()
+                .isEqualTo("Cannot parse ZoneId because string_value is missing from ParamValue.");
+    }
+
+    @Test
+    public void toZonedDateTime_fromList() throws Exception {
+        List<ParamValue> input =
+                Collections.singletonList(
+                        ParamValue.newBuilder().setStringValue("2018-06-17T15:10:05Z").build());
+
+        assertThat(SlotTypeConverter.ofSingular(TypeConverters::toZonedDateTime).convert(input))
+                .isEqualTo(ZonedDateTime.of(2018, 6, 17, 15, 10, 5, 0, ZoneOffset.UTC));
+    }
+
+    @Test
+    public void toZonedDateTime_invalidStringFormat_throwsException() {
+        List<ParamValue> input =
+                Collections.singletonList(
+                        ParamValue.newBuilder()
+                                .setStringValue("Failed to parse ISO 8601 string to ZonedDateTime")
+                                .build());
+
+        StructConversionException thrown =
+                assertThrows(
+                        StructConversionException.class,
+                        () ->
+                                SlotTypeConverter.ofSingular(TypeConverters::toZonedDateTime)
+                                        .convert(input));
+        assertThat(thrown)
+                .hasMessageThat()
+                .isEqualTo("Failed to parse ISO 8601 string to ZonedDateTime");
+    }
+
+    @Test
+    public void toZonedDateTime_stringTypeMissing_throwsException() {
+        List<ParamValue> input =
+                Collections.singletonList(ParamValue.newBuilder().setNumberValue(100).build());
+
+        StructConversionException thrown =
+                assertThrows(
+                        StructConversionException.class,
+                        () ->
+                                SlotTypeConverter.ofSingular(TypeConverters::toZonedDateTime)
+                                        .convert(input));
+        assertThat(thrown)
+                .hasMessageThat()
+                .isEqualTo(
+                        "Cannot parse datetime because string_value is missing from ParamValue.");
+    }
+
+    @Test
+    public void toDuration_success() throws Exception {
+        List<ParamValue> input =
+                Collections.singletonList(ParamValue.newBuilder().setStringValue("PT5M").build());
+
+        Duration convertedDuration =
+                SlotTypeConverter.ofSingular(TypeConverters::toDuration).convert(input);
+
+        assertThat(convertedDuration).isEqualTo(Duration.ofMinutes(5));
+    }
+
+    @Test
+    public void toDuration_stringTypeMissing_throwsException() {
+        List<ParamValue> input =
+                Collections.singletonList(ParamValue.newBuilder().setNumberValue(100).build());
+
+        StructConversionException thrown =
+                assertThrows(
+                        StructConversionException.class,
+                        () ->
+                                SlotTypeConverter.ofSingular(TypeConverters::toDuration)
+                                        .convert(input));
+        assertThat(thrown)
+                .hasMessageThat()
+                .isEqualTo(
+                        "Cannot parse duration because string_value is missing from ParamValue.");
+    }
+
+    @Test
+    public void searchActionConverter_withoutNestedObject() throws Exception {
+        ParamValue input =
+                ParamValue.newBuilder()
+                        .setStructValue(
+                                Struct.newBuilder()
+                                        .putFields(
+                                                "@type",
+                                                Value.newBuilder()
+                                                        .setStringValue("SearchAction")
+                                                        .build())
+                                        .putFields(
+                                                "query",
+                                                Value.newBuilder()
+                                                        .setStringValue("grocery")
+                                                        .build())
+                                        .build())
+                        .build();
+
+        SearchAction<ItemList> output =
+                TypeConverters.createSearchActionConverter(TypeConverters.ITEM_LIST_TYPE_SPEC)
+                        .toSearchAction(input);
+
+        assertThat(output).isEqualTo(SearchAction.newBuilder().setQuery("grocery").build());
+    }
+
+    @Test
+    public void searchActionConverter_withNestedObject() throws Exception {
+        ItemList itemList =
+                ItemList.newBuilder()
+                        .addListItem(ListItem.newBuilder().setName("sugar").build())
+                        .build();
+        Struct nestedObject = TypeConverters.ITEM_LIST_TYPE_SPEC.toStruct(itemList);
+        ParamValue input =
+                ParamValue.newBuilder()
+                        .setStructValue(
+                                Struct.newBuilder()
+                                        .putFields(
+                                                "@type",
+                                                Value.newBuilder()
+                                                        .setStringValue("SearchAction")
+                                                        .build())
+                                        .putFields(
+                                                "object",
+                                                Value.newBuilder()
+                                                        .setStructValue(nestedObject)
+                                                        .build())
+                                        .build())
+                        .build();
+
+        SearchAction<ItemList> output =
+                TypeConverters.createSearchActionConverter(TypeConverters.ITEM_LIST_TYPE_SPEC)
+                        .toSearchAction(input);
+
+        assertThat(output).isEqualTo(SearchAction.newBuilder().setObject(itemList).build());
+    }
+
+    @Test
+    public void toParamValues_string_success() {
+        ParamValue output = TypeConverters.toParamValue("grocery");
+
+        assertThat(output).isEqualTo(ParamValue.newBuilder().setStringValue("grocery").build());
+    }
+
+    @Test
+    public void toTimer_success() throws Exception {
+        Timer timer = Timer.newBuilder().setId("abc").build();
+
+        assertThat(
+                        TypeConverters.toTimer(
+                                ParamValue.newBuilder()
+                                        .setStructValue(
+                                                Struct.newBuilder()
+                                                        .putFields(
+                                                                "@type",
+                                                                Value.newBuilder()
+                                                                        .setStringValue("Timer")
+                                                                        .build())
+                                                        .putFields(
+                                                                "identifier",
+                                                                Value.newBuilder()
+                                                                        .setStringValue("abc")
+                                                                        .build()))
+                                        .build()))
+                .isEqualTo(timer);
+    }
+
+    @Test
+    public void toParamValues_call_success() {
+        assertThat(TypeConverters.toParamValue(CALL_JAVA_THING))
+                .isEqualTo(
+                        ParamValue.newBuilder()
+                                .setStructValue(CALL_STRUCT)
+                                .setIdentifier("id")
+                                .build());
+    }
+
+    @Test
+    public void toParamValues_message_success() {
+        assertThat(TypeConverters.toParamValue(MESSAGE_JAVA_THING))
+                .isEqualTo(
+                        ParamValue.newBuilder()
+                                .setStructValue(MESSAGE_STRUCT)
+                                .setIdentifier("id")
+                                .build());
+    }
+
+    @Test
+    public void toParamValues_safetyCheck_success() {
+        assertThat(TypeConverters.toParamValue(SAFETY_CHECK_JAVA_THING))
+                .isEqualTo(
+                        ParamValue.newBuilder()
+                                .setStructValue(SAFETY_CHECK_STRUCT)
+                                .setIdentifier("id")
+                                .build());
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImplTest.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImplTest.java
new file mode 100644
index 0000000..52d6c2a
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImplTest.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.converters;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
+import androidx.appactions.interaction.capabilities.core.testing.spec.TestEntity;
+
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.time.Duration;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+
+@RunWith(JUnit4.class)
+public final class TypeSpecImplTest {
+
+    @Test
+    public void bindEnumField_convertsSuccessfully() throws Exception {
+        TypeSpec<TestEntity> entityTypeSpec =
+                TypeSpecBuilder.newBuilder("TestEntity", TestEntity::newBuilder)
+                        .bindEnumField(
+                                "enum", TestEntity::getEnum, TestEntity.Builder::setEnum,
+                                TestEntity.TestEnum.class)
+                        .build();
+        TestEntity entity = TestEntity.newBuilder().setEnum(TestEntity.TestEnum.VALUE_1).build();
+        Struct entityStruct =
+                Struct.newBuilder()
+                        .putFields("@type", Value.newBuilder().setStringValue("TestEntity").build())
+                        .putFields("enum", Value.newBuilder().setStringValue("value_1").build())
+                        .build();
+
+        Struct convertedStruct = entityTypeSpec.toStruct(entity);
+        assertThat(convertedStruct).isEqualTo(entityStruct);
+
+        TestEntity convertedEntity = entityTypeSpec.fromStruct(entityStruct);
+        assertThat(convertedEntity).isEqualTo(entity);
+    }
+
+    @Test
+    public void bindEnumField_throwsException() throws Exception {
+        TypeSpec<TestEntity> entityTypeSpec =
+                TypeSpecBuilder.newBuilder("TestEntity", TestEntity::newBuilder)
+                        .bindEnumField(
+                                "enum", TestEntity::getEnum, TestEntity.Builder::setEnum,
+                                TestEntity.TestEnum.class)
+                        .build();
+        Struct malformedStruct =
+                Struct.newBuilder()
+                        .putFields("@type", Value.newBuilder().setStringValue("TestEntity").build())
+                        .putFields("enum", Value.newBuilder().setStringValue("invalid").build())
+                        .build();
+
+        assertThrows(StructConversionException.class,
+                () -> entityTypeSpec.fromStruct(malformedStruct));
+    }
+
+    @Test
+    public void bindDurationField_convertsSuccessfully() throws Exception {
+        TypeSpec<TestEntity> entityTypeSpec =
+                TypeSpecBuilder.newBuilder("TestEntity", TestEntity::newBuilder)
+                        .bindDurationField("duration", TestEntity::getDuration,
+                                TestEntity.Builder::setDuration)
+                        .build();
+        TestEntity entity = TestEntity.newBuilder().setDuration(Duration.ofMinutes(5)).build();
+        Struct entityStruct =
+                Struct.newBuilder()
+                        .putFields("@type", Value.newBuilder().setStringValue("TestEntity").build())
+                        .putFields("duration", Value.newBuilder().setStringValue("PT5M").build())
+                        .build();
+
+        Struct convertedStruct = entityTypeSpec.toStruct(entity);
+        assertThat(convertedStruct).isEqualTo(entityStruct);
+
+        TestEntity convertedEntity = entityTypeSpec.fromStruct(entityStruct);
+        assertThat(convertedEntity).isEqualTo(entity);
+    }
+
+    @Test
+    public void bindZonedDateTimeField_convertsSuccessfully() throws Exception {
+        TypeSpec<TestEntity> entityTypeSpec =
+                TypeSpecBuilder.newBuilder("TestEntity", TestEntity::newBuilder)
+                        .bindZonedDateTimeField(
+                                "date", TestEntity::getZonedDateTime,
+                                TestEntity.Builder::setZonedDateTime)
+                        .build();
+        TestEntity entity =
+                TestEntity.newBuilder()
+                        .setZonedDateTime(ZonedDateTime.of(2022, 1, 1, 8, 0, 0, 0, ZoneOffset.UTC))
+                        .build();
+        Struct entityStruct =
+                Struct.newBuilder()
+                        .putFields("@type", Value.newBuilder().setStringValue("TestEntity").build())
+                        .putFields("date",
+                                Value.newBuilder().setStringValue("2022-01-01T08:00Z").build())
+                        .build();
+
+        Struct convertedStruct = entityTypeSpec.toStruct(entity);
+        assertThat(convertedStruct).isEqualTo(entityStruct);
+
+        TestEntity convertedEntity = entityTypeSpec.fromStruct(entityStruct);
+        assertThat(convertedEntity).isEqualTo(entity);
+    }
+
+    @Test
+    public void bindZonedDateTimeField_zoneId_convertsSuccessfully() throws Exception {
+        TypeSpec<TestEntity> entityTypeSpec =
+                TypeSpecBuilder.newBuilder("TestEntity", TestEntity::newBuilder)
+                        .bindZonedDateTimeField(
+                                "date", TestEntity::getZonedDateTime,
+                                TestEntity.Builder::setZonedDateTime)
+                        .build();
+        TestEntity entity =
+                TestEntity.newBuilder()
+                        .setZonedDateTime(
+                                ZonedDateTime.of(2022, 1, 1, 8, 0, 0, 0, ZoneId.of("UTC+01:00")))
+                        .build();
+        Struct entityStruct =
+                Struct.newBuilder()
+                        .putFields("@type", Value.newBuilder().setStringValue("TestEntity").build())
+                        .putFields("date",
+                                Value.newBuilder().setStringValue("2022-01-01T08:00+01:00").build())
+                        .build();
+        TestEntity expectedEntity =
+                TestEntity.newBuilder()
+                        .setZonedDateTime(
+                                ZonedDateTime.of(2022, 1, 1, 8, 0, 0, 0, ZoneOffset.of("+01:00")))
+                        .build();
+
+        Struct convertedStruct = entityTypeSpec.toStruct(entity);
+        assertThat(convertedStruct).isEqualTo(entityStruct);
+
+        TestEntity convertedEntity = entityTypeSpec.fromStruct(entityStruct);
+        assertThat(convertedEntity).isEqualTo(expectedEntity);
+    }
+
+    @Test
+    public void bindZonedDateTimeField_throwsException() throws Exception {
+        TypeSpec<TestEntity> entityTypeSpec =
+                TypeSpecBuilder.newBuilder("TestEntity", TestEntity::newBuilder)
+                        .bindZonedDateTimeField(
+                                "date", TestEntity::getZonedDateTime,
+                                TestEntity.Builder::setZonedDateTime)
+                        .build();
+        Struct malformedStruct =
+                Struct.newBuilder()
+                        .putFields("@type", Value.newBuilder().setStringValue("TestEntity").build())
+                        .putFields("date",
+                                Value.newBuilder().setStringValue("2022-01-01T08").build())
+                        .build();
+
+        assertThrows(StructConversionException.class,
+                () -> entityTypeSpec.fromStruct(malformedStruct));
+    }
+
+    @Test
+    public void bindSpecField_convertsSuccessfully() throws Exception {
+        TypeSpec<TestEntity> entityTypeSpec =
+                TypeSpecBuilder.newBuilder("TestEntity", TestEntity::newBuilder)
+                        .bindSpecField(
+                                "entity",
+                                TestEntity::getEntity,
+                                TestEntity.Builder::setEntity,
+                                TypeSpecBuilder.newBuilder("TestEntity", TestEntity::newBuilder)
+                                        .bindStringField("name", TestEntity::getName,
+                                                TestEntity.Builder::setName)
+                                        .build())
+                        .build();
+        TestEntity entity =
+                TestEntity.newBuilder()
+                        .setEntity(TestEntity.newBuilder().setName("entity name").build())
+                        .build();
+        Struct entityStruct =
+                Struct.newBuilder()
+                        .putFields("@type", Value.newBuilder().setStringValue("TestEntity").build())
+                        .putFields(
+                                "entity",
+                                Value.newBuilder()
+                                        .setStructValue(
+                                                Struct.newBuilder()
+                                                        .putFields(
+                                                                "@type",
+                                                                Value.newBuilder().setStringValue(
+                                                                        "TestEntity").build())
+                                                        .putFields(
+                                                                "name",
+                                                                Value.newBuilder().setStringValue(
+                                                                        "entity name").build())
+                                                        .build())
+                                        .build())
+                        .build();
+
+        Struct convertedStruct = entityTypeSpec.toStruct(entity);
+        assertThat(convertedStruct).isEqualTo(entityStruct);
+
+        TestEntity convertedEntity = entityTypeSpec.fromStruct(entityStruct);
+        assertThat(convertedEntity).isEqualTo(entity);
+    }
+
+    @Test
+    public void bindSpecField_throwsException() throws Exception {
+        TypeSpec<TestEntity> entityTypeSpec =
+                TypeSpecBuilder.newBuilder("TestEntity", TestEntity::newBuilder)
+                        .bindSpecField(
+                                "entity",
+                                TestEntity::getEntity,
+                                TestEntity.Builder::setEntity,
+                                TypeSpecBuilder.newBuilder("TestEntity", TestEntity::newBuilder)
+                                        .bindStringField("name", TestEntity::getName,
+                                                TestEntity.Builder::setName)
+                                        .build())
+                        .build();
+        Struct malformedStruct =
+                Struct.newBuilder()
+                        .putFields("@type", Value.newBuilder().setStringValue("TestEntity").build())
+                        .putFields("entity",
+                                Value.newBuilder().setStringValue("wrong value").build())
+                        .build();
+
+        assertThrows(StructConversionException.class,
+                () -> entityTypeSpec.fromStruct(malformedStruct));
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionCapabilityImplTest.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionCapabilityImplTest.java
new file mode 100644
index 0000000..4e2e5b8
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionCapabilityImplTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.spec;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+
+import androidx.appactions.interaction.capabilities.core.ExecutionResult;
+import androidx.appactions.interaction.capabilities.core.impl.ArgumentsWrapper;
+import androidx.appactions.interaction.capabilities.core.impl.CallbackInternal;
+import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters;
+import androidx.appactions.interaction.capabilities.core.properties.EntityProperty;
+import androidx.appactions.interaction.capabilities.core.testing.spec.Argument;
+import androidx.appactions.interaction.capabilities.core.testing.spec.Output;
+import androidx.appactions.interaction.capabilities.core.testing.spec.Property;
+import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment;
+import androidx.appactions.interaction.proto.FulfillmentResponse;
+import androidx.appactions.interaction.proto.FulfillmentResponse.StructuredOutput;
+import androidx.appactions.interaction.proto.FulfillmentResponse.StructuredOutput.OutputValue;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
+import java.util.Optional;
+
+@RunWith(JUnit4.class)
+public final class ActionCapabilityImplTest {
+
+    private static final ActionSpec<Property, Argument, Output> ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed("actions.intent.TEST")
+                    .setDescriptor(Property.class)
+                    .setArgument(Argument.class, Argument::newBuilder)
+                    .setOutput(Output.class)
+                    .bindRequiredEntityParameter(
+                            "requiredEntity",
+                            Property::requiredEntityField,
+                            Argument.Builder::setRequiredEntityField)
+                    .bindOptionalOutput(
+                            "optionalStringOutput", Output::optionalStringField,
+                            TypeConverters::toParamValue)
+                    .bindRepeatedOutput(
+                            "repeatedStringOutput", Output::repeatedStringField,
+                            TypeConverters::toParamValue)
+                    .build();
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+    @Captor
+    ArgumentCaptor<FulfillmentResponse> mCaptor;
+    @Mock
+    private CallbackInternal mCallbackInternal;
+
+    @Test
+    @SuppressWarnings("JdkImmutableCollections")
+    public void execute_convertExecutionResult() {
+        Property property =
+                Property.newBuilder().setRequiredEntityField(
+                        EntityProperty.newBuilder().build()).build();
+
+        ExecutionResult<Output> executionResult =
+                ExecutionResult.<Output>newBuilderWithOutput()
+                        .setOutput(
+                                Output.builder()
+                                        .setOptionalStringField("test2")
+                                        .setRepeatedStringField(List.of("test3", "test4"))
+                                        .build())
+                        .build();
+        ActionCapabilityImpl<Property, Argument, Output> capability =
+                new ActionCapabilityImpl<>(
+                        ACTION_SPEC,
+                        Optional.of("id"),
+                        property,
+                        (argument, callback) -> callback.onSuccess(executionResult));
+        StructuredOutput expectedExecutionOutput =
+                StructuredOutput.newBuilder()
+                        .addOutputValues(
+                                OutputValue.newBuilder()
+                                        .setName("optionalStringOutput")
+                                        .addValues(ParamValue.newBuilder().setStringValue(
+                                                "test2").build())
+                                        .build())
+                        .addOutputValues(
+                                OutputValue.newBuilder()
+                                        .setName("repeatedStringOutput")
+                                        .addValues(ParamValue.newBuilder().setStringValue(
+                                                "test3").build())
+                                        .addValues(ParamValue.newBuilder().setStringValue(
+                                                "test4").build())
+                                        .build())
+                        .build();
+
+        capability.execute(ArgumentsWrapper.create(Fulfillment.getDefaultInstance()),
+                mCallbackInternal);
+
+        verify(mCallbackInternal).onSuccess(mCaptor.capture());
+        assertThat(mCaptor.getValue().getExecutionOutput().getOutputValuesList())
+                .containsExactlyElementsIn(expectedExecutionOutput.getOutputValuesList());
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecTest.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecTest.java
new file mode 100644
index 0000000..f2cc5b8
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecTest.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.spec;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.ActionCapability;
+import androidx.appactions.interaction.capabilities.core.ActionExecutor;
+import androidx.appactions.interaction.capabilities.core.impl.ActionCapabilityInternal;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters;
+import androidx.appactions.interaction.capabilities.core.properties.Entity;
+import androidx.appactions.interaction.capabilities.core.properties.EntityProperty;
+import androidx.appactions.interaction.capabilities.core.properties.EnumProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
+import androidx.appactions.interaction.capabilities.core.testing.spec.Output;
+import androidx.appactions.interaction.capabilities.core.values.EntityValue;
+import androidx.appactions.interaction.proto.AppActionsContext.AppAction;
+import androidx.appactions.interaction.proto.AppActionsContext.IntentParameter;
+import androidx.appactions.interaction.proto.FulfillmentResponse.StructuredOutput;
+import androidx.appactions.interaction.proto.FulfillmentResponse.StructuredOutput.OutputValue;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import com.google.auto.value.AutoValue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.List;
+import java.util.Optional;
+
+@RunWith(JUnit4.class)
+public final class ActionSpecTest {
+
+    private static final ActionSpec<Property, Argument, Output> ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed("actions.intent.TEST")
+                    .setDescriptor(Property.class)
+                    .setArgument(Argument.class, Argument::newBuilder)
+                    .setOutput(Output.class)
+                    .bindRequiredEntityParameter(
+                            "requiredEntity",
+                            Property::requiredEntityField,
+                            Argument.Builder::setRequiredEntityField)
+                    .bindOptionalEntityParameter(
+                            "optionalEntity",
+                            Property::optionalEntityField,
+                            Argument.Builder::setOptionalEntityField)
+                    .bindOptionalEnumParameter(
+                            "optionalEnum",
+                            TestEnum.class,
+                            Property::optionalEnumField,
+                            Argument.Builder::setOptionalEnumField)
+                    .bindRepeatedEntityParameter(
+                            "repeatedEntity",
+                            Property::repeatedEntityField,
+                            Argument.Builder::setRepeatedEntityField)
+                    .bindRequiredStringParameter(
+                            "requiredString",
+                            Property::requiredStringField,
+                            Argument.Builder::setRequiredStringField)
+                    .bindOptionalStringParameter(
+                            "optionalString",
+                            Property::optionalStringField,
+                            Argument.Builder::setOptionalStringField)
+                    .bindRepeatedStringParameter(
+                            "repeatedString",
+                            Property::repeatedStringField,
+                            Argument.Builder::setRepeatedStringField)
+                    .bindOptionalOutput(
+                            "optionalStringOutput", Output::optionalStringField,
+                            TypeConverters::toParamValue)
+                    .bindRepeatedOutput(
+                            "repeatedStringOutput", Output::repeatedStringField,
+                            TypeConverters::toParamValue)
+                    .build();
+
+    private static ActionCapability createCapability(
+            String id, Property property, ActionExecutor<Argument, Output> executor) {
+        return new ActionCapabilityImpl<>(ACTION_SPEC, Optional.of(id), property, executor);
+    }
+
+    @Test
+    public void getAppAction_onlyRequiredProperty() {
+        Property property =
+                Property.create(
+                        EntityProperty.newBuilder()
+                                .addPossibleEntity(
+                                        Entity.newBuilder()
+                                                .setId("contact_2")
+                                                .setName("Donald")
+                                                .setAlternateNames("Duck")
+                                                .build())
+                                .setValueMatchRequired(true)
+                                .build(),
+                        StringProperty.EMPTY);
+
+        ActionCapabilityInternal capability =
+                (ActionCapabilityInternal) createCapability("id", property, (arg, callback) -> {
+                });
+
+        assertThat(capability.getAppAction())
+                .isEqualTo(
+                        AppAction.newBuilder()
+                                .setIdentifier("id")
+                                .setName("actions.intent.TEST")
+                                .addParams(
+                                        IntentParameter.newBuilder()
+                                                .setName("requiredEntity")
+                                                .addPossibleEntities(
+                                                        androidx.appactions.interaction.proto.Entity.newBuilder()
+                                                                .setIdentifier("contact_2")
+                                                                .setName("Donald")
+                                                                .addAlternateNames("Duck")
+                                                                .build())
+                                                .setIsRequired(false)
+                                                .setEntityMatchRequired(true))
+                                .addParams(IntentParameter.newBuilder().setName("requiredString"))
+                                .build());
+    }
+
+    @Test
+    public void getAppAction_allProperties() {
+        Property property =
+                Property.create(
+                        EntityProperty.newBuilder()
+                                .addPossibleEntity(
+                                        Entity.newBuilder()
+                                                .setId("contact_2")
+                                                .setName("Donald")
+                                                .setAlternateNames("Duck")
+                                                .build())
+                                .build(),
+                        Optional.of(
+                                EntityProperty.newBuilder()
+                                        .addPossibleEntity(
+                                                Entity.newBuilder()
+                                                        .setId("entity1")
+                                                        .setName("optional possible entity")
+                                                        .build())
+                                        .setIsRequired(true)
+                                        .build()),
+                        Optional.of(
+                                EnumProperty.newBuilder(TestEnum.class)
+                                        .addSupportedEnumValues(TestEnum.VALUE_1)
+                                        .setIsRequired(true)
+                                        .build()),
+                        Optional.of(
+                                EntityProperty.newBuilder()
+                                        .addPossibleEntity(
+                                                Entity.newBuilder().setId("entity1").setName(
+                                                        "repeated entity1").build())
+                                        .addPossibleEntity(
+                                                Entity.newBuilder().setId("entity2").setName(
+                                                        "repeated entity2").build())
+                                        .setIsRequired(true)
+                                        .build()),
+                        StringProperty.EMPTY,
+                        Optional.of(
+                                StringProperty.newBuilder()
+                                        .addPossibleValue("value1")
+                                        .setValueMatchRequired(true)
+                                        .setIsRequired(true)
+                                        .build()),
+                        Optional.of(StringProperty.PROHIBITED));
+
+        ActionCapabilityInternal capability =
+                (ActionCapabilityInternal) createCapability("id", property, (arg, callback) -> {
+                });
+
+        assertThat(capability.getAppAction())
+                .isEqualTo(
+                        AppAction.newBuilder()
+                                .setIdentifier("id")
+                                .setName("actions.intent.TEST")
+                                .addParams(
+                                        IntentParameter.newBuilder()
+                                                .setName("requiredEntity")
+                                                .addPossibleEntities(
+                                                        androidx.appactions.interaction.proto.Entity.newBuilder()
+                                                                .setIdentifier("contact_2")
+                                                                .setName("Donald")
+                                                                .addAlternateNames("Duck")
+                                                                .build())
+                                                .setIsRequired(false)
+                                                .setEntityMatchRequired(false))
+                                .addParams(
+                                        IntentParameter.newBuilder()
+                                                .setName("optionalEntity")
+                                                .addPossibleEntities(
+                                                        androidx.appactions.interaction.proto.Entity.newBuilder()
+                                                                .setIdentifier("entity1")
+                                                                .setName("optional possible entity")
+                                                                .build())
+                                                .setIsRequired(true)
+                                                .setEntityMatchRequired(false))
+                                .addParams(
+                                        IntentParameter.newBuilder()
+                                                .setName("optionalEnum")
+                                                .addPossibleEntities(
+                                                        androidx.appactions.interaction.proto.Entity.newBuilder()
+                                                                .setIdentifier(
+                                                                        TestEnum.VALUE_1.toString())
+                                                                .build())
+                                                .setIsRequired(true)
+                                                .setEntityMatchRequired(true))
+                                .addParams(
+                                        IntentParameter.newBuilder()
+                                                .setName("repeatedEntity")
+                                                .addPossibleEntities(
+                                                        androidx.appactions.interaction.proto.Entity.newBuilder()
+                                                                .setIdentifier("entity1")
+                                                                .setName("repeated entity1")
+                                                                .build())
+                                                .addPossibleEntities(
+                                                        androidx.appactions.interaction.proto.Entity.newBuilder()
+                                                                .setIdentifier("entity2")
+                                                                .setName("repeated entity2")
+                                                                .build())
+                                                .setIsRequired(true)
+                                                .setEntityMatchRequired(false))
+                                .addParams(IntentParameter.newBuilder().setName("requiredString"))
+                                .addParams(
+                                        IntentParameter.newBuilder()
+                                                .setName("optionalString")
+                                                .addPossibleEntities(
+                                                        androidx.appactions.interaction.proto.Entity.newBuilder()
+                                                                .setIdentifier("value1")
+                                                                .setName("value1")
+                                                                .build())
+                                                .setIsRequired(true)
+                                                .setEntityMatchRequired(true))
+                                .addParams(
+                                        IntentParameter.newBuilder().setName(
+                                                "repeatedString").setIsProhibited(true))
+                                .build());
+    }
+
+    @Test
+    @SuppressWarnings("JdkImmutableCollections")
+    public void convertOutputToProto_string() {
+        Output output =
+                Output.builder()
+                        .setOptionalStringField("test2")
+                        .setRepeatedStringField(List.of("test3", "test4"))
+                        .build();
+
+        StructuredOutput expectedExecutionOutput =
+                StructuredOutput.newBuilder()
+                        .addOutputValues(
+                                OutputValue.newBuilder()
+                                        .setName("optionalStringOutput")
+                                        .addValues(ParamValue.newBuilder().setStringValue(
+                                                "test2").build())
+                                        .build())
+                        .addOutputValues(
+                                OutputValue.newBuilder()
+                                        .setName("repeatedStringOutput")
+                                        .addValues(ParamValue.newBuilder().setStringValue(
+                                                "test3").build())
+                                        .addValues(ParamValue.newBuilder().setStringValue(
+                                                "test4").build())
+                                        .build())
+                        .build();
+
+        StructuredOutput executionOutput = ACTION_SPEC.convertOutputToProto(output);
+
+        assertThat(executionOutput.getOutputValuesList())
+                .containsExactlyElementsIn(expectedExecutionOutput.getOutputValuesList());
+    }
+
+    enum TestEnum {
+        VALUE_1,
+        VALUE_2,
+    }
+
+    @AutoValue
+    abstract static class Argument {
+
+        static Builder newBuilder() {
+            return new AutoValue_ActionSpecTest_Argument.Builder();
+        }
+
+        abstract EntityValue requiredEntityField();
+
+        abstract EntityValue optionalEntityField();
+
+        abstract TestEnum optionalEnumField();
+
+        abstract List<EntityValue> repeatedEntityField();
+
+        abstract String requiredStringField();
+
+        abstract String optionalStringField();
+
+        abstract List<String> repeatedStringField();
+
+        @AutoValue.Builder
+        abstract static class Builder implements BuilderOf<Argument> {
+
+            abstract Builder setRequiredEntityField(EntityValue value);
+
+            abstract Builder setOptionalEntityField(EntityValue value);
+
+            abstract Builder setOptionalEnumField(TestEnum value);
+
+            abstract Builder setRepeatedEntityField(List<EntityValue> repeated);
+
+            abstract Builder setRequiredStringField(String value);
+
+            abstract Builder setOptionalStringField(String value);
+
+            abstract Builder setRepeatedStringField(List<String> repeated);
+
+            @NonNull
+            @Override
+            public abstract Argument build();
+        }
+    }
+
+    @AutoValue
+    abstract static class Property {
+
+        static Property create(
+                EntityProperty requiredEntityField,
+                Optional<EntityProperty> optionalEntityField,
+                Optional<EnumProperty<TestEnum>> optionalEnumField,
+                Optional<EntityProperty> repeatedEntityField,
+                StringProperty requiredStringField,
+                Optional<StringProperty> optionalStringField,
+                Optional<StringProperty> repeatedStringField) {
+            return new AutoValue_ActionSpecTest_Property(
+                    requiredEntityField,
+                    optionalEntityField,
+                    optionalEnumField,
+                    repeatedEntityField,
+                    requiredStringField,
+                    optionalStringField,
+                    repeatedStringField);
+        }
+
+        static Property create(EntityProperty requiredEntityField,
+                StringProperty requiredStringField) {
+            return create(
+                    requiredEntityField,
+                    Optional.empty(),
+                    Optional.empty(),
+                    Optional.empty(),
+                    requiredStringField,
+                    Optional.empty(),
+                    Optional.empty());
+        }
+
+        abstract EntityProperty requiredEntityField();
+
+        abstract Optional<EntityProperty> optionalEntityField();
+
+        abstract Optional<EnumProperty<TestEnum>> optionalEnumField();
+
+        abstract Optional<EntityProperty> repeatedEntityField();
+
+        abstract StringProperty requiredStringField();
+
+        abstract Optional<StringProperty> optionalStringField();
+
+        abstract Optional<StringProperty> repeatedStringField();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/EntitySearchResultTest.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/EntitySearchResultTest.java
new file mode 100644
index 0000000..3d0bf1c
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/EntitySearchResultTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class EntitySearchResultTest {
+
+    @Test
+    public void emptyList() {
+        assertThat(EntitySearchResult.empty().possibleValues()).isEmpty();
+    }
+
+    @Test
+    public void test() {
+        EntitySearchResult.Builder<String> gr = EntitySearchResult.newBuilder();
+        gr.addPossibleValue("foo");
+        gr.addPossibleValue("bar");
+
+        assertThat(gr.build().possibleValues()).containsExactly("foo", "bar");
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.java
new file mode 100644
index 0000000..89df7ba
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.java
@@ -0,0 +1,1770 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+import static androidx.appactions.interaction.capabilities.core.testing.ArgumentUtils.buildRequestArgs;
+import static androidx.appactions.interaction.capabilities.core.testing.ArgumentUtils.buildSearchActionParamValue;
+import static androidx.appactions.interaction.capabilities.core.testing.TestingUtils.CB_TIMEOUT;
+import static androidx.appactions.interaction.capabilities.core.testing.TestingUtils.buildActionCallback;
+import static androidx.appactions.interaction.capabilities.core.testing.TestingUtils.buildActionCallbackWithFulfillmentResponse;
+import static androidx.appactions.interaction.capabilities.core.testing.TestingUtils.buildErrorActionCallback;
+import static androidx.appactions.interaction.capabilities.core.testing.TestingUtils.buildTouchEventCallback;
+import static androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.Type.CANCEL;
+import static androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.Type.CONFIRM;
+import static androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.Type.SYNC;
+import static androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.Type.TERMINATE;
+import static androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.Type.UNKNOWN_TYPE;
+import static androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.Type.UNRECOGNIZED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.AbstractCapabilityBuilder;
+import androidx.appactions.interaction.capabilities.core.AbstractTaskHandlerBuilder;
+import androidx.appactions.interaction.capabilities.core.ConfirmationOutput;
+import androidx.appactions.interaction.capabilities.core.ExecutionResult;
+import androidx.appactions.interaction.capabilities.core.impl.ActionCapabilityInternal;
+import androidx.appactions.interaction.capabilities.core.impl.ErrorStatusInternal;
+import androidx.appactions.interaction.capabilities.core.impl.TouchEventCallback;
+import androidx.appactions.interaction.capabilities.core.impl.concurrent.Futures;
+import androidx.appactions.interaction.capabilities.core.impl.converters.DisambigEntityConverter;
+import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters;
+import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec;
+import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder;
+import androidx.appactions.interaction.capabilities.core.properties.EntityProperty;
+import androidx.appactions.interaction.capabilities.core.properties.EnumProperty;
+import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
+import androidx.appactions.interaction.capabilities.core.task.AppEntityResolver;
+import androidx.appactions.interaction.capabilities.core.task.EntitySearchResult;
+import androidx.appactions.interaction.capabilities.core.task.InvalidTaskException;
+import androidx.appactions.interaction.capabilities.core.task.OnDialogFinishListener;
+import androidx.appactions.interaction.capabilities.core.task.OnInitListener;
+import androidx.appactions.interaction.capabilities.core.task.OnReadyToConfirmListener;
+import androidx.appactions.interaction.capabilities.core.task.ValidationResult;
+import androidx.appactions.interaction.capabilities.core.task.ValueListener;
+import androidx.appactions.interaction.capabilities.core.testing.TestingUtils;
+import androidx.appactions.interaction.capabilities.core.testing.TestingUtils.ReusableTouchEventCallback;
+import androidx.appactions.interaction.capabilities.core.testing.TestingUtils.TouchEventResult;
+import androidx.appactions.interaction.capabilities.core.testing.spec.Argument;
+import androidx.appactions.interaction.capabilities.core.testing.spec.CapabilityStructFill;
+import androidx.appactions.interaction.capabilities.core.testing.spec.CapabilityTwoEntityValues;
+import androidx.appactions.interaction.capabilities.core.testing.spec.CapabilityTwoStrings;
+import androidx.appactions.interaction.capabilities.core.testing.spec.Confirmation;
+import androidx.appactions.interaction.capabilities.core.testing.spec.Output;
+import androidx.appactions.interaction.capabilities.core.testing.spec.Property;
+import androidx.appactions.interaction.capabilities.core.testing.spec.SettableFutureWrapper;
+import androidx.appactions.interaction.capabilities.core.testing.spec.TestEnum;
+import androidx.appactions.interaction.capabilities.core.values.EntityValue;
+import androidx.appactions.interaction.capabilities.core.values.ListItem;
+import androidx.appactions.interaction.capabilities.core.values.SearchAction;
+import androidx.appactions.interaction.proto.AppActionsContext.AppAction;
+import androidx.appactions.interaction.proto.AppActionsContext.IntentParameter;
+import androidx.appactions.interaction.proto.CurrentValue;
+import androidx.appactions.interaction.proto.DisambiguationData;
+import androidx.appactions.interaction.proto.Entity;
+import androidx.appactions.interaction.proto.FulfillmentResponse;
+import androidx.appactions.interaction.proto.FulfillmentResponse.StructuredOutput;
+import androidx.appactions.interaction.proto.FulfillmentResponse.StructuredOutput.OutputValue;
+import androidx.appactions.interaction.proto.ParamValue;
+import androidx.appactions.interaction.proto.TaskInfo;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+
+@RunWith(JUnit4.class)
+public final class TaskCapabilityImplTest {
+
+    private static final Optional<DisambigEntityConverter<EntityValue>> DISAMBIG_ENTITY_CONVERTER =
+            Optional.of(TypeConverters::toEntity);
+    private static final GenericResolverInternal<EntityValue> AUTO_ACCEPT_ENTITY_VALUE =
+            GenericResolverInternal.fromAppEntityResolver(
+                    new AppEntityResolver<EntityValue>() {
+                        @Override
+                        public ListenableFuture<EntitySearchResult<EntityValue>> lookupAndRender(
+                                SearchAction<EntityValue> searchAction) {
+                            EntitySearchResult.Builder<EntityValue> result =
+                                    EntitySearchResult.newBuilder();
+                            return Futures.immediateFuture(
+                                    result.addPossibleValue(EntityValue.ofId("valid1")).build());
+                        }
+
+                        @NonNull
+                        @Override
+                        public ListenableFuture<ValidationResult> onReceived(EntityValue newValue) {
+                            return Futures.immediateFuture(ValidationResult.newAccepted());
+                        }
+                    });
+    private static final GenericResolverInternal<EntityValue> AUTO_REJECT_ENTITY_VALUE =
+            GenericResolverInternal.fromAppEntityResolver(
+                    new AppEntityResolver<EntityValue>() {
+                        @Override
+                        public ListenableFuture<EntitySearchResult<EntityValue>> lookupAndRender(
+                                SearchAction<EntityValue> searchAction) {
+                            EntitySearchResult.Builder<EntityValue> result =
+                                    EntitySearchResult.newBuilder();
+                            return Futures.immediateFuture(
+                                    result.addPossibleValue(EntityValue.ofId("valid1")).build());
+                        }
+
+                        @NonNull
+                        @Override
+                        public ListenableFuture<ValidationResult> onReceived(EntityValue newValue) {
+                            return Futures.immediateFuture(ValidationResult.newRejected());
+                        }
+                    });
+    private static final String CAPABILITY_NAME = "actions.intent.TEST";
+    private static final ActionSpec<Property, Argument, Output> ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                    .setDescriptor(Property.class)
+                    .setArgument(Argument.class, Argument::newBuilder)
+                    .setOutput(Output.class)
+                    .bindRequiredEntityParameter(
+                            "required",
+                            Property::requiredEntityField,
+                            Argument.Builder::setRequiredEntityField)
+                    .bindOptionalStringParameter(
+                            "optional",
+                            Property::optionalStringField,
+                            Argument.Builder::setOptionalStringField)
+                    .bindOptionalEnumParameter(
+                            "optionalEnum",
+                            TestEnum.class,
+                            Property::enumField,
+                            Argument.Builder::setEnumField)
+                    .bindRepeatedStringParameter(
+                            "repeated",
+                            Property::repeatedStringField,
+                            Argument.Builder::setRepeatedStringField)
+                    .build();
+    private static final Property SINGLE_REQUIRED_FIELD_PROPERTY =
+            Property.newBuilder()
+                    .setRequiredEntityField(EntityProperty.newBuilder().setIsRequired(true).build())
+                    .build();
+    private static final Optional<OnReadyToConfirmListener<Argument, Confirmation>>
+            EMPTY_CONFIRM_LISTENER = Optional.empty();
+    private static final OnDialogFinishListener<Argument, Output> EMPTY_FINISH_LISTENER =
+            (finalArgs) ->
+                    Futures.immediateFuture(ExecutionResult.<Output>getDefaultInstanceWithOutput());
+
+    private static boolean groundingPredicate(ParamValue paramValue) {
+        return !paramValue.hasIdentifier();
+    }
+
+    private static List<CurrentValue> getCurrentValues(String argName, AppAction appAction) {
+        return appAction.getParamsList().stream()
+                .filter(intentParam -> intentParam.getName().equals(argName))
+                .findFirst()
+                .orElse(IntentParameter.getDefaultInstance())
+                .getCurrentValueList();
+    }
+
+    private static <TaskUpdaterT extends AbstractTaskUpdater>
+            TaskCapabilityImpl<Property, Argument, Output, Confirmation, TaskUpdaterT>
+                    createTaskCapability(
+                            Property property,
+                            TaskParamRegistry paramRegistry,
+                            Supplier<TaskUpdaterT> taskUpdaterSupplier,
+                            Optional<OnInitListener<TaskUpdaterT>> onInitListener,
+                            Optional<OnReadyToConfirmListener<Argument, Confirmation>>
+                                    optionalOnReadyToConfirmListener,
+                            OnDialogFinishListener<Argument, Output> onTaskFinishListener) {
+
+        Optional<OnReadyToConfirmListenerInternal<Confirmation>> onReadyToConfirmListenerInternal =
+                optionalOnReadyToConfirmListener.isPresent()
+                        ? Optional.of(
+                                (args) ->
+                                        optionalOnReadyToConfirmListener
+                                                .get()
+                                                .onReadyToConfirm(ACTION_SPEC.buildArgument(args)))
+                        : Optional.empty();
+
+        TaskCapabilityImpl<Property, Argument, Output, Confirmation, TaskUpdaterT> taskCapability =
+                new TaskCapabilityImpl<>(
+                        "id",
+                        ACTION_SPEC,
+                        property,
+                        paramRegistry,
+                        onInitListener,
+                        onReadyToConfirmListenerInternal,
+                        onTaskFinishListener,
+                        /* confirmationOutputBindings= */ new HashMap<>(),
+                        /* executionOutputBindings= */ new HashMap<>(),
+                        Executors.newFixedThreadPool(5));
+        taskCapability.setTaskUpdaterSupplier(taskUpdaterSupplier);
+        return taskCapability;
+    }
+
+    @Test
+    public void getAppAction_executeNeverCalled_taskIsUninitialized() {
+        ActionCapabilityInternal capability =
+                createTaskCapability(
+                        SINGLE_REQUIRED_FIELD_PROPERTY,
+                        TaskParamRegistry.builder().build(),
+                        RequiredTaskUpdater::new,
+                        Optional.empty(),
+                        EMPTY_CONFIRM_LISTENER,
+                        EMPTY_FINISH_LISTENER);
+
+        assertThat(capability.getAppAction())
+                .isEqualTo(
+                        AppAction.newBuilder()
+                                .setName("actions.intent.TEST")
+                                .setIdentifier("id")
+                                .addParams(
+                                        IntentParameter.newBuilder()
+                                                .setName("required")
+                                                .setIsRequired(true))
+                                .setTaskInfo(
+                                        TaskInfo.newBuilder().setSupportsPartialFulfillment(true))
+                                .build());
+    }
+
+    @Test
+    public void onInitInvoked_invokedOnce() throws Exception {
+        AtomicInteger onSuccessInvocationCount = new AtomicInteger(0);
+        ActionCapabilityInternal capability =
+                createTaskCapability(
+                        SINGLE_REQUIRED_FIELD_PROPERTY,
+                        TaskParamRegistry.builder().build(),
+                        RequiredTaskUpdater::new,
+                        Optional.of(
+                                (unused) -> {
+                                    onSuccessInvocationCount.incrementAndGet();
+                                    return Futures.immediateVoidFuture();
+                                }),
+                        EMPTY_CONFIRM_LISTENER,
+                        EMPTY_FINISH_LISTENER);
+
+        // TURN 1.
+        SettableFutureWrapper<Boolean> onSuccessInvoked = new SettableFutureWrapper<>();
+
+        capability.execute(
+                buildRequestArgs(SYNC, "unknownArgName", "foo"),
+                buildActionCallback(onSuccessInvoked));
+
+        assertThat(onSuccessInvoked.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+        assertThat(onSuccessInvocationCount.get()).isEqualTo(1);
+
+        // TURN 2.
+        SettableFutureWrapper<Boolean> onSuccessInvoked2 = new SettableFutureWrapper<>();
+
+        capability.execute(
+                buildRequestArgs(
+                        SYNC,
+                        "required",
+                        ParamValue.newBuilder().setIdentifier("foo").setStringValue("foo").build()),
+                buildActionCallback(onSuccessInvoked2));
+
+        assertThat(onSuccessInvoked2.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+        assertThat(onSuccessInvocationCount.get()).isEqualTo(1);
+    }
+
+    @Test
+    public void fulfillmentType_terminate_taskStateCleared() throws Exception {
+        ActionCapabilityInternal capability =
+                createTaskCapability(
+                        SINGLE_REQUIRED_FIELD_PROPERTY,
+                        TaskParamRegistry.builder().build(),
+                        RequiredTaskUpdater::new,
+                        Optional.empty(),
+                        EMPTY_CONFIRM_LISTENER,
+                        EMPTY_FINISH_LISTENER);
+
+        // TURN 1.
+        SettableFutureWrapper<Boolean> onSuccessInvoked = new SettableFutureWrapper<>();
+
+        capability.execute(
+                buildRequestArgs(
+                        SYNC,
+                        "required",
+                        ParamValue.newBuilder().setIdentifier("foo").setStringValue("foo").build()),
+                buildActionCallback(onSuccessInvoked));
+
+        assertThat(onSuccessInvoked.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+
+        // TURN 2.
+        SettableFutureWrapper<Boolean> onSuccessInvoked2 = new SettableFutureWrapper<>();
+
+        capability.execute(buildRequestArgs(TERMINATE), buildActionCallback(onSuccessInvoked2));
+
+        assertThat(onSuccessInvoked2.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+        assertThat(capability.getAppAction())
+                .isEqualTo(
+                        AppAction.newBuilder()
+                                .setName("actions.intent.TEST")
+                                .setIdentifier("id")
+                                .addParams(
+                                        IntentParameter.newBuilder()
+                                                .setName("required")
+                                                .setIsRequired(true))
+                                .setTaskInfo(
+                                        TaskInfo.newBuilder().setSupportsPartialFulfillment(true))
+                                .build());
+    }
+
+    @Test
+    public void fulfillmentType_cancel_taskStateCleared() throws Exception {
+        SettableFutureWrapper<RequiredTaskUpdater> taskUpdaterCb = new SettableFutureWrapper<>();
+        ActionCapabilityInternal capability =
+                createTaskCapability(
+                        SINGLE_REQUIRED_FIELD_PROPERTY,
+                        TaskParamRegistry.builder().build(),
+                        RequiredTaskUpdater::new,
+                        TestingUtils.buildOnInitListener(taskUpdaterCb),
+                        EMPTY_CONFIRM_LISTENER,
+                        EMPTY_FINISH_LISTENER);
+
+        // TURN 1.
+        SettableFutureWrapper<Boolean> onSuccessInvoked = new SettableFutureWrapper<>();
+
+        capability.execute(buildRequestArgs(SYNC), buildActionCallback(onSuccessInvoked));
+
+        assertThat(onSuccessInvoked.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+        assertThat(taskUpdaterCb.getFuture().get(CB_TIMEOUT, MILLISECONDS).isDestroyed()).isFalse();
+
+        // TURN 2.
+        SettableFutureWrapper<Boolean> onSuccessInvoked2 = new SettableFutureWrapper<>();
+
+        capability.execute(buildRequestArgs(CANCEL), buildActionCallback(onSuccessInvoked2));
+
+        assertThat(onSuccessInvoked2.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+        assertThat(capability.getAppAction())
+                .isEqualTo(
+                        AppAction.newBuilder()
+                                .setName("actions.intent.TEST")
+                                .setIdentifier("id")
+                                .addParams(
+                                        IntentParameter.newBuilder()
+                                                .setName("required")
+                                                .setIsRequired(true))
+                                .setTaskInfo(
+                                        TaskInfo.newBuilder().setSupportsPartialFulfillment(true))
+                                .build());
+        assertThat(taskUpdaterCb.getFuture().get(CB_TIMEOUT, MILLISECONDS).isDestroyed()).isTrue();
+    }
+
+    @Test
+    public void fulfillmentType_unknown_errorReported() throws Exception {
+        ActionCapabilityInternal capability =
+                createTaskCapability(
+                        SINGLE_REQUIRED_FIELD_PROPERTY,
+                        TaskParamRegistry.builder().build(),
+                        RequiredTaskUpdater::new,
+                        Optional.empty(),
+                        EMPTY_CONFIRM_LISTENER,
+                        EMPTY_FINISH_LISTENER);
+
+        // TURN 1 (UNKNOWN).
+        SettableFutureWrapper<ErrorStatusInternal> errorCb = new SettableFutureWrapper<>();
+
+        capability.execute(buildRequestArgs(UNKNOWN_TYPE), buildErrorActionCallback(errorCb));
+
+        assertThat(errorCb.getFuture().get(CB_TIMEOUT, MILLISECONDS))
+                .isEqualTo(ErrorStatusInternal.INVALID_REQUEST_TYPE);
+        assertThat(capability.getAppAction())
+                .isEqualTo(
+                        AppAction.newBuilder()
+                                .setName("actions.intent.TEST")
+                                .setIdentifier("id")
+                                .addParams(
+                                        IntentParameter.newBuilder()
+                                                .setName("required")
+                                                .setIsRequired(true))
+                                .setTaskInfo(
+                                        TaskInfo.newBuilder().setSupportsPartialFulfillment(true))
+                                .build());
+
+        // TURN 2 (UNRECOGNIZED).
+        SettableFutureWrapper<ErrorStatusInternal> errorCb2 = new SettableFutureWrapper<>();
+
+        capability.execute(buildRequestArgs(UNRECOGNIZED), buildErrorActionCallback(errorCb2));
+
+        assertThat(errorCb2.getFuture().get(CB_TIMEOUT, MILLISECONDS))
+                .isEqualTo(ErrorStatusInternal.INVALID_REQUEST_TYPE);
+        assertThat(capability.getAppAction())
+                .isEqualTo(
+                        AppAction.newBuilder()
+                                .setName("actions.intent.TEST")
+                                .setIdentifier("id")
+                                .addParams(
+                                        IntentParameter.newBuilder()
+                                                .setName("required")
+                                                .setIsRequired(true))
+                                .setTaskInfo(
+                                        TaskInfo.newBuilder().setSupportsPartialFulfillment(true))
+                                .build());
+    }
+
+    @Test
+    public void slotFilling_optionalButRejectedParam_onFinishNotInvoked() throws Exception {
+        AtomicInteger onFinishInvocationCount = new AtomicInteger(0);
+        CapabilityTwoEntityValues.Property property =
+                CapabilityTwoEntityValues.Property.newBuilder()
+                        .setSlotA(EntityProperty.newBuilder().setIsRequired(true).build())
+                        .setSlotB(EntityProperty.newBuilder().setIsRequired(false).build())
+                        .build();
+        TaskParamRegistry paramRegistry =
+                TaskParamRegistry.builder()
+                        .addTaskParameter(
+                                "slotA",
+                                TaskCapabilityImplTest::groundingPredicate,
+                                AUTO_ACCEPT_ENTITY_VALUE,
+                                DISAMBIG_ENTITY_CONVERTER,
+                                Optional.of(
+                                        unused -> SearchAction.<EntityValue>newBuilder().build()),
+                                TypeConverters::toEntityValue)
+                        .addTaskParameter(
+                                "slotB",
+                                TaskCapabilityImplTest::groundingPredicate,
+                                AUTO_REJECT_ENTITY_VALUE,
+                                DISAMBIG_ENTITY_CONVERTER,
+                                Optional.of(
+                                        unused -> SearchAction.<EntityValue>newBuilder().build()),
+                                TypeConverters::toEntityValue)
+                        .build();
+        TaskCapabilityImpl<
+                        CapabilityTwoEntityValues.Property,
+                        CapabilityTwoEntityValues.Argument,
+                        Void,
+                        Void,
+                        EmptyTaskUpdater>
+                capability =
+                        new TaskCapabilityImpl<>(
+                                "fakeId",
+                                CapabilityTwoEntityValues.ACTION_SPEC,
+                                property,
+                                paramRegistry,
+                                /* onInitListener= */ Optional.empty(),
+                                /* onReadyToConfirmListener= */ Optional.empty(),
+                                (finalArgs) -> {
+                                    onFinishInvocationCount.incrementAndGet();
+                                    return Futures.immediateFuture(
+                                            ExecutionResult.getDefaultInstance());
+                                },
+                                /* confirmationOutputBindings= */ Collections.emptyMap(),
+                                /* executionOutputBindings= */ Collections.emptyMap(),
+                                Runnable::run);
+        capability.setTaskUpdaterSupplier(EmptyTaskUpdater::new);
+
+        // TURN 1.
+        SettableFutureWrapper<Boolean> onSuccessInvoked = new SettableFutureWrapper<>();
+        capability.execute(
+                buildRequestArgs(
+                        SYNC,
+                        "slotA",
+                        ParamValue.newBuilder().setIdentifier("foo").setStringValue("foo").build(),
+                        "slotB",
+                        ParamValue.newBuilder().setIdentifier("bar").setStringValue("bar").build()),
+                buildActionCallback(onSuccessInvoked));
+
+        assertThat(onSuccessInvoked.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+        assertThat(onFinishInvocationCount.get()).isEqualTo(0);
+        assertThat(getCurrentValues("slotA", capability.getAppAction()))
+                .containsExactly(
+                        CurrentValue.newBuilder()
+                                .setValue(
+                                        ParamValue.newBuilder()
+                                                .setIdentifier("foo")
+                                                .setStringValue("foo"))
+                                .setStatus(CurrentValue.Status.ACCEPTED)
+                                .build());
+        assertThat(getCurrentValues("slotB", capability.getAppAction()))
+                .containsExactly(
+                        CurrentValue.newBuilder()
+                                .setValue(
+                                        ParamValue.newBuilder()
+                                                .setIdentifier("bar")
+                                                .setStringValue("bar"))
+                                .setStatus(CurrentValue.Status.REJECTED)
+                                .build());
+    }
+
+    @Test
+    public void slotFilling_assistantRemovedParam_clearInSdkState() throws Exception {
+        Property property =
+                Property.newBuilder()
+                        .setRequiredEntityField(
+                                EntityProperty.newBuilder().setIsRequired(true).build())
+                        .setEnumField(
+                                EnumProperty.newBuilder(TestEnum.class)
+                                        .addSupportedEnumValues(TestEnum.VALUE_1, TestEnum.VALUE_2)
+                                        .setIsRequired(true)
+                                        .build())
+                        .build();
+        ActionCapabilityInternal capability =
+                createTaskCapability(
+                        property,
+                        TaskParamRegistry.builder().build(),
+                        RequiredTaskUpdater::new,
+                        Optional.empty(),
+                        EMPTY_CONFIRM_LISTENER,
+                        EMPTY_FINISH_LISTENER);
+
+        // TURN 1.
+        SettableFutureWrapper<Boolean> onSuccessInvoked = new SettableFutureWrapper<>();
+
+        capability.execute(
+                buildRequestArgs(
+                        SYNC,
+                        "required",
+                        ParamValue.newBuilder().setIdentifier("foo").setStringValue("foo").build()),
+                buildActionCallback(onSuccessInvoked));
+
+        assertThat(onSuccessInvoked.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+        assertThat(getCurrentValues("required", capability.getAppAction()))
+                .containsExactly(
+                        CurrentValue.newBuilder()
+                                .setValue(
+                                        ParamValue.newBuilder()
+                                                .setIdentifier("foo")
+                                                .setStringValue("foo"))
+                                .setStatus(CurrentValue.Status.ACCEPTED)
+                                .build());
+        assertThat(getCurrentValues("optionalEnum", capability.getAppAction())).isEmpty();
+
+        // TURN 2.
+        SettableFutureWrapper<Boolean> onSuccessInvoked2 = new SettableFutureWrapper<>();
+
+        capability.execute(
+                buildRequestArgs(SYNC, "optionalEnum", TestEnum.VALUE_2),
+                buildActionCallback(onSuccessInvoked2));
+
+        assertThat(onSuccessInvoked2.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+        assertThat(getCurrentValues("required", capability.getAppAction())).isEmpty();
+        assertThat(getCurrentValues("optionalEnum", capability.getAppAction()))
+                .containsExactly(
+                        CurrentValue.newBuilder()
+                                .setValue(ParamValue.newBuilder().setIdentifier("VALUE_2"))
+                                .setStatus(CurrentValue.Status.ACCEPTED)
+                                .build());
+    }
+
+    @Test
+    public void disambig_singleParam_disambigEntitiesInContext() throws Exception {
+        TaskParamRegistry.Builder paramRegistry = TaskParamRegistry.builder();
+        paramRegistry.addTaskParameter(
+                "required",
+                TaskCapabilityImplTest::groundingPredicate,
+                GenericResolverInternal.fromAppEntityResolver(
+                        new AppEntityResolver<EntityValue>() {
+                            @Override
+                            public ListenableFuture<EntitySearchResult<EntityValue>>
+                                    lookupAndRender(SearchAction<EntityValue> searchAction) {
+                                EntitySearchResult.Builder<EntityValue> result =
+                                        EntitySearchResult.newBuilder();
+                                return Futures.immediateFuture(
+                                        result.addPossibleValue(EntityValue.ofId("valid1"))
+                                                .addPossibleValue(EntityValue.ofId("valid2"))
+                                                .build());
+                            }
+
+                            @NonNull
+                            @Override
+                            public ListenableFuture<ValidationResult> onReceived(
+                                    EntityValue newValue) {
+                                return Futures.immediateFuture(ValidationResult.newAccepted());
+                            }
+                        }),
+                DISAMBIG_ENTITY_CONVERTER,
+                Optional.of(unused -> SearchAction.<EntityValue>newBuilder().build()),
+                TypeConverters::toEntityValue);
+        ActionCapabilityInternal capability =
+                createTaskCapability(
+                        SINGLE_REQUIRED_FIELD_PROPERTY,
+                        paramRegistry.build(),
+                        RequiredTaskUpdater::new,
+                        Optional.empty(),
+                        EMPTY_CONFIRM_LISTENER,
+                        EMPTY_FINISH_LISTENER);
+
+        // TURN 1.
+        SettableFutureWrapper<Boolean> onSuccessInvoked = new SettableFutureWrapper<>();
+
+        capability.execute(
+                buildRequestArgs(SYNC, "required", buildSearchActionParamValue("invalid")),
+                buildActionCallback(onSuccessInvoked));
+
+        assertThat(onSuccessInvoked.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+        assertThat(capability.getAppAction())
+                .isEqualTo(
+                        AppAction.newBuilder()
+                                .setName("actions.intent.TEST")
+                                .setIdentifier("id")
+                                .addParams(
+                                        IntentParameter.newBuilder()
+                                                .setName("required")
+                                                .setIsRequired(true)
+                                                .addCurrentValue(
+                                                        CurrentValue.newBuilder()
+                                                                .setValue(
+                                                                        buildSearchActionParamValue(
+                                                                                "invalid"))
+                                                                .setStatus(
+                                                                        CurrentValue.Status
+                                                                                .DISAMBIG)
+                                                                .setDisambiguationData(
+                                                                        DisambiguationData
+                                                                                .newBuilder()
+                                                                                .addEntities(
+                                                                                        Entity
+                                                                                                .newBuilder()
+                                                                                                .setIdentifier(
+                                                                                                        "valid1")
+                                                                                                .setName(
+                                                                                                        "valid1"))
+                                                                                .addEntities(
+                                                                                        Entity
+                                                                                                .newBuilder()
+                                                                                                .setIdentifier(
+                                                                                                        "valid2")
+                                                                                                .setName(
+                                                                                                        "valid2")))))
+                                .setTaskInfo(
+                                        TaskInfo.newBuilder().setSupportsPartialFulfillment(true))
+                                .build());
+
+        // TURN 2.
+        SettableFutureWrapper<Boolean> turn2SuccessInvoked = new SettableFutureWrapper<>();
+
+        capability.execute(
+                buildRequestArgs(
+                        SYNC,
+                        "required",
+                        ParamValue.newBuilder()
+                                .setIdentifier("valid2")
+                                .setStringValue("valid2")
+                                .build()),
+                buildActionCallback(turn2SuccessInvoked));
+
+        assertThat(turn2SuccessInvoked.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+        assertThat(capability.getAppAction())
+                .isEqualTo(
+                        AppAction.newBuilder()
+                                .setName("actions.intent.TEST")
+                                .setIdentifier("id")
+                                .addParams(
+                                        IntentParameter.newBuilder()
+                                                .setName("required")
+                                                .setIsRequired(true)
+                                                .addCurrentValue(
+                                                        CurrentValue.newBuilder()
+                                                                .setValue(
+                                                                        ParamValue.newBuilder()
+                                                                                .setIdentifier(
+                                                                                        "valid2")
+                                                                                .setStringValue(
+                                                                                        "valid2"))
+                                                                .setStatus(
+                                                                        CurrentValue.Status
+                                                                                .ACCEPTED)))
+                                .setTaskInfo(
+                                        TaskInfo.newBuilder().setSupportsPartialFulfillment(true))
+                                .build());
+    }
+
+    /**
+     * Assistant sends grounded objects as identifier only, but we need to mark the entire value
+     * struct as accepted.
+     */
+    @Test
+    public void identifierOnly_refillsStruct() throws Exception {
+        ListItem item1 = ListItem.newBuilder().setName("red apple").setId("item1").build();
+        ListItem item2 = ListItem.newBuilder().setName("green apple").setId("item2").build();
+        SettableFutureWrapper<ListItem> onReceivedCb = new SettableFutureWrapper<>();
+        CapabilityStructFill.Property property =
+                CapabilityStructFill.Property.newBuilder()
+                        .setItemList(SimpleProperty.REQUIRED)
+                        .setAnyString(StringProperty.newBuilder().setIsRequired(true).build())
+                        .build();
+        TaskParamRegistry.Builder paramRegistryBuilder = TaskParamRegistry.builder();
+        paramRegistryBuilder.addTaskParameter(
+                "listItem",
+                TaskCapabilityImplTest::groundingPredicate,
+                GenericResolverInternal.fromAppEntityResolver(
+                        new AppEntityResolver<ListItem>() {
+                            @NonNull
+                            @Override
+                            public ListenableFuture<ValidationResult> onReceived(
+                                    ListItem listItem) {
+                                onReceivedCb.set(listItem);
+                                return Futures.immediateFuture(ValidationResult.newAccepted());
+                            }
+
+                            @Override
+                            public ListenableFuture<EntitySearchResult<ListItem>> lookupAndRender(
+                                    SearchAction<ListItem> searchAction) {
+                                return Futures.immediateFuture(
+                                        EntitySearchResult.<ListItem>newBuilder()
+                                                .addPossibleValue(item1)
+                                                .addPossibleValue(item2)
+                                                .build());
+                            }
+                        }),
+                Optional.of((DisambigEntityConverter<ListItem>) TypeConverters::toEntity),
+                Optional.of(unused -> SearchAction.<ListItem>newBuilder().build()),
+                TypeConverters::toListItem);
+        SettableFutureWrapper<ListItem> onFinishListItemCb = new SettableFutureWrapper<>();
+        SettableFutureWrapper<String> onFinishStringCb = new SettableFutureWrapper<>();
+        TaskCapabilityImpl<
+                        CapabilityStructFill.Property,
+                        CapabilityStructFill.Argument,
+                        Void,
+                        Void,
+                        EmptyTaskUpdater>
+                capability =
+                        new TaskCapabilityImpl<>(
+                                "selectListItem",
+                                CapabilityStructFill.ACTION_SPEC,
+                                property,
+                                paramRegistryBuilder.build(),
+                                /* onInitListener= */ Optional.empty(),
+                                /* onReadyToConfirmListener= */ Optional.empty(),
+                                (argument) -> {
+                                    ListItem listItem = argument.listItem().orElse(null);
+                                    String string = argument.anyString().orElse(null);
+                                    onFinishListItemCb.set(listItem);
+                                    onFinishStringCb.set(string);
+                                    return Futures.immediateFuture(
+                                            ExecutionResult.getDefaultInstance());
+                                },
+                                /* confirmationOutputBindings= */ Collections.emptyMap(),
+                                /* executionOutputBindings= */ Collections.emptyMap(),
+                                Runnable::run);
+        capability.setTaskUpdaterSupplier(EmptyTaskUpdater::new);
+
+        // first sync request
+        SettableFutureWrapper<Boolean> firstTurnSuccess = new SettableFutureWrapper<>();
+        capability.execute(
+                buildRequestArgs(SYNC, "listItem", buildSearchActionParamValue("apple")),
+                buildActionCallback(firstTurnSuccess));
+        assertThat(firstTurnSuccess.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+        assertThat(onReceivedCb.getFuture().isDone()).isFalse();
+        assertThat(onFinishListItemCb.getFuture().isDone()).isFalse();
+        assertThat(capability.getAppAction())
+                .isEqualTo(
+                        AppAction.newBuilder()
+                                .setName("actions.intent.TEST")
+                                .setIdentifier("selectListItem")
+                                .addParams(
+                                        IntentParameter.newBuilder()
+                                                .setName("listItem")
+                                                .setIsRequired(true)
+                                                .addCurrentValue(
+                                                        CurrentValue.newBuilder()
+                                                                .setValue(
+                                                                        buildSearchActionParamValue(
+                                                                                "apple"))
+                                                                .setStatus(
+                                                                        CurrentValue.Status
+                                                                                .DISAMBIG)
+                                                                .setDisambiguationData(
+                                                                        DisambiguationData
+                                                                                .newBuilder()
+                                                                                .addEntities(
+                                                                                        TypeConverters
+                                                                                                .toEntity(
+                                                                                                        item1))
+                                                                                .addEntities(
+                                                                                        TypeConverters
+                                                                                                .toEntity(
+                                                                                                        item2))
+                                                                                .build())
+                                                                .build())
+                                                .build())
+                                .addParams(
+                                        IntentParameter.newBuilder()
+                                                .setName("string")
+                                                .setIsRequired(true)
+                                                .build())
+                                .setTaskInfo(
+                                        TaskInfo.newBuilder().setSupportsPartialFulfillment(true))
+                                .build());
+
+        // second sync request, sending grounded ParamValue with identifier only
+        SettableFutureWrapper<Boolean> secondTurnSuccess = new SettableFutureWrapper<>();
+        capability.execute(
+                buildRequestArgs(
+                        SYNC, "listItem", ParamValue.newBuilder().setIdentifier("item2").build()),
+                buildActionCallback(secondTurnSuccess));
+        assertThat(secondTurnSuccess.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+        assertThat(onReceivedCb.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isEqualTo(item2);
+        assertThat(onFinishListItemCb.getFuture().isDone()).isFalse();
+
+        // third sync request, sending grounded ParamValue with identifier only, completes task
+        SettableFutureWrapper<Boolean> thirdTurnSuccess = new SettableFutureWrapper<>();
+        capability.execute(
+                buildRequestArgs(
+                        SYNC,
+                        "listItem",
+                        ParamValue.newBuilder().setIdentifier("item2").build(),
+                        "string",
+                        "unused"),
+                buildActionCallback(thirdTurnSuccess));
+        assertThat(thirdTurnSuccess.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+        assertThat(onFinishListItemCb.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isEqualTo(item2);
+        assertThat(onFinishStringCb.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isEqualTo("unused");
+    }
+
+    @Test
+    public void executionResult_resultReturned() throws Exception {
+        OnDialogFinishListener<Argument, Output> finishListener =
+                (argument) ->
+                        Futures.immediateFuture(
+                                ExecutionResult.<Output>newBuilderWithOutput()
+                                        .setOutput(
+                                                Output.builder()
+                                                        .setOptionalStringField("bar")
+                                                        .setRepeatedStringField(
+                                                                Arrays.asList("bar1", "bar2"))
+                                                        .build())
+                                        .build());
+        ActionCapabilityInternal capability =
+                (ActionCapabilityInternal)
+                        new CapabilityBuilder()
+                                .setTaskHandlerBuilder(
+                                        new TaskHandlerBuilder()
+                                                .setOnFinishListener(finishListener))
+                                .build();
+        SettableFutureWrapper<FulfillmentResponse> onSuccessInvoked = new SettableFutureWrapper<>();
+        StructuredOutput expectedOutput =
+                StructuredOutput.newBuilder()
+                        .addOutputValues(
+                                OutputValue.newBuilder()
+                                        .setName("optionalStringOutput")
+                                        .addValues(
+                                                ParamValue.newBuilder()
+                                                        .setStringValue("bar")
+                                                        .build())
+                                        .build())
+                        .addOutputValues(
+                                OutputValue.newBuilder()
+                                        .setName("repeatedStringOutput")
+                                        .addValues(
+                                                ParamValue.newBuilder()
+                                                        .setStringValue("bar1")
+                                                        .build())
+                                        .addValues(
+                                                ParamValue.newBuilder()
+                                                        .setStringValue("bar2")
+                                                        .build())
+                                        .build())
+                        .build();
+
+        capability.execute(
+                buildRequestArgs(
+                        SYNC,
+                        /* args...= */ "required",
+                        ParamValue.newBuilder().setIdentifier("foo").setStringValue("foo").build()),
+                buildActionCallbackWithFulfillmentResponse(onSuccessInvoked));
+
+        assertThat(
+                        onSuccessInvoked
+                                .getFuture()
+                                .get(CB_TIMEOUT, MILLISECONDS)
+                                .getExecutionOutput()
+                                .getOutputValuesList())
+                .containsExactlyElementsIn(expectedOutput.getOutputValuesList());
+    }
+
+    @Test
+    public void touchEvent_fillOnlySlot_onFinishInvoked() throws Exception {
+        EntityValue slotValue = EntityValue.newBuilder().setId("id1").setValue("value").build();
+        SettableFutureWrapper<RequiredTaskUpdater> updaterFuture = new SettableFutureWrapper<>();
+        SettableFutureWrapper<Argument> onFinishFuture = new SettableFutureWrapper<>();
+        SettableFutureWrapper<FulfillmentResponse> touchEventResponse =
+                new SettableFutureWrapper<>();
+
+        ActionCapabilityInternal capability =
+                createTaskCapability(
+                        SINGLE_REQUIRED_FIELD_PROPERTY,
+                        TaskParamRegistry.builder().build(),
+                        RequiredTaskUpdater::new,
+                        TestingUtils.buildOnInitListener(updaterFuture),
+                        EMPTY_CONFIRM_LISTENER,
+                        TestingUtils.<Argument, Output>buildOnFinishListener(onFinishFuture));
+        capability.setTouchEventCallback(buildTouchEventCallback(touchEventResponse));
+
+        // Turn 1. No args but capability triggered (value updater should be set now).
+        SettableFutureWrapper<Boolean> onSuccessInvoked = new SettableFutureWrapper<>();
+        capability.execute(buildRequestArgs(SYNC), buildActionCallback(onSuccessInvoked));
+
+        assertThat(onSuccessInvoked.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+        assertThat(onFinishFuture.getFuture().isDone()).isFalse();
+        assertThat(touchEventResponse.getFuture().isDone()).isFalse();
+
+        // Turn 2. Invoke the TaskCapability via the updater.
+        // TaskUpdater should be usable after onFinishListener from the first turn.
+        RequiredTaskUpdater taskUpdater = updaterFuture.getFuture().get(CB_TIMEOUT, MILLISECONDS);
+        taskUpdater.setRequiredEntityValue(slotValue);
+
+        assertThat(
+                        onFinishFuture
+                                .getFuture()
+                                .get(CB_TIMEOUT, MILLISECONDS)
+                                .requiredEntityField()
+                                .get())
+                .isNotNull();
+        assertThat(
+                        onFinishFuture
+                                .getFuture()
+                                .get(CB_TIMEOUT, MILLISECONDS)
+                                .requiredEntityField()
+                                .get())
+                .isEqualTo(slotValue);
+        assertThat(touchEventResponse.getFuture().get(CB_TIMEOUT, MILLISECONDS))
+                .isEqualTo(FulfillmentResponse.getDefaultInstance());
+    }
+
+    @Test
+    public void touchEvent_callbackNotSet_onFinishNotInvoked() throws Exception {
+        EntityValue slotValue = EntityValue.newBuilder().setId("id1").setValue("value").build();
+        SettableFutureWrapper<RequiredTaskUpdater> updaterFuture = new SettableFutureWrapper<>();
+        SettableFutureWrapper<Argument> onFinishFuture = new SettableFutureWrapper<>();
+        ActionCapabilityInternal capability =
+                createTaskCapability(
+                        SINGLE_REQUIRED_FIELD_PROPERTY,
+                        TaskParamRegistry.builder().build(),
+                        RequiredTaskUpdater::new,
+                        TestingUtils.buildOnInitListener(updaterFuture),
+                        EMPTY_CONFIRM_LISTENER,
+                        TestingUtils.<Argument, Output>buildOnFinishListener(onFinishFuture));
+        // Explicitly set to null for testing.
+        capability.setTouchEventCallback(null);
+
+        // Turn 1. No args but capability triggered (value updater should be set now).
+        SettableFutureWrapper<Boolean> onSuccessInvoked = new SettableFutureWrapper<>();
+        capability.execute(buildRequestArgs(SYNC), buildActionCallback(onSuccessInvoked));
+
+        assertThat(onSuccessInvoked.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+        assertThat(onFinishFuture.getFuture().isDone()).isFalse();
+
+        // Turn 2. Invoke the TaskCapability via the updater.
+        RequiredTaskUpdater taskUpdater = updaterFuture.getFuture().get(CB_TIMEOUT, MILLISECONDS);
+        taskUpdater.setRequiredEntityValue(slotValue);
+
+        assertThat(onFinishFuture.getFuture().isDone()).isFalse();
+    }
+
+    @Test
+    public void touchEvent_emptyValues_onFinishNotInvoked() throws Exception {
+        SettableFutureWrapper<RequiredTaskUpdater> updaterFuture = new SettableFutureWrapper<>();
+        SettableFutureWrapper<Argument> onFinishFuture = new SettableFutureWrapper<>();
+        SettableFutureWrapper<FulfillmentResponse> touchEventResponse =
+                new SettableFutureWrapper<>();
+        TaskCapabilityImpl<Property, Argument, Output, Confirmation, RequiredTaskUpdater>
+                capability =
+                        createTaskCapability(
+                                SINGLE_REQUIRED_FIELD_PROPERTY,
+                                TaskParamRegistry.builder().build(),
+                                RequiredTaskUpdater::new,
+                                TestingUtils.buildOnInitListener(updaterFuture),
+                                EMPTY_CONFIRM_LISTENER,
+                                TestingUtils.<Argument, Output>buildOnFinishListener(
+                                        onFinishFuture));
+        capability.setTouchEventCallback(buildTouchEventCallback(touchEventResponse));
+
+        // Turn 1. No args but capability triggered (value updater should be set now).
+        SettableFutureWrapper<Boolean> onSuccessInvoked = new SettableFutureWrapper<>();
+        capability.execute(buildRequestArgs(SYNC), buildActionCallback(onSuccessInvoked));
+
+        assertThat(onSuccessInvoked.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+        assertThat(onFinishFuture.getFuture().isDone()).isFalse();
+        assertThat(touchEventResponse.getFuture().isDone()).isFalse();
+
+        // Turn 2. Invoke the TaskCapability via the updater.
+        // TaskUpdater should be usable after onFinishListener from the first turn.
+        capability.updateParamValues(Collections.emptyMap());
+
+        assertThat(onFinishFuture.getFuture().isDone()).isFalse();
+        assertThat(touchEventResponse.getFuture().isDone()).isFalse();
+    }
+
+    @Test
+    public void touchEvent_fillOnlySlot_confirmationRequired_onReadyToConfirmInvoked()
+            throws Exception {
+        EntityValue slotValue = EntityValue.newBuilder().setId("id1").setValue("value").build();
+        SettableFutureWrapper<RequiredTaskUpdater> updaterFuture = new SettableFutureWrapper<>();
+        SettableFutureWrapper<Argument> onReadyToConfirmFuture = new SettableFutureWrapper<>();
+        SettableFutureWrapper<Argument> onFinishFuture = new SettableFutureWrapper<>();
+        SettableFutureWrapper<FulfillmentResponse> touchEventResponse =
+                new SettableFutureWrapper<>();
+
+        TaskCapabilityImpl<Property, Argument, Output, Confirmation, RequiredTaskUpdater>
+                capability =
+                        createTaskCapability(
+                                SINGLE_REQUIRED_FIELD_PROPERTY,
+                                TaskParamRegistry.builder().build(),
+                                RequiredTaskUpdater::new,
+                                TestingUtils.buildOnInitListener(updaterFuture),
+                                TestingUtils.<Argument, Confirmation>buildOnReadyToConfirmListener(
+                                        onReadyToConfirmFuture),
+                                TestingUtils.<Argument, Output>buildOnFinishListener(
+                                        onFinishFuture));
+        capability.setTouchEventCallback(buildTouchEventCallback(touchEventResponse));
+
+        // Turn 1. No args but capability triggered (value updater should be set now).
+        SettableFutureWrapper<Boolean> onSuccessInvoked = new SettableFutureWrapper<>();
+        capability.execute(buildRequestArgs(SYNC), buildActionCallback(onSuccessInvoked));
+
+        assertThat(onSuccessInvoked.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+        assertThat(onReadyToConfirmFuture.getFuture().isDone()).isFalse();
+        assertThat(onFinishFuture.getFuture().isDone()).isFalse();
+        assertThat(touchEventResponse.getFuture().isDone()).isFalse();
+
+        // Turn 2. Invoke the TaskCapability via the updater.
+        // TaskUpdater should be usable after onReadyToConfirmListener from the first turn.
+        RequiredTaskUpdater taskUpdater = updaterFuture.getFuture().get(CB_TIMEOUT, MILLISECONDS);
+        taskUpdater.setRequiredEntityValue(slotValue);
+
+        assertThat(onFinishFuture.getFuture().isDone()).isFalse();
+        assertThat(
+                        onReadyToConfirmFuture
+                                .getFuture()
+                                .get(CB_TIMEOUT, MILLISECONDS)
+                                .requiredEntityField()
+                                .get())
+                .isNotNull();
+        assertThat(
+                        onReadyToConfirmFuture
+                                .getFuture()
+                                .get(CB_TIMEOUT, MILLISECONDS)
+                                .requiredEntityField()
+                                .get())
+                .isEqualTo(slotValue);
+        assertThat(touchEventResponse.getFuture().get(CB_TIMEOUT, MILLISECONDS))
+                .isEqualTo(FulfillmentResponse.getDefaultInstance());
+    }
+
+    @Test
+    public void requiredConfirmation_throwsExecptionWhenConfirmationListenerIsNotSet()
+            throws Exception {
+        InvalidTaskException exception =
+                assertThrows(
+                        InvalidTaskException.class,
+                        () ->
+                                new CapabilityBuilderWithRequiredConfirmation()
+                                        .setTaskHandlerBuilder(
+                                                new TaskHandlerBuilderWithRequiredConfirmation())
+                                        .build());
+
+        assertThat(exception)
+                .hasMessageThat()
+                .contains("ConfirmationType is REQUIRED, but onReadyToConfirmListener is not set.");
+    }
+
+    @Test
+    public void confirmationNotSupported_throwsExecptionWhenConfirmationListenerIsSet()
+            throws Exception {
+
+        OnReadyToConfirmListenerInternal<Confirmation> onReadyToConfirmListener =
+                (args) ->
+                        Futures.immediateFuture(
+                                ConfirmationOutput.<Confirmation>newBuilderWithConfirmation()
+                                        .setConfirmation(
+                                                Confirmation.builder()
+                                                        .setOptionalStringField("bar")
+                                                        .build())
+                                        .build());
+
+        InvalidTaskException exception =
+                assertThrows(
+                        InvalidTaskException.class,
+                        () ->
+                                new CapabilityBuilder()
+                                        .setTaskHandlerBuilder(
+                                                new TaskHandlerBuilder()
+                                                        .setOnReadyToConfirmListener(
+                                                                onReadyToConfirmListener))
+                                        .build());
+
+        assertThat(exception)
+                .hasMessageThat()
+                .contains(
+                        "ConfirmationType is NOT_SUPPORTED, but onReadyToConfirmListener is set.");
+    }
+
+    @Test
+    public void confirmationOutput_resultReturned() throws Exception {
+        OnReadyToConfirmListenerInternal<Confirmation> onReadyToConfirmListener =
+                (args) ->
+                        Futures.immediateFuture(
+                                ConfirmationOutput.<Confirmation>newBuilderWithConfirmation()
+                                        .setConfirmation(
+                                                Confirmation.builder()
+                                                        .setOptionalStringField("bar")
+                                                        .build())
+                                        .build());
+        OnDialogFinishListener<Argument, Output> finishListener =
+                (argument) ->
+                        Futures.immediateFuture(
+                                ExecutionResult.<Output>newBuilderWithOutput()
+                                        .setOutput(
+                                                Output.builder()
+                                                        .setOptionalStringField("baz")
+                                                        .setRepeatedStringField(
+                                                                Arrays.asList("baz1", "baz2"))
+                                                        .build())
+                                        .build());
+        ActionCapabilityInternal capability =
+                (ActionCapabilityInternal)
+                        new CapabilityBuilderWithRequiredConfirmation()
+                                .setTaskHandlerBuilder(
+                                        new TaskHandlerBuilderWithRequiredConfirmation()
+                                                .setOnReadyToConfirmListener(
+                                                        onReadyToConfirmListener)
+                                                .setOnFinishListener(finishListener))
+                                .build();
+        SettableFutureWrapper<FulfillmentResponse> onSuccessInvoked = new SettableFutureWrapper<>();
+        StructuredOutput expectedOutput =
+                StructuredOutput.newBuilder()
+                        .addOutputValues(
+                                OutputValue.newBuilder()
+                                        .setName("optionalStringOutput")
+                                        .addValues(
+                                                ParamValue.newBuilder()
+                                                        .setStringValue("bar")
+                                                        .build())
+                                        .build())
+                        .build();
+
+        capability.execute(
+                buildRequestArgs(
+                        SYNC,
+                        /* args...= */ "required",
+                        ParamValue.newBuilder().setIdentifier("foo").setStringValue("foo").build()),
+                buildActionCallbackWithFulfillmentResponse(onSuccessInvoked));
+
+        assertThat(
+                        onSuccessInvoked
+                                .getFuture()
+                                .get(CB_TIMEOUT, MILLISECONDS)
+                                .getConfirmationData()
+                                .getOutputValuesList())
+                .containsExactlyElementsIn(expectedOutput.getOutputValuesList());
+    }
+
+    @Test
+    public void executionResult_resultReturnedAfterConfirm() throws Exception {
+        // Build the capability
+        OnReadyToConfirmListenerInternal<Confirmation> onReadyToConfirmListener =
+                (args) ->
+                        Futures.immediateFuture(
+                                ConfirmationOutput.<Confirmation>newBuilderWithConfirmation()
+                                        .setConfirmation(
+                                                Confirmation.builder()
+                                                        .setOptionalStringField("bar")
+                                                        .build())
+                                        .build());
+        OnDialogFinishListener<Argument, Output> finishListener =
+                (argument) ->
+                        Futures.immediateFuture(
+                                ExecutionResult.<Output>newBuilderWithOutput()
+                                        .setOutput(
+                                                Output.builder()
+                                                        .setOptionalStringField("baz")
+                                                        .setRepeatedStringField(
+                                                                Arrays.asList("baz1", "baz2"))
+                                                        .build())
+                                        .build());
+        ActionCapabilityInternal capability =
+                (ActionCapabilityInternal)
+                        new CapabilityBuilderWithRequiredConfirmation()
+                                .setTaskHandlerBuilder(
+                                        new TaskHandlerBuilderWithRequiredConfirmation()
+                                                .setOnReadyToConfirmListener(
+                                                        onReadyToConfirmListener)
+                                                .setOnFinishListener(finishListener))
+                                .build();
+        SettableFutureWrapper<FulfillmentResponse> onSuccessInvokedFirstTurn =
+                new SettableFutureWrapper<>();
+
+        // Send a sync request that triggers confirmation
+        capability.execute(
+                buildRequestArgs(
+                        SYNC,
+                        /* args...= */ "required",
+                        ParamValue.newBuilder().setIdentifier("foo").setStringValue("foo").build()),
+                buildActionCallbackWithFulfillmentResponse(onSuccessInvokedFirstTurn));
+
+        // Confirm the BIC
+        StructuredOutput expectedConfirmationOutput =
+                StructuredOutput.newBuilder()
+                        .addOutputValues(
+                                OutputValue.newBuilder()
+                                        .setName("optionalStringOutput")
+                                        .addValues(
+                                                ParamValue.newBuilder()
+                                                        .setStringValue("bar")
+                                                        .build())
+                                        .build())
+                        .build();
+        assertThat(
+                        onSuccessInvokedFirstTurn
+                                .getFuture()
+                                .get(CB_TIMEOUT, MILLISECONDS)
+                                .getConfirmationData()
+                                .getOutputValuesList())
+                .containsExactlyElementsIn(expectedConfirmationOutput.getOutputValuesList());
+
+        // Send a CONFIRM request which indicates the user confirmed the BIC. This triggers
+        // onFinish.
+        SettableFutureWrapper<FulfillmentResponse> onSuccessInvokedSecondTurn =
+                new SettableFutureWrapper<>();
+        capability.execute(
+                buildRequestArgs(CONFIRM),
+                buildActionCallbackWithFulfillmentResponse(onSuccessInvokedSecondTurn));
+
+        // Confirm the BIO
+        StructuredOutput expectedOutput =
+                StructuredOutput.newBuilder()
+                        .addOutputValues(
+                                OutputValue.newBuilder()
+                                        .setName("optionalStringOutput")
+                                        .addValues(
+                                                ParamValue.newBuilder()
+                                                        .setStringValue("baz")
+                                                        .build())
+                                        .build())
+                        .addOutputValues(
+                                OutputValue.newBuilder()
+                                        .setName("repeatedStringOutput")
+                                        .addValues(
+                                                ParamValue.newBuilder()
+                                                        .setStringValue("baz1")
+                                                        .build())
+                                        .addValues(
+                                                ParamValue.newBuilder()
+                                                        .setStringValue("baz2")
+                                                        .build())
+                                        .build())
+                        .build();
+        assertThat(
+                        onSuccessInvokedSecondTurn
+                                .getFuture()
+                                .get(CB_TIMEOUT, MILLISECONDS)
+                                .getExecutionOutput()
+                                .getOutputValuesList())
+                .containsExactlyElementsIn(expectedOutput.getOutputValuesList());
+
+        // send TERMINATE request after CONFIRM
+        SettableFutureWrapper<Boolean> terminateCb = new SettableFutureWrapper<>();
+        capability.execute(buildRequestArgs(TERMINATE), buildActionCallback(terminateCb));
+
+        assertThat(terminateCb.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+    }
+
+    @Test
+    public void concurrentRequests_prioritizeAssistantRequest() throws Exception {
+        CapabilityTwoStrings.Property property =
+                CapabilityTwoStrings.Property.newBuilder()
+                        .setStringSlotA(StringProperty.newBuilder().setIsRequired(true).build())
+                        .setStringSlotB(StringProperty.newBuilder().setIsRequired(true).build())
+                        .build();
+        DeferredValueListener<String> slotAListener = new DeferredValueListener<>();
+        DeferredValueListener<String> slotBListener = new DeferredValueListener<>();
+        TaskParamRegistry.Builder paramRegistryBuilder = TaskParamRegistry.builder();
+        paramRegistryBuilder.addTaskParameter(
+                "stringSlotA",
+                unused -> false,
+                GenericResolverInternal.fromValueListener(slotAListener),
+                Optional.empty(),
+                Optional.empty(),
+                TypeConverters::toStringValue);
+        paramRegistryBuilder.addTaskParameter(
+                "stringSlotB",
+                unused -> false,
+                GenericResolverInternal.fromValueListener(slotBListener),
+                Optional.empty(),
+                Optional.empty(),
+                TypeConverters::toStringValue);
+        SettableFutureWrapper<String> onFinishCb = new SettableFutureWrapper<>();
+        TaskCapabilityImpl<
+                        CapabilityTwoStrings.Property,
+                        CapabilityTwoStrings.Argument,
+                        Void,
+                        Void,
+                        EmptyTaskUpdater>
+                taskCapability =
+                        new TaskCapabilityImpl<>(
+                                "myTestCapability",
+                                CapabilityTwoStrings.ACTION_SPEC,
+                                property,
+                                paramRegistryBuilder.build(),
+                                /* onInitListener= */ Optional.empty(),
+                                /* onReadyToConfirmListener= */ Optional.empty(),
+                                (argument) -> {
+                                    String slotA = argument.stringSlotA().orElse(null);
+                                    String slotB = argument.stringSlotB().orElse(null);
+                                    onFinishCb.set(String.format("%s %s", slotA, slotB));
+                                    return Futures.immediateFuture(
+                                            ExecutionResult.getDefaultInstance());
+                                },
+                                new HashMap<>(),
+                                new HashMap<>(),
+                                Runnable::run);
+        taskCapability.setTaskUpdaterSupplier(EmptyTaskUpdater::new);
+        ReusableTouchEventCallback touchEventCallback = new ReusableTouchEventCallback();
+        taskCapability.setTouchEventCallback(touchEventCallback);
+
+        // first assistant request
+        SettableFutureWrapper<FulfillmentResponse> firstTurnResult = new SettableFutureWrapper<>();
+        taskCapability.execute(
+                buildRequestArgs(SYNC, "stringSlotA", "apple"),
+                buildActionCallbackWithFulfillmentResponse(firstTurnResult));
+
+        // manual input request
+        Map<String, List<ParamValue>> touchEventParamValues = new HashMap<>();
+        touchEventParamValues.put(
+                "stringSlotA",
+                Collections.singletonList(ParamValue.newBuilder().setIdentifier("banana").build()));
+        taskCapability.updateParamValues(touchEventParamValues);
+
+        // second assistant request
+        SettableFutureWrapper<FulfillmentResponse> secondTurnResult = new SettableFutureWrapper<>();
+        taskCapability.execute(
+                buildRequestArgs(SYNC, "stringSlotA", "apple", "stringSlotB", "smoothie"),
+                buildActionCallbackWithFulfillmentResponse(secondTurnResult));
+
+        assertThat(firstTurnResult.getFuture().isDone()).isFalse();
+        assertThat(touchEventCallback.getLastResult()).isEqualTo(null);
+
+        // unblock first assistant request
+        slotAListener.setValidationResult(ValidationResult.newAccepted());
+        assertThat(firstTurnResult.getFuture().get(CB_TIMEOUT, MILLISECONDS))
+                .isEqualTo(FulfillmentResponse.getDefaultInstance());
+        assertThat(touchEventCallback.getLastResult()).isEqualTo(null);
+
+        // unblock second assistant request
+        slotBListener.setValidationResult(ValidationResult.newAccepted());
+        assertThat(secondTurnResult.getFuture().get(CB_TIMEOUT, MILLISECONDS))
+                .isEqualTo(FulfillmentResponse.getDefaultInstance());
+
+        assertThat(onFinishCb.getFuture().get(CB_TIMEOUT, MILLISECONDS))
+                .isEqualTo("apple smoothie");
+
+        // since task already finished, the manual input update was ignored.
+        assertThat(touchEventCallback.getLastResult()).isNull();
+    }
+
+    @Test
+    public void concurrentRequests_touchEventFinishesTask() throws Exception {
+        CapabilityTwoStrings.Property property =
+                CapabilityTwoStrings.Property.newBuilder()
+                        .setStringSlotA(StringProperty.newBuilder().setIsRequired(true).build())
+                        .setStringSlotB(StringProperty.newBuilder().setIsRequired(true).build())
+                        .build();
+        DeferredValueListener<String> slotAListener = new DeferredValueListener<>();
+        DeferredValueListener<String> slotBListener = new DeferredValueListener<>();
+        TaskParamRegistry.Builder paramRegistryBuilder = TaskParamRegistry.builder();
+        paramRegistryBuilder.addTaskParameter(
+                "stringSlotA",
+                unused -> false,
+                GenericResolverInternal.fromValueListener(slotAListener),
+                Optional.empty(),
+                Optional.empty(),
+                TypeConverters::toStringValue);
+        paramRegistryBuilder.addTaskParameter(
+                "stringSlotB",
+                unused -> false,
+                GenericResolverInternal.fromValueListener(slotBListener),
+                Optional.empty(),
+                Optional.empty(),
+                TypeConverters::toStringValue);
+        SettableFutureWrapper<String> onFinishCb = new SettableFutureWrapper<>();
+        TaskCapabilityImpl<
+                        CapabilityTwoStrings.Property,
+                        CapabilityTwoStrings.Argument,
+                        Void,
+                        Void,
+                        EmptyTaskUpdater>
+                taskCapability =
+                        new TaskCapabilityImpl<>(
+                                "myTestCapability",
+                                CapabilityTwoStrings.ACTION_SPEC,
+                                property,
+                                paramRegistryBuilder.build(),
+                                /* onInitListener= */ Optional.empty(),
+                                /* onReadyToConfirmListener= */ Optional.empty(),
+                                (argument) -> {
+                                    String slotA = argument.stringSlotA().orElse(null);
+                                    String slotB = argument.stringSlotB().orElse(null);
+                                    onFinishCb.set(String.format("%s %s", slotA, slotB));
+                                    return Futures.immediateFuture(
+                                            ExecutionResult.getDefaultInstance());
+                                },
+                                new HashMap<>(),
+                                new HashMap<>(),
+                                Runnable::run);
+        taskCapability.setTaskUpdaterSupplier(EmptyTaskUpdater::new);
+        ReusableTouchEventCallback touchEventCallback = new ReusableTouchEventCallback();
+        taskCapability.setTouchEventCallback(touchEventCallback);
+
+        // first assistant request
+        SettableFutureWrapper<FulfillmentResponse> firstTurnResult = new SettableFutureWrapper<>();
+        taskCapability.execute(
+                buildRequestArgs(SYNC, "stringSlotA", "apple"),
+                buildActionCallbackWithFulfillmentResponse(firstTurnResult));
+
+        // manual input request
+        Map<String, List<ParamValue>> touchEventParamValues = new HashMap<>();
+        touchEventParamValues.put(
+                "stringSlotB",
+                Collections.singletonList(
+                        ParamValue.newBuilder().setIdentifier("smoothie").build()));
+        taskCapability.updateParamValues(touchEventParamValues);
+
+        // second assistant request
+        SettableFutureWrapper<FulfillmentResponse> secondTurnResult = new SettableFutureWrapper<>();
+        taskCapability.execute(
+                buildRequestArgs(SYNC, "stringSlotA", "banana"),
+                buildActionCallbackWithFulfillmentResponse(secondTurnResult));
+
+        assertThat(firstTurnResult.getFuture().isDone()).isFalse();
+        assertThat(touchEventCallback.getLastResult()).isEqualTo(null);
+
+        // unblock first assistant request
+        slotAListener.setValidationResult(ValidationResult.newAccepted());
+        assertThat(firstTurnResult.getFuture().get(CB_TIMEOUT, MILLISECONDS))
+                .isEqualTo(FulfillmentResponse.getDefaultInstance());
+        assertThat(touchEventCallback.getLastResult()).isEqualTo(null);
+
+        // unblock second assistant request
+        slotAListener.setValidationResult(ValidationResult.newAccepted());
+        assertThat(secondTurnResult.getFuture().get(CB_TIMEOUT, MILLISECONDS))
+                .isEqualTo(FulfillmentResponse.getDefaultInstance());
+        assertThat(onFinishCb.getFuture().get(CB_TIMEOUT, MILLISECONDS))
+                .isEqualTo("banana smoothie");
+        assertThat(touchEventCallback.getLastResult().getKind())
+                .isEqualTo(TouchEventResult.Kind.SUCCESS);
+    }
+
+    @Test
+    public void touchEvent_noDisambig_continuesProcessing() throws Exception {
+        TaskParamRegistry.Builder paramRegistryBuilder = TaskParamRegistry.builder();
+        SettableFutureWrapper<RequiredTaskUpdater> taskUpdaterCb = new SettableFutureWrapper<>();
+        SettableFutureWrapper<Boolean> onFinishCb = new SettableFutureWrapper<>();
+        CapabilityTwoEntityValues.Property property =
+                CapabilityTwoEntityValues.Property.newBuilder()
+                        .setSlotA(EntityProperty.newBuilder().setIsRequired(true).build())
+                        .setSlotB(EntityProperty.newBuilder().setIsRequired(true).build())
+                        .build();
+        paramRegistryBuilder.addTaskParameter(
+                "slotA",
+                paramValue -> !paramValue.hasIdentifier(),
+                GenericResolverInternal.fromAppEntityResolver(
+                        new AppEntityResolver<EntityValue>() {
+                            @NonNull
+                            @Override
+                            public ListenableFuture<ValidationResult> onReceived(
+                                    EntityValue unused) {
+                                return Futures.immediateFuture(ValidationResult.newAccepted());
+                            }
+
+                            @Override
+                            public ListenableFuture<EntitySearchResult<EntityValue>>
+                                    lookupAndRender(SearchAction<EntityValue> unused) {
+                                return Futures.immediateFuture(
+                                        EntitySearchResult.<EntityValue>newBuilder()
+                                                .addPossibleValue(EntityValue.ofId("entityValue1"))
+                                                .addPossibleValue(EntityValue.ofId("entityValue2"))
+                                                .build());
+                            }
+                        }),
+                Optional.of(TypeConverters::toEntity),
+                Optional.of(paramValue -> SearchAction.<EntityValue>newBuilder().build()),
+                TypeConverters::toEntityValue);
+        TaskCapabilityImpl<
+                        CapabilityTwoEntityValues.Property,
+                        CapabilityTwoEntityValues.Argument,
+                        Void,
+                        Void,
+                        RequiredTaskUpdater>
+                taskCapability =
+                        new TaskCapabilityImpl<>(
+                                "fakeId",
+                                CapabilityTwoEntityValues.ACTION_SPEC,
+                                property,
+                                paramRegistryBuilder.build(),
+                                /* onInitListener= */ Optional.of(
+                                        taskUpdater -> {
+                                            taskUpdaterCb.set(taskUpdater);
+                                            return Futures.immediateVoidFuture();
+                                        }),
+                                /* onReadyToConfirmListener= */ Optional.empty(),
+                                /* onFinishListener= */ (paramValuesMap) -> {
+                            onFinishCb.set(true);
+                            return Futures.immediateFuture(
+                                            ExecutionResult.getDefaultInstance());
+                        },
+                                /* confirmationOutputBindings= */ new HashMap<>(),
+                                /* executionOutputBindings= */ new HashMap<>(),
+                                Runnable::run);
+        taskCapability.setTaskUpdaterSupplier(RequiredTaskUpdater::new);
+        SettableFutureWrapper<FulfillmentResponse> touchEventCb = new SettableFutureWrapper<>();
+        TouchEventCallback touchEventCallback = buildTouchEventCallback(touchEventCb);
+        taskCapability.setTouchEventCallback(touchEventCallback);
+
+        // turn 1
+        SettableFutureWrapper<Boolean> turn1Finished = new SettableFutureWrapper<>();
+        taskCapability.execute(
+                buildRequestArgs(
+                        SYNC, "slotA", buildSearchActionParamValue("query"), "slotB", "anything"),
+                buildActionCallback(turn1Finished));
+
+        assertThat(turn1Finished.getFuture().get(CB_TIMEOUT, MILLISECONDS)).isTrue();
+        assertThat(taskCapability.getAppAction())
+                .isEqualTo(
+                        AppAction.newBuilder()
+                                .setName("actions.intent.TEST")
+                                .setIdentifier("fakeId")
+                                .addParams(
+                                        IntentParameter.newBuilder()
+                                                .setName("slotA")
+                                                .setIsRequired(true)
+                                                .addCurrentValue(
+                                                        CurrentValue.newBuilder()
+                                                                .setValue(
+                                                                        buildSearchActionParamValue(
+                                                                                "query"))
+                                                                .setStatus(
+                                                                        CurrentValue.Status
+                                                                                .DISAMBIG)
+                                                                .setDisambiguationData(
+                                                                        DisambiguationData
+                                                                                .newBuilder()
+                                                                                .addEntities(
+                                                                                        Entity
+                                                                                                .newBuilder()
+                                                                                                .setIdentifier(
+                                                                                                        "entityValue1")
+                                                                                                .setName(
+                                                                                                        "entityValue1")
+                                                                                                .build())
+                                                                                .addEntities(
+                                                                                        Entity
+                                                                                                .newBuilder()
+                                                                                                .setIdentifier(
+                                                                                                        "entityValue2")
+                                                                                                .setName(
+                                                                                                        "entityValue2")
+                                                                                                .build())
+                                                                                .build())
+                                                                .build())
+                                                .build())
+                                .addParams(
+                                        IntentParameter.newBuilder()
+                                                .setName("slotB")
+                                                .setIsRequired(true)
+                                                .addCurrentValue(
+                                                        CurrentValue.newBuilder()
+                                                                .setValue(
+                                                                        ParamValue.newBuilder()
+                                                                                .setStringValue(
+                                                                                        "anything")
+                                                                                .build())
+                                                                .setStatus(
+                                                                        CurrentValue.Status.PENDING)
+                                                                .build())
+                                                .build())
+                                .setTaskInfo(
+                                        TaskInfo.newBuilder()
+                                                .setSupportsPartialFulfillment(true)
+                                                .build())
+                                .build());
+
+        // turn 2
+        taskCapability.updateParamValues(
+                Collections.singletonMap(
+                        "slotA",
+                        Collections.singletonList(
+                                ParamValue.newBuilder()
+                                        .setIdentifier("entityValue1")
+                                        .setStringValue("entityValue1")
+                                        .build())));
+
+        assertThat(touchEventCb.getFuture().get(CB_TIMEOUT, MILLISECONDS))
+                .isEqualTo(FulfillmentResponse.getDefaultInstance());
+        assertThat(onFinishCb.getFuture().get()).isTrue();
+    }
+
+    private static class RequiredTaskUpdater extends AbstractTaskUpdater {
+        void setRequiredEntityValue(EntityValue entityValue) {
+            super.updateParamValues(
+                    Collections.singletonMap(
+                            "required",
+                            Collections.singletonList(TypeConverters.toParamValue(entityValue))));
+        }
+    }
+
+    private static class EmptyTaskUpdater extends AbstractTaskUpdater {}
+
+    private static class DeferredValueListener<T> implements ValueListener<T> {
+
+        final AtomicReference<Completer<ValidationResult>> mCompleterRef = new AtomicReference<>();
+
+        void setValidationResult(ValidationResult t) {
+            Completer<ValidationResult> completer = mCompleterRef.getAndSet(null);
+            if (completer == null) {
+                throw new IllegalStateException("no onReceived is waiting");
+            }
+            completer.set(t);
+        }
+
+        @NonNull
+        @Override
+        public ListenableFuture<ValidationResult> onReceived(T value) {
+            return CallbackToFutureAdapter.getFuture(
+                    newCompleter -> {
+                        Completer<ValidationResult> oldCompleter =
+                                mCompleterRef.getAndSet(newCompleter);
+                        if (oldCompleter != null) {
+                            oldCompleter.setCancelled();
+                        }
+                        return "waiting for setValidationResult";
+                    });
+        }
+    }
+
+    private static class CapabilityBuilder
+            extends AbstractCapabilityBuilder<
+                    CapabilityBuilder,
+                    Property,
+                    Argument,
+                    Output,
+                    Confirmation,
+                    RequiredTaskUpdater> {
+        @SuppressWarnings("CheckReturnValue")
+        private CapabilityBuilder() {
+            super(ACTION_SPEC);
+            setId("id");
+            setProperty(SINGLE_REQUIRED_FIELD_PROPERTY);
+        }
+
+        @NonNull
+        public final CapabilityBuilder setTaskHandlerBuilder(
+                TaskHandlerBuilder taskHandlerBuilder) {
+            return setTaskHandler(taskHandlerBuilder.build());
+        }
+    }
+
+    private static class TaskHandlerBuilder
+            extends AbstractTaskHandlerBuilder<
+                    TaskHandlerBuilder, Argument, Output, Confirmation, RequiredTaskUpdater> {
+
+        private TaskHandlerBuilder() {
+            super.registerExecutionOutput(
+                    "optionalStringOutput",
+                    Output::optionalStringField,
+                    TypeConverters::toParamValue);
+            super.registerRepeatedExecutionOutput(
+                    "repeatedStringOutput",
+                    Output::repeatedStringField,
+                    TypeConverters::toParamValue);
+        }
+
+        @Override
+        protected Supplier<RequiredTaskUpdater> getTaskUpdaterSupplier() {
+            return RequiredTaskUpdater::new;
+        }
+
+        @CanIgnoreReturnValue
+        public TaskHandlerBuilder setOnReadyToConfirmListener(
+                OnReadyToConfirmListenerInternal<Confirmation> listener) {
+            return super.setOnReadyToConfirmListenerInternal(listener);
+        }
+    }
+
+    private static class CapabilityBuilderWithRequiredConfirmation
+            extends AbstractCapabilityBuilder<
+                    CapabilityBuilderWithRequiredConfirmation,
+                    Property,
+                    Argument,
+                    Output,
+                    Confirmation,
+                    RequiredTaskUpdater> {
+        @SuppressWarnings("CheckReturnValue")
+        private CapabilityBuilderWithRequiredConfirmation() {
+            super(ACTION_SPEC);
+            setProperty(SINGLE_REQUIRED_FIELD_PROPERTY);
+            setId("id");
+        }
+
+        @NonNull
+        public final CapabilityBuilderWithRequiredConfirmation setTaskHandlerBuilder(
+                TaskHandlerBuilderWithRequiredConfirmation taskHandlerBuilder) {
+            return setTaskHandler(taskHandlerBuilder.build());
+        }
+    }
+
+    private static class TaskHandlerBuilderWithRequiredConfirmation
+            extends AbstractTaskHandlerBuilder<
+                    TaskHandlerBuilderWithRequiredConfirmation,
+                    Argument,
+                    Output,
+                    Confirmation,
+                    RequiredTaskUpdater> {
+
+        private TaskHandlerBuilderWithRequiredConfirmation() {
+            super(ConfirmationType.REQUIRED);
+            super.registerExecutionOutput(
+                    "optionalStringOutput",
+                    Output::optionalStringField,
+                    TypeConverters::toParamValue);
+            super.registerRepeatedExecutionOutput(
+                    "repeatedStringOutput",
+                    Output::repeatedStringField,
+                    TypeConverters::toParamValue);
+            super.registerConfirmationOutput(
+                    "optionalStringOutput",
+                    Confirmation::optionalStringField,
+                    TypeConverters::toParamValue);
+        }
+
+        @Override
+        protected Supplier<RequiredTaskUpdater> getTaskUpdaterSupplier() {
+            return RequiredTaskUpdater::new;
+        }
+
+        @CanIgnoreReturnValue
+        public TaskHandlerBuilderWithRequiredConfirmation setOnReadyToConfirmListener(
+                OnReadyToConfirmListenerInternal<Confirmation> listener) {
+            return super.setOnReadyToConfirmListenerInternal(listener);
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtilsTest.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtilsTest.java
new file mode 100644
index 0000000..f1f6ae4
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtilsTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appactions.interaction.capabilities.core.impl.converters.PropertyConverter;
+import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
+import androidx.appactions.interaction.proto.AppActionsContext.IntentParameter;
+import androidx.appactions.interaction.proto.CurrentValue;
+import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.FulfillmentValue;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public final class TaskCapabilityUtilsTest {
+
+    @Test
+    public void isSlotFillingComplete_allRequiredParamsFilled_returnsTrue() {
+        Map<String, List<ParamValue>> args = new HashMap<>();
+        args.put(
+                "required",
+                Collections.singletonList(
+                        ParamValue.newBuilder().setStringValue("Donald").build()));
+        List<IntentParameter> intentParameters = new ArrayList<>();
+        intentParameters.add(
+                PropertyConverter.getIntentParameter(
+                        "required", StringProperty.newBuilder().setIsRequired(true).build()));
+
+        assertThat(TaskCapabilityUtils.isSlotFillingComplete(args, intentParameters)).isTrue();
+    }
+
+    @Test
+    public void isSlotFillingComplete_notAllRequiredParamsFilled_returnsFalse() {
+        List<IntentParameter> intentParameters = new ArrayList<>();
+        intentParameters.add(
+                PropertyConverter.getIntentParameter(
+                        "required", StringProperty.newBuilder().setIsRequired(true).build()));
+
+        assertThat(
+                TaskCapabilityUtils.isSlotFillingComplete(Collections.emptyMap(), intentParameters))
+                .isFalse();
+    }
+
+    @Test
+    public void canSkipSlotProcessing_true() {
+        List<CurrentValue> currentValues =
+                Collections.singletonList(
+                        CurrentValue.newBuilder()
+                                .setValue(ParamValue.newBuilder().setBoolValue(true).build())
+                                .setStatus(CurrentValue.Status.ACCEPTED)
+                                .build());
+        List<FulfillmentValue> fulfillmentValues =
+                Collections.singletonList(
+                        FulfillmentValue.newBuilder()
+                                .setValue(ParamValue.newBuilder().setBoolValue(true).build())
+                                .build());
+        assertThat(TaskCapabilityUtils.canSkipSlotProcessing(currentValues, fulfillmentValues))
+                .isTrue();
+    }
+
+    @Test
+    public void canSkipSlotProcessing_false_sizeDifference() {
+        List<CurrentValue> currentValues =
+                Collections.singletonList(
+                        CurrentValue.newBuilder()
+                                .setValue(ParamValue.newBuilder().setStringValue("a").build())
+                                .setStatus(CurrentValue.Status.ACCEPTED)
+                                .build());
+        List<FulfillmentValue> fulfillmentValues = new ArrayList<>();
+        fulfillmentValues.add(
+                FulfillmentValue.newBuilder()
+                        .setValue(ParamValue.newBuilder().setStringValue("a").build())
+                        .build());
+        fulfillmentValues.add(
+                FulfillmentValue.newBuilder()
+                        .setValue(ParamValue.newBuilder().setStringValue("b").build())
+                        .build());
+        assertThat(TaskCapabilityUtils.canSkipSlotProcessing(currentValues, fulfillmentValues))
+                .isFalse();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessorTest.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessorTest.java
new file mode 100644
index 0000000..6ec0dfb
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessorTest.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.task.impl;
+
+import static androidx.appactions.interaction.capabilities.core.testing.ArgumentUtils.buildSearchActionParamValue;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.concurrent.Futures;
+import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters;
+import androidx.appactions.interaction.capabilities.core.task.AppEntityResolver;
+import androidx.appactions.interaction.capabilities.core.task.EntitySearchResult;
+import androidx.appactions.interaction.capabilities.core.task.InventoryResolver;
+import androidx.appactions.interaction.capabilities.core.task.ValidationResult;
+import androidx.appactions.interaction.capabilities.core.task.ValueListListener;
+import androidx.appactions.interaction.capabilities.core.task.ValueListener;
+import androidx.appactions.interaction.capabilities.core.testing.spec.SettableFutureWrapper;
+import androidx.appactions.interaction.capabilities.core.values.SearchAction;
+import androidx.appactions.interaction.proto.CurrentValue;
+import androidx.appactions.interaction.proto.CurrentValue.Status;
+import androidx.appactions.interaction.proto.DisambiguationData;
+import androidx.appactions.interaction.proto.Entity;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+@RunWith(JUnit4.class)
+public final class TaskSlotProcessorTest {
+
+    private <T> GenericResolverInternal<T> createAssistantDisambigResolver(
+            ValidationResult validationResult,
+            Consumer<T> valueConsumer,
+            Consumer<List<String>> renderConsumer) {
+        return GenericResolverInternal.fromInventoryResolver(
+                new InventoryResolver<T>() {
+                    @NonNull
+                    @Override
+                    public ListenableFuture<ValidationResult> onReceived(T value) {
+                        valueConsumer.accept(value);
+                        return Futures.immediateFuture(validationResult);
+                    }
+
+                    @NonNull
+                    @Override
+                    public ListenableFuture<Void> renderChoices(@NonNull List<String> entityIDs) {
+                        renderConsumer.accept(entityIDs);
+                        return Futures.immediateVoidFuture();
+                    }
+                });
+    }
+
+    private <T> GenericResolverInternal<T> createValueResolver(
+            ValidationResult validationResult, Consumer<T> valueConsumer) {
+        return GenericResolverInternal.fromValueListener(
+                new ValueListener<T>() {
+                    @NonNull
+                    @Override
+                    public ListenableFuture<ValidationResult> onReceived(T value) {
+                        valueConsumer.accept(value);
+                        return Futures.immediateFuture(validationResult);
+                    }
+                });
+    }
+
+    private <T> GenericResolverInternal<T> createValueResolver(ValidationResult validationResult) {
+        return createValueResolver(validationResult, (unused) -> {
+        });
+    }
+
+    private <T> GenericResolverInternal<T> createValueListResolver(
+            ValidationResult validationResult, Consumer<List<T>> valueConsumer) {
+        return GenericResolverInternal.fromValueListListener(
+                new ValueListListener<T>() {
+                    @NonNull
+                    @Override
+                    public ListenableFuture<ValidationResult> onReceived(List<T> value) {
+                        valueConsumer.accept(value);
+                        return Futures.immediateFuture(validationResult);
+                    }
+                });
+    }
+
+    private <T> GenericResolverInternal<T> createAppEntityResolver(
+            ValidationResult validationResult,
+            Consumer<T> valueConsumer,
+            EntitySearchResult<T> appSearchResult,
+            Consumer<SearchAction<T>> appSearchConsumer) {
+        return GenericResolverInternal.fromAppEntityResolver(
+                new AppEntityResolver<T>() {
+                    @NonNull
+                    @Override
+                    public ListenableFuture<ValidationResult> onReceived(T value) {
+                        valueConsumer.accept(value);
+                        return Futures.immediateFuture(validationResult);
+                    }
+
+                    @Override
+                    public ListenableFuture<EntitySearchResult<T>> lookupAndRender(
+                            SearchAction<T> searchAction) {
+                        appSearchConsumer.accept(searchAction);
+                        return Futures.immediateFuture(appSearchResult);
+                    }
+                });
+    }
+
+    @Test
+    public void processSlot_singleValue_accepted() throws Exception {
+        TaskParamRegistry paramRegistry =
+                TaskParamRegistry.builder()
+                        .addTaskParameter(
+                                "singularValue",
+                                (paramValue) -> false,
+                                createValueResolver(ValidationResult.newAccepted()),
+                                Optional.empty(),
+                                Optional.empty(),
+                                TypeConverters::toStringValue)
+                        .build();
+        List<ParamValue> args =
+                Collections.singletonList(
+                        ParamValue.newBuilder().setIdentifier("testValue").build());
+
+        SlotProcessingResult result =
+                TaskSlotProcessor.processSlot(
+                                "singularValue",
+                                TaskCapabilityUtils.paramValuesToCurrentValue(args, Status.PENDING),
+                                paramRegistry,
+                                Runnable::run)
+                        .get();
+
+        assertThat(result.isSuccessful()).isTrue();
+        assertThat(result.processedValues())
+                .containsExactly(
+                        CurrentValue.newBuilder().setValue(args.get(0)).setStatus(
+                                Status.ACCEPTED).build());
+    }
+
+    @Test
+    public void processSlot_singleValue_rejected() throws Exception {
+        TaskParamRegistry paramRegistry =
+                TaskParamRegistry.builder()
+                        .addTaskParameter(
+                                "singularValue",
+                                (paramValue) -> false,
+                                createValueResolver(ValidationResult.newRejected()),
+                                Optional.empty(),
+                                Optional.empty(),
+                                TypeConverters::toStringValue)
+                        .build();
+        List<ParamValue> args =
+                Collections.singletonList(
+                        ParamValue.newBuilder().setIdentifier("testValue").build());
+
+        SlotProcessingResult result =
+                TaskSlotProcessor.processSlot(
+                                "singularValue",
+                                TaskCapabilityUtils.paramValuesToCurrentValue(args, Status.PENDING),
+                                paramRegistry,
+                                Runnable::run)
+                        .get();
+
+        assertThat(result.isSuccessful()).isFalse();
+        assertThat(result.processedValues())
+                .containsExactly(
+                        CurrentValue.newBuilder().setValue(args.get(0)).setStatus(
+                                Status.REJECTED).build());
+    }
+
+    @Test
+    public void processSlot_repeatedValue_accepted() throws Exception {
+        SettableFutureWrapper<List<String>> lastReceivedArgs = new SettableFutureWrapper<>();
+        TaskParamRegistry paramRegistry =
+                TaskParamRegistry.builder()
+                        .addTaskParameter(
+                                "repeatedValue",
+                                (paramValue) -> false,
+                                createValueListResolver(ValidationResult.newAccepted(),
+                                        lastReceivedArgs::set),
+                                Optional.empty(),
+                                Optional.empty(),
+                                TypeConverters::toStringValue)
+                        .build();
+        List<ParamValue> args =
+                Arrays.asList(
+                        ParamValue.newBuilder().setIdentifier("testValue1").build(),
+                        ParamValue.newBuilder().setIdentifier("testValue2").build());
+
+        SlotProcessingResult result =
+                TaskSlotProcessor.processSlot(
+                                "repeatedValue",
+                                TaskCapabilityUtils.paramValuesToCurrentValue(args, Status.PENDING),
+                                paramRegistry,
+                                Runnable::run)
+                        .get();
+
+        assertThat(result.isSuccessful()).isTrue();
+        assertThat(result.processedValues())
+                .containsExactly(
+                        CurrentValue.newBuilder().setValue(args.get(0)).setStatus(
+                                Status.ACCEPTED).build(),
+                        CurrentValue.newBuilder().setValue(args.get(1)).setStatus(
+                                Status.ACCEPTED).build());
+        assertThat(lastReceivedArgs.getFuture().get()).containsExactly("testValue1", "testValue2");
+    }
+
+    @Test
+    public void processSlot_repeatedValue_rejected() throws Exception {
+        SettableFutureWrapper<List<String>> lastReceivedArgs = new SettableFutureWrapper<>();
+        TaskParamRegistry paramRegistry =
+                TaskParamRegistry.builder()
+                        .addTaskParameter(
+                                "repeatedValue",
+                                (paramValue) -> false,
+                                createValueListResolver(ValidationResult.newRejected(),
+                                        lastReceivedArgs::set),
+                                Optional.empty(),
+                                Optional.empty(),
+                                TypeConverters::toStringValue)
+                        .build();
+        List<ParamValue> args =
+                Arrays.asList(
+                        ParamValue.newBuilder().setIdentifier("testValue1").build(),
+                        ParamValue.newBuilder().setIdentifier("testValue2").build());
+
+        SlotProcessingResult result =
+                TaskSlotProcessor.processSlot(
+                                "repeatedValue",
+                                TaskCapabilityUtils.paramValuesToCurrentValue(args, Status.PENDING),
+                                paramRegistry,
+                                Runnable::run)
+                        .get();
+
+        assertThat(result.isSuccessful()).isFalse();
+        assertThat(result.processedValues())
+                .containsExactly(
+                        CurrentValue.newBuilder().setValue(args.get(0)).setStatus(
+                                Status.REJECTED).build(),
+                        CurrentValue.newBuilder().setValue(args.get(1)).setStatus(
+                                Status.REJECTED).build());
+        assertThat(lastReceivedArgs.getFuture().get()).containsExactly("testValue1", "testValue2");
+    }
+
+    @Test
+    public void listValues_oneAccepted_oneAssistantDisambig_invokesRendererAndOnReceived()
+            throws Exception {
+        SettableFutureWrapper<String> onReceivedCb = new SettableFutureWrapper<>();
+        SettableFutureWrapper<List<String>> renderCb = new SettableFutureWrapper<>();
+        TaskParamRegistry paramRegistry =
+                TaskParamRegistry.builder()
+                        .addTaskParameter(
+                                "assistantDrivenSlot",
+                                (paramValue) -> !paramValue.hasIdentifier(),
+                                createAssistantDisambigResolver(
+                                        ValidationResult.newAccepted(), onReceivedCb::set,
+                                        renderCb::set),
+                                Optional.empty(),
+                                Optional.empty(),
+                                TypeConverters::toStringValue)
+                        .build();
+        CurrentValue previouslyAccepted =
+                CurrentValue.newBuilder()
+                        .setStatus(Status.ACCEPTED)
+                        .setValue(
+                                ParamValue.newBuilder()
+                                        .setIdentifier("id")
+                                        .setStructValue(
+                                                Struct.newBuilder()
+                                                        .putFields("id",
+                                                                Value.newBuilder().setStringValue(
+                                                                        "1234").build())))
+                        .build();
+        List<CurrentValue> values =
+                Arrays.asList(
+                        previouslyAccepted,
+                        CurrentValue.newBuilder()
+                                .setStatus(Status.PENDING)
+                                .setDisambiguationData(
+                                        DisambiguationData.newBuilder()
+                                                .addEntities(Entity.newBuilder().setIdentifier(
+                                                        "entity-1"))
+                                                .addEntities(Entity.newBuilder().setIdentifier(
+                                                        "entity-2")))
+                                .build());
+
+        SlotProcessingResult result =
+                TaskSlotProcessor.processSlot("assistantDrivenSlot", values, paramRegistry,
+                                Runnable::run)
+                        .get();
+
+        assertThat(result.isSuccessful()).isFalse();
+        assertThat(onReceivedCb.getFuture().get()).isEqualTo("id");
+        assertThat(renderCb.getFuture().get()).containsExactly("entity-1", "entity-2");
+        assertThat(result.processedValues())
+                .containsExactly(
+                        previouslyAccepted,
+                        CurrentValue.newBuilder()
+                                .setStatus(Status.DISAMBIG)
+                                .setDisambiguationData(
+                                        DisambiguationData.newBuilder()
+                                                .addEntities(Entity.newBuilder().setIdentifier(
+                                                        "entity-1"))
+                                                .addEntities(Entity.newBuilder().setIdentifier(
+                                                        "entity-2")))
+                                .build());
+    }
+
+    @Test
+    public void singularValue_appDisambigRejected_onReceivedNotCalled() throws Exception {
+        SettableFutureWrapper<String> onReceivedCb = new SettableFutureWrapper<>();
+        SettableFutureWrapper<SearchAction<String>> appSearchCb = new SettableFutureWrapper<>();
+        EntitySearchResult<String> entitySearchResult = EntitySearchResult.empty();
+        GenericResolverInternal<String> resolver =
+                createAppEntityResolver(
+                        ValidationResult.newAccepted(), // should not be invoked.
+                        onReceivedCb::set,
+                        entitySearchResult, // app-grounding returns REJECTED in all cases
+                        appSearchCb::set);
+        TaskParamRegistry paramRegistry =
+                TaskParamRegistry.builder()
+                        .addTaskParameter(
+                                "appDrivenSlot",
+                                (paramValue) -> true, // always invoke app-grounding in all cases
+                                resolver,
+                                Optional.of((unused) -> Entity.getDefaultInstance()),
+                                Optional.of(
+                                        (unused) ->
+                                                SearchAction.<String>newBuilder()
+                                                        .setQuery("A")
+                                                        .setObject("nested")
+                                                        .build()),
+                                TypeConverters::toStringValue) // Not invoked
+                        .build();
+        List<CurrentValue> values =
+                Arrays.asList(
+                        CurrentValue.newBuilder()
+                                .setStatus(Status.PENDING)
+                                .setValue(buildSearchActionParamValue("A"))
+                                .build());
+
+        SlotProcessingResult result =
+                TaskSlotProcessor.processSlot("appDrivenSlot", values, paramRegistry,
+                        Runnable::run).get();
+
+        assertThat(result.isSuccessful()).isFalse();
+        assertThat(onReceivedCb.getFuture().isDone()).isFalse();
+        assertThat(appSearchCb.getFuture().isDone()).isTrue();
+        assertThat(appSearchCb.getFuture().get())
+                .isEqualTo(SearchAction.<String>newBuilder().setQuery("A").setObject(
+                        "nested").build());
+        assertThat(result.processedValues())
+                .containsExactly(
+                        CurrentValue.newBuilder()
+                                .setStatus(Status.REJECTED)
+                                .setValue(buildSearchActionParamValue("A"))
+                                .build());
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/ArgumentUtils.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/ArgumentUtils.java
new file mode 100644
index 0000000..32a97b4
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/ArgumentUtils.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.testing;
+
+import static java.util.stream.Collectors.toMap;
+
+import androidx.appactions.interaction.capabilities.core.impl.ArgumentsWrapper;
+import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment;
+import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.FulfillmentParam;
+import androidx.appactions.interaction.proto.ParamValue;
+
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/** Utilities for creating objects to make testing classes less verbose. */
+public final class ArgumentUtils {
+
+    private ArgumentUtils() {
+    }
+
+    /**
+     * Useful for one-shot BIIs where the task data is not needed and the ParamValues are singular.
+     */
+    public static ArgumentsWrapper buildArgs(Map<String, ParamValue> args) {
+        return ArgumentUtils.buildListArgs(
+                args.entrySet().stream()
+                        .collect(toMap(e -> e.getKey(),
+                                e -> Collections.singletonList(e.getValue()))));
+    }
+
+    /** Useful for one-shot BIIs where the task data is not needed. */
+    public static ArgumentsWrapper buildListArgs(Map<String, List<ParamValue>> args) {
+        Fulfillment.Builder builder = Fulfillment.newBuilder();
+        for (Map.Entry<String, List<ParamValue>> entry : args.entrySet()) {
+            builder.addParams(
+                    FulfillmentParam.newBuilder().setName(entry.getKey()).addAllValues(
+                            entry.getValue()));
+        }
+        return ArgumentsWrapper.create(builder.build());
+    }
+
+    private static ParamValue toParamValue(Object argVal) {
+        if (argVal instanceof Integer) {
+            return ParamValue.newBuilder().setNumberValue(((Integer) argVal).intValue()).build();
+        } else if (argVal instanceof Double) {
+            return ParamValue.newBuilder().setNumberValue(((Double) argVal).doubleValue()).build();
+        } else if (argVal instanceof String) {
+            return ParamValue.newBuilder().setStringValue((String) argVal).build();
+        } else if (argVal instanceof Enum) {
+            return ParamValue.newBuilder().setIdentifier(argVal.toString()).build();
+        } else if (argVal instanceof ParamValue) {
+            return (ParamValue) argVal;
+        }
+        throw new IllegalArgumentException("invalid argument type.");
+    }
+
+    public static ParamValue buildSearchActionParamValue(String query) {
+        return ParamValue.newBuilder()
+                .setStructValue(
+                        Struct.newBuilder()
+                                .putFields("@type",
+                                        Value.newBuilder().setStringValue("SearchAction").build())
+                                .putFields("query",
+                                        Value.newBuilder().setStringValue(query).build())
+                                .build())
+                .build();
+    }
+
+    /**
+     * Convenience method to build ArgumentsWrapper based on plain java types. Input args should be
+     * even in length, where each String argName is followed by any type of argVal.
+     */
+    public static ArgumentsWrapper buildRequestArgs(Fulfillment.Type type, Object... args) {
+        Fulfillment.Builder builder = Fulfillment.newBuilder();
+        if (type != Fulfillment.Type.UNRECOGNIZED) {
+            builder.setType(type);
+        }
+        if (args.length == 0) {
+            return ArgumentsWrapper.create(builder.build());
+        }
+        if (args.length % 2 != 0) {
+            throw new IllegalArgumentException("Must call function with even number of args");
+        }
+        Map<String, List<ParamValue>> argsMap = new LinkedHashMap<>();
+        for (int argNamePos = 0, argValPos = 1; argValPos < args.length; ) {
+            if (!(args[argNamePos] instanceof String)) {
+                throw new IllegalArgumentException(
+                        "Argument must be instance of String but got: "
+                                + args[argNamePos].getClass());
+            }
+            String argName = (String) args[argNamePos];
+            ParamValue paramValue = toParamValue(args[argValPos]);
+            argsMap.computeIfAbsent(argName, (unused) -> new ArrayList<>());
+            Objects.requireNonNull(argsMap.get(argName)).add(paramValue);
+            argNamePos += 2;
+            argValPos += 2;
+        }
+        argsMap.entrySet().stream()
+                .forEach(
+                        entry ->
+                                builder.addParams(
+                                        FulfillmentParam.newBuilder()
+                                                .setName(entry.getKey())
+                                                .addAllValues(entry.getValue())));
+        return ArgumentsWrapper.create(builder.build());
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/TestingUtils.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/TestingUtils.java
new file mode 100644
index 0000000..da5a952
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/TestingUtils.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.testing;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.ConfirmationOutput;
+import androidx.appactions.interaction.capabilities.core.ExecutionResult;
+import androidx.appactions.interaction.capabilities.core.impl.CallbackInternal;
+import androidx.appactions.interaction.capabilities.core.impl.ErrorStatusInternal;
+import androidx.appactions.interaction.capabilities.core.impl.TouchEventCallback;
+import androidx.appactions.interaction.capabilities.core.impl.concurrent.Futures;
+import androidx.appactions.interaction.capabilities.core.task.OnDialogFinishListener;
+import androidx.appactions.interaction.capabilities.core.task.OnInitListener;
+import androidx.appactions.interaction.capabilities.core.task.OnReadyToConfirmListener;
+import androidx.appactions.interaction.capabilities.core.testing.spec.SettableFutureWrapper;
+import androidx.appactions.interaction.proto.FulfillmentResponse;
+import androidx.appactions.interaction.proto.TouchEventMetadata;
+
+import com.google.auto.value.AutoOneOf;
+import com.google.auto.value.AutoValue;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
+
+public final class TestingUtils {
+
+    public static final long CB_TIMEOUT = 1000L;
+
+    private TestingUtils() {}
+
+    public static CallbackInternal buildActionCallback(SettableFutureWrapper<Boolean> future) {
+        return new CallbackInternal() {
+            @Override
+            public void onSuccess(FulfillmentResponse response) {
+                future.set(true);
+            }
+
+            @Override
+            public void onError(ErrorStatusInternal error) {
+                future.set(false);
+            }
+        };
+    }
+
+    public static CallbackInternal buildActionCallbackWithFulfillmentResponse(
+            SettableFutureWrapper<FulfillmentResponse> future) {
+        return new CallbackInternal() {
+            @Override
+            public void onSuccess(FulfillmentResponse response) {
+                future.set(response);
+            }
+
+            @Override
+            public void onError(ErrorStatusInternal error) {
+                future.setException(
+                        new IllegalStateException(
+                                String.format(
+                                        "expected FulfillmentResponse, but got ErrorStatus=%s "
+                                                + "instead",
+                                        error)));
+            }
+        };
+    }
+
+    public static CallbackInternal buildErrorActionCallback(
+            SettableFutureWrapper<ErrorStatusInternal> future) {
+        return new CallbackInternal() {
+            @Override
+            public void onSuccess(FulfillmentResponse response) {
+                future.setException(
+                        new IllegalStateException(
+                                "expected ErrorStatus, but got FulfillmentResponse instead"));
+            }
+
+            @Override
+            public void onError(ErrorStatusInternal error) {
+                future.set(error);
+            }
+        };
+    }
+
+    public static <T> Optional<OnInitListener<T>> buildOnInitListener(
+            SettableFutureWrapper<T> updaterFuture) {
+        return Optional.of(
+                new OnInitListener<T>() {
+                    @NonNull
+                    @Override
+                    public ListenableFuture<Void> onInit(T taskUpdater) {
+                        updaterFuture.set(taskUpdater);
+                        return Futures.immediateVoidFuture();
+                    }
+                });
+    }
+
+    public static <ArgumentT, ConfirmationT>
+            Optional<OnReadyToConfirmListener<ArgumentT, ConfirmationT>>
+                    buildOnReadyToConfirmListener(SettableFutureWrapper<ArgumentT> future) {
+        return Optional.of(
+                (finalArgs) -> {
+                    future.set(finalArgs);
+                    return Futures.immediateFuture(
+                            ConfirmationOutput.<ConfirmationT>getDefaultInstanceWithConfirmation());
+                });
+    }
+
+    public static <ArgumentT, OutputT>
+            OnDialogFinishListener<ArgumentT, OutputT> buildOnFinishListener(
+                    SettableFutureWrapper<ArgumentT> future) {
+        return (finalArgs) -> {
+            future.set(finalArgs);
+            return Futures.immediateFuture(ExecutionResult.<OutputT>getDefaultInstanceWithOutput());
+        };
+    }
+
+    public static TouchEventCallback buildTouchEventCallback(
+            SettableFutureWrapper<FulfillmentResponse> future) {
+        return new TouchEventCallback() {
+            @Override
+            public void onSuccess(
+                    @NonNull FulfillmentResponse fulfillmentResponse,
+                    @NonNull TouchEventMetadata touchEventMetadata) {
+                future.set(fulfillmentResponse);
+            }
+
+            @Override
+            public void onError(@NonNull ErrorStatusInternal errorStatus) {}
+        };
+    }
+
+    @AutoValue
+    public abstract static class TouchEventSuccessResult {
+        public static TouchEventSuccessResult create(
+                FulfillmentResponse fulfillmentResponse, TouchEventMetadata touchEventMetadata) {
+            return new AutoValue_TestingUtils_TouchEventSuccessResult(
+                    fulfillmentResponse, touchEventMetadata);
+        }
+
+        public abstract FulfillmentResponse fulfillmentResponse();
+
+        public abstract TouchEventMetadata touchEventMetadata();
+    }
+
+    @AutoOneOf(TouchEventResult.Kind.class)
+    public abstract static class TouchEventResult {
+        public static TouchEventResult of(TouchEventSuccessResult result) {
+            return AutoOneOf_TestingUtils_TouchEventResult.success(result);
+        }
+
+        public static TouchEventResult of(ErrorStatusInternal error) {
+            return AutoOneOf_TestingUtils_TouchEventResult.error(error);
+        }
+
+        public abstract Kind getKind();
+
+        public abstract TouchEventSuccessResult success();
+
+        public abstract ErrorStatusInternal error();
+
+        public enum Kind {
+            SUCCESS,
+            ERROR
+        }
+    }
+
+    public static class ReusableTouchEventCallback implements TouchEventCallback {
+
+        AtomicReference<TouchEventResult> mResultRef = new AtomicReference<>();
+
+        @Override
+        public void onSuccess(
+                @NonNull FulfillmentResponse fulfillmentResponse,
+                @NonNull TouchEventMetadata touchEventMetadata) {
+            mResultRef.set(
+                    TouchEventResult.of(
+                            TouchEventSuccessResult.create(
+                                    fulfillmentResponse, touchEventMetadata)));
+        }
+
+        @Override
+        public void onError(@NonNull ErrorStatusInternal errorStatus) {
+            mResultRef.set(TouchEventResult.of(errorStatus));
+        }
+
+        public TouchEventResult getLastResult() {
+            return mResultRef.get();
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Argument.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Argument.java
new file mode 100644
index 0000000..4e625f2
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Argument.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.testing.spec;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+import androidx.appactions.interaction.capabilities.core.values.EntityValue;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.List;
+import java.util.Optional;
+
+/** Testing implementation of a capability Argument. */
+@AutoValue
+public abstract class Argument {
+
+    public static Builder newBuilder() {
+        return new AutoValue_Argument.Builder();
+    }
+
+    public abstract Optional<EntityValue> requiredEntityField();
+
+    public abstract Optional<String> optionalStringField();
+
+    public abstract Optional<TestEnum> enumField();
+
+    public abstract Optional<List<String>> repeatedStringField();
+
+    /** Builder for the testing Argument. */
+    @AutoValue.Builder
+    public abstract static class Builder implements BuilderOf<Argument> {
+
+        public abstract Builder setRequiredEntityField(EntityValue value);
+
+        public abstract Builder setOptionalStringField(String value);
+
+        public abstract Builder setEnumField(TestEnum value);
+
+        public abstract Builder setRepeatedStringField(List<String> value);
+
+        @NonNull
+        @Override
+        public abstract Argument build();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityStructFill.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityStructFill.java
new file mode 100644
index 0000000..fc4b9b4
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityStructFill.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.testing.spec;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters;
+import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec;
+import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder;
+import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
+import androidx.appactions.interaction.capabilities.core.values.ListItem;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Optional;
+
+/** Used to test the filling behavior of structured entities (e.g. ListItem) */
+public final class CapabilityStructFill {
+
+    private static final String CAPABILITY_NAME = "actions.intent.TEST";
+    public static final ActionSpec<Property, Argument, Void> ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                    .setDescriptor(Property.class)
+                    .setArgument(Argument.class, Argument::newBuilder)
+                    .bindStructParameter(
+                            "listItem",
+                            Property::itemList,
+                            Argument.Builder::setListItem,
+                            TypeConverters::toListItem)
+                    .bindOptionalStringParameter(
+                            "string", Property::anyString, Argument.Builder::setAnyString)
+                    .build();
+
+    private CapabilityStructFill() {
+    }
+
+    /** Two required strings */
+    @AutoValue
+    public abstract static class Argument {
+        public static Builder newBuilder() {
+            return new AutoValue_CapabilityStructFill_Argument.Builder();
+        }
+
+        public abstract Optional<ListItem> listItem();
+
+        public abstract Optional<String> anyString();
+
+        /** Builder for the testing Argument. */
+        @AutoValue.Builder
+        public abstract static class Builder implements BuilderOf<Argument> {
+
+            public abstract Builder setListItem(@NonNull ListItem value);
+
+            public abstract Builder setAnyString(@NonNull String value);
+
+            @NonNull
+            @Override
+            public abstract Argument build();
+        }
+    }
+
+    /** Two required strings */
+    @AutoValue
+    public abstract static class Property {
+        @NonNull
+        public static Builder newBuilder() {
+            return new AutoValue_CapabilityStructFill_Property.Builder();
+        }
+
+        public abstract Optional<SimpleProperty> itemList();
+
+        public abstract Optional<StringProperty> anyString();
+
+        /** Builder for {@link Property} */
+        @AutoValue.Builder
+        public abstract static class Builder {
+
+            @NonNull
+            public abstract Builder setItemList(@NonNull SimpleProperty value);
+
+            @NonNull
+            public abstract Builder setAnyString(@NonNull StringProperty value);
+
+            @NonNull
+            public abstract Property build();
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityTwoEntityValues.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityTwoEntityValues.java
new file mode 100644
index 0000000..298a57f
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityTwoEntityValues.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.testing.spec;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec;
+import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder;
+import androidx.appactions.interaction.capabilities.core.properties.EntityProperty;
+import androidx.appactions.interaction.capabilities.core.values.EntityValue;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Optional;
+
+public final class CapabilityTwoEntityValues {
+
+    private static final String CAPABILITY_NAME = "actions.intent.TEST";
+    public static final ActionSpec<Property, Argument, Void> ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                    .setDescriptor(Property.class)
+                    .setArgument(Argument.class, Argument::newBuilder)
+                    .bindOptionalEntityParameter("slotA", Property::slotA,
+                            Argument.Builder::setSlotA)
+                    .bindOptionalEntityParameter("slotB", Property::slotB,
+                            Argument.Builder::setSlotB)
+                    .build();
+
+    private CapabilityTwoEntityValues() {
+    }
+
+    /** Two required strings */
+    @AutoValue
+    public abstract static class Argument {
+        public static Builder newBuilder() {
+            return new AutoValue_CapabilityTwoEntityValues_Argument.Builder();
+        }
+
+        public abstract Optional<EntityValue> slotA();
+
+        public abstract Optional<EntityValue> slotB();
+
+        /** Builder for the testing Argument. */
+        @AutoValue.Builder
+        public abstract static class Builder implements BuilderOf<Argument> {
+
+            public abstract Builder setSlotA(@NonNull EntityValue value);
+
+            public abstract Builder setSlotB(@NonNull EntityValue value);
+
+            @NonNull
+            @Override
+            public abstract Argument build();
+        }
+    }
+
+    /** Two required strings */
+    @AutoValue
+    public abstract static class Property {
+        @NonNull
+        public static Builder newBuilder() {
+            return new AutoValue_CapabilityTwoEntityValues_Property.Builder();
+        }
+
+        public abstract Optional<EntityProperty> slotA();
+
+        public abstract Optional<EntityProperty> slotB();
+
+        /** Builder for {@link Property} */
+        @AutoValue.Builder
+        public abstract static class Builder {
+
+            @NonNull
+            public abstract Builder setSlotA(@NonNull EntityProperty value);
+
+            @NonNull
+            public abstract Builder setSlotB(@NonNull EntityProperty value);
+
+            @NonNull
+            public abstract Property build();
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityTwoStrings.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityTwoStrings.java
new file mode 100644
index 0000000..f504c72
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityTwoStrings.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.testing.spec;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec;
+import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder;
+import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Optional;
+
+public final class CapabilityTwoStrings {
+
+    private static final String CAPABILITY_NAME = "actions.intent.TEST";
+    public static final ActionSpec<Property, Argument, Void> ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                    .setDescriptor(Property.class)
+                    .setArgument(Argument.class, Argument::newBuilder)
+                    .bindOptionalStringParameter(
+                            "stringSlotA", Property::stringSlotA, Argument.Builder::setStringSlotA)
+                    .bindOptionalStringParameter(
+                            "stringSlotB", Property::stringSlotB, Argument.Builder::setStringSlotB)
+                    .build();
+
+    private CapabilityTwoStrings() {
+    }
+
+    /** Two required strings */
+    @AutoValue
+    public abstract static class Argument {
+        public static Builder newBuilder() {
+            return new AutoValue_CapabilityTwoStrings_Argument.Builder();
+        }
+
+        public abstract Optional<String> stringSlotA();
+
+        public abstract Optional<String> stringSlotB();
+
+        /** Builder for the testing Argument. */
+        @AutoValue.Builder
+        public abstract static class Builder implements BuilderOf<Argument> {
+
+            public abstract Builder setStringSlotA(@NonNull String value);
+
+            public abstract Builder setStringSlotB(@NonNull String value);
+
+            @NonNull
+            @Override
+            public abstract Argument build();
+        }
+    }
+
+    /** Two required strings */
+    @AutoValue
+    public abstract static class Property {
+        @NonNull
+        public static Builder newBuilder() {
+            return new AutoValue_CapabilityTwoStrings_Property.Builder();
+        }
+
+        public abstract Optional<StringProperty> stringSlotA();
+
+        public abstract Optional<StringProperty> stringSlotB();
+
+        /** Builder for {@link Property} */
+        @AutoValue.Builder
+        public abstract static class Builder {
+
+            @NonNull
+            public abstract Builder setStringSlotA(@NonNull StringProperty value);
+
+            @NonNull
+            public abstract Builder setStringSlotB(@NonNull StringProperty value);
+
+            @NonNull
+            public abstract Property build();
+        }
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Confirmation.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Confirmation.java
new file mode 100644
index 0000000..491a282
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Confirmation.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.testing.spec;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Optional;
+
+/** Testing implementation of a capability Confirmation. */
+@AutoValue
+public abstract class Confirmation {
+
+    public static Builder builder() {
+        return new AutoValue_Confirmation.Builder();
+    }
+
+    public abstract Optional<String> optionalStringField();
+
+    @AutoValue.Builder
+    public abstract static class Builder {
+
+        public abstract Builder setOptionalStringField(String value);
+
+        public abstract Confirmation build();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Output.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Output.java
new file mode 100644
index 0000000..c18a61a
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Output.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.testing.spec;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.List;
+import java.util.Optional;
+
+/** Testing implementation of a capability Output. */
+@AutoValue
+public abstract class Output {
+
+    public static Builder builder() {
+        return new AutoValue_Output.Builder();
+    }
+
+    public abstract Optional<String> optionalStringField();
+
+    @SuppressWarnings("AutoValueImmutableFields")
+    public abstract List<String> repeatedStringField();
+
+    @AutoValue.Builder
+    public abstract static class Builder {
+
+        public abstract Builder setOptionalStringField(String value);
+
+        public abstract Builder setRepeatedStringField(List<String> value);
+
+        public abstract Output build();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Property.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Property.java
new file mode 100644
index 0000000..78d3e97
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Property.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.testing.spec;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+import androidx.appactions.interaction.capabilities.core.properties.EntityProperty;
+import androidx.appactions.interaction.capabilities.core.properties.EnumProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Optional;
+
+/** Testing implementation of a capability Property. */
+@AutoValue
+public abstract class Property {
+
+    public static Builder newBuilder() {
+        return new AutoValue_Property.Builder();
+    }
+
+    public abstract EntityProperty requiredEntityField();
+
+    public abstract Optional<StringProperty> optionalStringField();
+
+    public abstract Optional<EnumProperty<TestEnum>> enumField();
+
+    public abstract Optional<StringProperty> repeatedStringField();
+
+    /** Builder for the testing Property. */
+    @AutoValue.Builder
+    public abstract static class Builder implements BuilderOf<Property> {
+
+        public abstract Builder setRequiredEntityField(EntityProperty property);
+
+        public abstract Builder setOptionalStringField(StringProperty property);
+
+        public abstract Builder setEnumField(EnumProperty<TestEnum> property);
+
+        public abstract Builder setRepeatedStringField(StringProperty property);
+
+        @NonNull
+        @Override
+        public abstract Property build();
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/SettableFutureWrapper.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/SettableFutureWrapper.java
new file mode 100644
index 0000000..0c3d697
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/SettableFutureWrapper.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.testing.spec;
+
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/** Utility class to make working with callbacks in tests easier. */
+public final class SettableFutureWrapper<V> {
+    private final ListenableFuture<V> mFuture;
+    private CallbackToFutureAdapter.Completer<V> mCompleter;
+
+    public SettableFutureWrapper() {
+        this.mFuture =
+                CallbackToFutureAdapter.getFuture(
+                        completer -> {
+                            this.mCompleter = completer;
+                            return "SettableFutureWrapper";
+                        });
+    }
+
+    public ListenableFuture<V> getFuture() {
+        return mFuture;
+    }
+
+    public boolean set(V result) {
+        return mCompleter.set(result);
+    }
+
+    public boolean setException(Throwable t) {
+        return mCompleter.setException(t);
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/TestEntity.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/TestEntity.java
new file mode 100644
index 0000000..ee3db39
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/TestEntity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.testing.spec;
+
+import androidx.annotation.NonNull;
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+
+import com.google.auto.value.AutoValue;
+
+import java.time.Duration;
+import java.time.ZonedDateTime;
+import java.util.Optional;
+
+/** A test class for capability value. */
+@AutoValue
+public abstract class TestEntity {
+
+    public static Builder newBuilder() {
+        return new AutoValue_TestEntity.Builder();
+    }
+
+    public abstract Optional<String> getName();
+
+    public abstract Optional<Duration> getDuration();
+
+    public abstract Optional<ZonedDateTime> getZonedDateTime();
+
+    public abstract Optional<TestEnum> getEnum();
+
+    public abstract Optional<TestEntity> getEntity();
+
+    public enum TestEnum {
+        VALUE_1("value_1"),
+        VALUE_2("value_2");
+
+        private final String mStringValue;
+
+        TestEnum(String stringValue) {
+            this.mStringValue = stringValue;
+        }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return mStringValue;
+        }
+    }
+
+    @AutoValue.Builder
+    public abstract static class Builder implements BuilderOf<TestEntity> {
+
+        public abstract Builder setName(String name);
+
+        public abstract Builder setDuration(Duration duration);
+
+        public abstract Builder setZonedDateTime(ZonedDateTime date);
+
+        public abstract Builder setEnum(TestEnum enumValue);
+
+        public abstract Builder setEntity(TestEntity entity);
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/TestEnum.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/TestEnum.java
new file mode 100644
index 0000000..c325c57
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/TestEnum.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.testing.spec;
+
+/** Sample enum value for testing. */
+public enum TestEnum {
+    VALUE_1,
+    VALUE_2,
+}
diff --git a/appactions/interaction/interaction-proto/src/main/proto/app_actions_data.proto b/appactions/interaction/interaction-proto/src/main/proto/app_actions_data.proto
index 79367bd..6a4d67d 100644
--- a/appactions/interaction/interaction-proto/src/main/proto/app_actions_data.proto
+++ b/appactions/interaction/interaction-proto/src/main/proto/app_actions_data.proto
@@ -193,8 +193,11 @@
       // "textField.name".
       string name = 1;
 
+      // The updated values for this parameter.
+      repeated ParamValue values = 2 [deprecated = true];
+
       // The values and disambig data for this parameter.
-      repeated FulfillmentValue fulfillment_values = 2;
+      repeated FulfillmentValue fulfillment_values = 3;
     }
 
     repeated FulfillmentParam params = 3;
diff --git a/camera/camera-viewfinder/api/1.1.0-beta03.txt b/appactions/interaction/interaction-service/api/current.txt
similarity index 100%
copy from camera/camera-viewfinder/api/1.1.0-beta03.txt
copy to appactions/interaction/interaction-service/api/current.txt
diff --git a/camera/camera-viewfinder/api/1.1.0-beta03.txt b/appactions/interaction/interaction-service/api/public_plus_experimental_current.txt
similarity index 100%
copy from camera/camera-viewfinder/api/1.1.0-beta03.txt
copy to appactions/interaction/interaction-service/api/public_plus_experimental_current.txt
diff --git a/camera/camera-viewfinder/api/res-1.1.0-beta03.txt b/appactions/interaction/interaction-service/api/res-current.txt
similarity index 100%
copy from camera/camera-viewfinder/api/res-1.1.0-beta03.txt
copy to appactions/interaction/interaction-service/api/res-current.txt
diff --git a/camera/camera-viewfinder/api/1.1.0-beta03.txt b/appactions/interaction/interaction-service/api/restricted_current.txt
similarity index 100%
copy from camera/camera-viewfinder/api/1.1.0-beta03.txt
copy to appactions/interaction/interaction-service/api/restricted_current.txt
diff --git a/appactions/interaction/interaction-service/build.gradle b/appactions/interaction/interaction-service/build.gradle
new file mode 100644
index 0000000..d528b0e
--- /dev/null
+++ b/appactions/interaction/interaction-service/build.gradle
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("com.google.protobuf")
+}
+
+dependencies {
+    implementation(libs.protobufLite)
+    implementation("androidx.annotation:annotation:1.1.0")
+    implementation(project(":appactions:interaction:interaction-proto"))
+}
+
+protobuf {
+    protoc {
+        artifact = libs.protobufCompiler.get()
+    }
+    // Generates the java proto-lite code for the protos in this project. See
+    // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
+    // for more information.
+    generateProtoTasks {
+        all().each { task ->
+            task.builtins {
+                java {
+                    option "lite"
+                }
+            }
+        }
+    }
+}
+
+android {
+    namespace "androidx.appactions.interaction.service"
+}
+
+androidx {
+    name = "androidx.appactions.interaction:interaction-service"
+    type = LibraryType.PUBLISHED_LIBRARY
+    inceptionYear = "2023"
+    description = "Library for integrating with Google Assistant via GRPC binder channel."
+}
diff --git a/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/proto/package-info.java b/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/proto/package-info.java
new file mode 100644
index 0000000..93db9d1
--- /dev/null
+++ b/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/proto/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** @hide */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.appactions.interaction.service.proto;
+
+import androidx.annotation.RestrictTo;
diff --git a/appactions/interaction/interaction-service/src/main/proto/app_interaction_service.proto b/appactions/interaction/interaction-service/src/main/proto/app_interaction_service.proto
new file mode 100644
index 0000000..d62822a
--- /dev/null
+++ b/appactions/interaction/interaction-service/src/main/proto/app_interaction_service.proto
@@ -0,0 +1,264 @@
+/*
+ * 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.
+ */
+syntax = "proto3";
+
+package androidx.appactions.interaction.service.proto;
+
+import "app_actions_data.proto";
+import "grounding.proto";
+import "touch_event.proto";
+
+option java_package = "androidx.appactions.interaction.service.proto";
+option java_outer_classname = "AppActionsServiceProto";
+
+// Request from host client (Assistant) to service provider (app) as part of the
+// RequestFulfillment streaming call. Throughout a dialog facilitated by the
+// provider, host might send multiple Request every time the
+// user issues a query. This proto contains user's query (parsed into BII).
+// NEXT_ID: 3
+message Request {
+  // Session_id is used to identify the hosts. When there are multiple hosts
+  // connecting to the provider at the same time for different requests,
+  // provider can use the session_ids to distinguish sessions, thus
+  // providing different responses to different hosts.
+  int32 session_id = 1;
+  // FulfillmentRequest contains the request data sent from Assistant, such as
+  // new values of BII arguments.
+  .androidx.appactions.interaction.proto.FulfillmentRequest fulfillment_request = 2;
+}
+
+// Response from service provider (app) to host client (Assistant) as part of
+// the RequestFulfillment streaming call. While the RequestFulfillment streaming
+// call is not completed, provider can stream Response when
+// there is new TTS to play or new UI to display.
+// NEXT_ID: 7
+message Response {
+  // Response of the capability invocation. Contains single-turn information
+  // (such as structured data for TTS). State updates (e.g. slot value changes)
+  // are reflected in the app_actions_context).
+  .androidx.appactions.interaction.proto.FulfillmentResponse fulfillment_response = 1;
+  // Context from the app. This should invalidate the Assistant app cache.
+  .androidx.appactions.interaction.proto.AppActionsContext app_actions_context = 2;
+  // Signal to the host that there is a new UI (RemoteViews or Tiles) ready to
+  // be requested.
+  UiUpdate ui_update = 3;
+  // Signal to the host that there is a new UI (RemoteViews or Tiles) ready to
+  // be requested, specifically for a list view.
+  CollectionUpdate collection_update = 4;
+  Status ending_status = 5;
+  // Additional single-turn response data (in addition to
+  // 'fulfillment_response') when the capability processing was triggered via
+  // manual input.
+  .androidx.appactions.interaction.proto.TouchEventMetadata touch_event_metadata = 6;
+}
+
+// The ending status of the dialog.
+// NEXT_ID: 3
+message Status {
+  string message = 1;
+  Code status_code = 2;
+
+  enum Code {
+    CODE_UNSPECIFIED = 0;
+    COMPLETE = 1;
+    USER_CANCELED = 2;
+    ERROR = 3;
+    TIMEOUT = 4;
+  }
+}
+// NEXT_ID: 1
+message UiUpdate {}
+
+// NEXT_ID: 2
+message CollectionUpdate {
+  repeated int32 view_ids = 1;
+}
+
+// Host request to fetch UI
+// NEXT_ID: 2
+message UiRequest {
+  int32 session_id = 1;
+}
+
+// A wrapper for weartile Layout and Resources
+// NEXT_ID: 3
+message TileLayout {
+  // bytes for androidx.wear.tiles.proto.Layout
+  bytes layout = 1;
+  // bytes for androidx.wear.tiles.proto.Resources
+  bytes resources = 2;
+}
+
+// Information about the RemoteViews.
+// NEXT_ID: 5
+message RemoteViewsInfo {
+  oneof width {
+    // Fixed width in dp. Won't resize when host width changes.
+    float width_dp = 1;
+    // Take up as much horizontal space as possible, automatically resizes when
+    // host width changes
+    bool width_match_parent = 2;
+  }
+  oneof height {
+    // Fixed height in dp. Won't resize when host height changes.
+    float height_dp = 3;
+    // Take up as much vertical space as possible, automatically resizes when
+    // host height changes
+    bool height_match_parent = 4;
+  }
+}
+
+// Provider response to return UI. If the UI is RemoteViews, it will be attached
+// as trailer metadata of the response.
+// NEXT_ID: 3
+message UiResponse {
+  oneof ui_type {
+    TileLayout tile_layout = 1;
+    RemoteViewsInfo remote_views_info = 2;
+  }
+}
+
+// NEXT_ID: 5
+message HostProperties {
+  // The width of the host area (in dp) where the app content can be displayed.
+  float host_view_width_dp = 1;
+
+  // The height of the host area (in dp) where the app content can be displayed.
+  float host_view_height_dp = 2;
+
+  DeviceType device_type = 3;
+
+  bool requires_ui = 4;
+
+  enum ResponseType {
+    // default
+    TYPE_UNSPECIFIED = 0;
+
+    // The host supports displaying UI, text, and speech.
+    SPEECH_AND_UI = 1;
+
+    // The host supports playing TTS and receiving user voice query.
+    SPEECH_ONLY = 2;
+
+    // The host supports displaying a string text
+    UI_ONLY = 3;
+  }
+
+  enum DeviceType {
+    UNSPECIFIED = 0;   // default
+    MOBILE = 1;        // the host is an Android phone or tablet.
+    ANDROID_AUTO = 2;  // the host is an Android Auto device.
+    WEAR_OS = 3;       // The host is a WearOS device.
+    SPEAKER = 4;       // The host is a smart speaker.
+  }
+}
+
+// Request containing the specification of the host to prepare the session.
+// NEXT_ID: 4
+message StartSessionRequest {
+  HostProperties host_properties = 1;
+  string intent_name = 2;
+  string identifier = 3;
+}
+
+// Response providing a session_id. Session_id works like a cookie in browser.
+// It is used to identify a session.
+// NEXT_ID: 2
+message StartSessionResponse {
+  int32 session_id = 1;
+}
+
+// Request and response corresponding to methods on IRemoteViewsFactory.aidl
+// frameworks/base/core/java/com/android/internal/widget/IRemoteViewsFactory.aidl
+// NEXT_ID: 10
+message CollectionRequest {
+  int32 session_id = 1;
+  int32 view_id = 2;
+  oneof request_data {
+    OnDestroy on_destroy = 3;
+    GetCount get_count = 4;
+    GetViewAt get_view_at = 5;
+    GetLoadingView get_loading_view = 6;
+    GetViewTypeCount get_view_type_count = 7;
+    GetItemId get_item_id = 8;
+    HasStableIds has_stable_ids = 9;
+  }
+
+  message OnDestroy {}
+  message GetCount {}
+  message GetViewAt {
+    int32 position = 1;
+  }
+  message GetLoadingView {}
+  message GetViewTypeCount {}
+  message GetItemId {
+    int32 position = 1;
+  }
+  message HasStableIds {}
+}
+
+// NEXT_ID: 5
+message CollectionResponse {
+  oneof response_data {
+    GetCount get_count = 1;
+    GetViewTypeCount get_view_type_count = 2;
+    GetItemId get_item_id = 3;
+    HasStableIds has_stable_ids = 4;
+  }
+
+  message GetCount {
+    int32 count = 1;
+  }
+  message GetViewTypeCount {
+    int32 view_type_count = 1;
+  }
+  message GetItemId {
+    int64 item_id = 1;
+  }
+  message HasStableIds {
+    bool has_stable_ids = 1;
+  }
+}
+
+// Service between Assistant app and 3P app. The 3P app acts as the server and the Assistant app is
+// the client that binds to it. This GRPC service facilitates the communication with the 3P app.
+service AppActionsService {
+  // Start up a session.
+  rpc StartUpSession(stream StartSessionRequest)
+      returns (stream StartSessionResponse);
+
+  // Send request fulfillment.
+  rpc SendRequestFulfillment(Request) returns (Response);
+
+  // Request RemoteViews or TileLayout. This method is called after the
+  // AppInteractionService SDK returns a signal in the Response of
+  // RequestFulfillment. The Response.ui_update signal indicates that the app
+  // provider has requested to send UI to Assistant. RemoteViews are not
+  // directly sent in the response of this method, but passed in the gRPC
+  // 'metadata'
+  // (https://grpc.io/docs/what-is-grpc/core-concepts/#metadata).
+  // TileLayout is sent directly as protos in the UiResponse.
+  rpc RequestUi(UiRequest) returns (UiResponse);
+
+  // Request RemoteViews specifically regarding a collection view / list items.
+  // Similar to the RequestUI RPC, the response might contain RemoteViews in
+  // gRPC 'metadata'.
+  rpc RequestCollection(CollectionRequest) returns (CollectionResponse);
+
+  // Request grounded candidates from the app
+  rpc RequestGrounding(.androidx.appactions.interaction.proto.GroundingRequest)
+      returns (.androidx.appactions.interaction.proto.GroundingResponse);
+}
\ No newline at end of file
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/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/AppCompatVectorDrawableIntegrationTest.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/AppCompatVectorDrawableIntegrationTest.java
index 3ba67256..8ed530e 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/AppCompatVectorDrawableIntegrationTest.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/AppCompatVectorDrawableIntegrationTest.java
@@ -34,6 +34,7 @@
 import androidx.test.rule.ActivityTestRule;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -64,10 +65,15 @@
         mCanvas = new Canvas(mBitmap);
     }
 
+    @Ignore("b/266237884")
     @Test
     @UiThreadTest
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void testVectorDrawableAutoMirrored() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         Activity activity = mActivityTestRule.getActivity();
         ImageView view1 = (ImageView) activity.findViewById(R.id.view_vector_1);
         Drawable vectorDrawable = view1.getDrawable();
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/KeyboardShortcutsTestCaseWithToolbar.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/KeyboardShortcutsTestCaseWithToolbar.java
index 0b38fe6..f126833 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/KeyboardShortcutsTestCaseWithToolbar.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/KeyboardShortcutsTestCaseWithToolbar.java
@@ -47,12 +47,13 @@
 
     @Test
     @MediumTest
-    @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
+    @SdkSuppress(minSdkVersion = 24, // O+ uses navigation clusters for jumping to ActionBar.
+            maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void testAccessActionBar() throws Throwable {
-        // Since O, we rely on keyboard navigation clusters for jumping to actionbar
-        if (Build.VERSION.SDK_INT <= 25) {
-            return;
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
         }
+
         final BaseTestActivity activity = mActivityTestRule.getActivity();
 
         final View editText = activity.findViewById(androidx.appcompat.test.R.id.editText);
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSyncToFrameworkTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSyncToFrameworkTestCase.kt
index ac32070..536e283 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSyncToFrameworkTestCase.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSyncToFrameworkTestCase.kt
@@ -21,6 +21,7 @@
 import android.content.ComponentName
 import android.content.Intent
 import android.content.pm.PackageManager
+import android.os.Build
 import android.os.LocaleList
 import androidx.appcompat.testutils.LocalesActivityTestRule
 import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
@@ -77,6 +78,10 @@
 
     @Test
     fun testAutoSync_preTToPostT_syncsSuccessfully() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         val firstActivity = rule.activity
 
         // activity is following the system and the requested locales are null.
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateTestCase.kt
index a3addf3..d07329e 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateTestCase.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateTestCase.kt
@@ -16,6 +16,7 @@
 
 package androidx.appcompat.app
 
+import android.os.Build
 import android.util.LayoutDirection.RTL
 import android.webkit.WebView
 import androidx.appcompat.testutils.LocalesActivityTestRule
@@ -33,6 +34,7 @@
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNull
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 
@@ -117,10 +119,15 @@
         assertNull(activity.lastConfigurationChangeAndClear)
     }
 
+    @Ignore("b/262902574")
     @SdkSuppress(minSdkVersion = 17, maxSdkVersion = 33)
     @Test
     @FlakyTest(bugId = 255765202)
     fun testLayoutDirectionAfterRecreating() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         setLocalesAndWaitForRecreate(rule, getRTLLocaleList())
 
         // Now assert that the layoutDirection of decorView is RTL
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/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
index 47462dc..59bdf03 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
@@ -20,8 +20,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
+
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResultPage;
+import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.util.PrefixUtil;
 
 import com.google.android.icing.proto.DocumentProto;
@@ -48,10 +51,24 @@
                 .setUri(id)
                 .setNamespace(namespace)
                 .setSchema(schemaType);
-        SearchResultProto searchResultProto = SearchResultProto.newBuilder()
-                .addResults(SearchResultProto.ResultProto.newBuilder()
-                        .setDocument(documentProtoBuilder))
+
+        // A joined document
+        DocumentProto.Builder joinedDocProtoBuilder = DocumentProto.newBuilder()
+                .setUri("id2")
+                .setNamespace(namespace)
+                .setSchema(schemaType);
+
+        SearchResultProto.ResultProto joinedResultProto = SearchResultProto.ResultProto.newBuilder()
+                .setDocument(joinedDocProtoBuilder).build();
+
+        SearchResultProto.ResultProto resultProto = SearchResultProto.ResultProto.newBuilder()
+                .setDocument(documentProtoBuilder)
+                .addJoinedResults(joinedResultProto)
                 .build();
+
+        SearchResultProto searchResultProto = SearchResultProto.newBuilder()
+                .addResults(resultProto).build();
+
         SchemaTypeConfigProto schemaTypeConfigProto =
                 SchemaTypeConfigProto.newBuilder()
                         .setSchemaType(schemaType)
@@ -60,6 +77,7 @@
                 ImmutableMap.of(schemaType, schemaTypeConfigProto));
 
         removePrefixesFromDocument(documentProtoBuilder);
+        removePrefixesFromDocument(joinedDocProtoBuilder);
         SearchResultPage searchResultPage =
                 SearchResultToProtoConverter.toSearchResultPage(searchResultProto, schemaMap);
         assertThat(searchResultPage.getResults()).hasSize(1);
@@ -69,5 +87,62 @@
         assertThat(result.getGenericDocument()).isEqualTo(
                 GenericDocumentToProtoConverter.toGenericDocument(
                         documentProtoBuilder.build(), prefix, schemaMap.get(prefix)));
+
+        assertThat(result.getJoinedResults()).hasSize(1);
+        assertThat(result.getJoinedResults().get(0).getGenericDocument()).isEqualTo(
+                GenericDocumentToProtoConverter.toGenericDocument(
+                        joinedDocProtoBuilder.build(), prefix, schemaMap.get(prefix)));
+    }
+
+    @Test
+    public void testToSearchResultProtoWithDoublyNested() throws Exception {
+        final String prefix =
+                "com.package.foo" + PrefixUtil.PACKAGE_DELIMITER + "databaseName"
+                        + PrefixUtil.DATABASE_DELIMITER;
+        final String id = "id";
+        final String namespace = prefix + "namespace";
+        final String schemaType = prefix + "schema";
+
+        // Building the SearchResult received from query.
+        DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
+                .setUri(id)
+                .setNamespace(namespace)
+                .setSchema(schemaType);
+
+        // A joined document
+        DocumentProto.Builder joinedDocProtoBuilder = DocumentProto.newBuilder()
+                .setUri("id2")
+                .setNamespace(namespace)
+                .setSchema(schemaType);
+
+        SearchResultProto.ResultProto joinedResultProto = SearchResultProto.ResultProto.newBuilder()
+                .setDocument(joinedDocProtoBuilder).build();
+
+        SearchResultProto.ResultProto nestedJoinedResultProto = SearchResultProto.ResultProto
+                .newBuilder()
+                .setDocument(joinedDocProtoBuilder)
+                .addJoinedResults(joinedResultProto)
+                .build();
+
+        SearchResultProto.ResultProto resultProto = SearchResultProto.ResultProto.newBuilder()
+                .setDocument(documentProtoBuilder)
+                .addJoinedResults(nestedJoinedResultProto)
+                .build();
+
+        SearchResultProto searchResultProto = SearchResultProto.newBuilder()
+                .addResults(resultProto).build();
+
+        SchemaTypeConfigProto schemaTypeConfigProto =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType(schemaType)
+                        .build();
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(prefix,
+                ImmutableMap.of(schemaType, schemaTypeConfigProto));
+
+        removePrefixesFromDocument(documentProtoBuilder);
+        Exception e = assertThrows(AppSearchException.class, () ->
+                SearchResultToProtoConverter.toSearchResultPage(searchResultProto, schemaMap));
+        assertThat(e.getMessage())
+                .isEqualTo("Nesting joined results within joined results not allowed.");
     }
 }
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
index e5a509c..271bf2a 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
@@ -23,6 +23,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import androidx.appsearch.app.JoinSpec;
 import androidx.appsearch.app.SearchSpec;
 import androidx.appsearch.localstorage.AppSearchImpl;
 import androidx.appsearch.localstorage.OptimizeStrategy;
@@ -32,6 +33,7 @@
 import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
 import androidx.appsearch.testutil.AppSearchTestUtils;
 
+import com.google.android.icing.proto.JoinSpecProto;
 import com.google.android.icing.proto.PropertyWeight;
 import com.google.android.icing.proto.ResultSpecProto;
 import com.google.android.icing.proto.SchemaTypeConfigProto;
@@ -94,6 +96,73 @@
     }
 
     @Test
+    public void testToSearchSpecProtoWithJoinSpec() throws Exception {
+        SearchSpec nestedSearchSpec = new SearchSpec.Builder()
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP).build();
+        SearchSpec.Builder searchSpec = new SearchSpec.Builder()
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE);
+
+        // Create a JoinSpec object and set it in the converter
+        JoinSpec joinSpec = new JoinSpec.Builder("childPropertyExpression")
+                .setNestedSearch("nestedQuery", nestedSearchSpec)
+                .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_SUM_RANKING_SIGNAL)
+                .setMaxJoinedResultCount(10)
+                .build();
+
+        searchSpec.setJoinSpec(joinSpec);
+        String prefix1 = PrefixUtil.createPrefix("package", "database1");
+        String prefix2 = PrefixUtil.createPrefix("package", "database2");
+
+        SchemaTypeConfigProto configProto = SchemaTypeConfigProto.getDefaultInstance();
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
+                /*queryExpression=*/"query",
+                searchSpec.build(),
+                /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
+                /*namespaceMap=*/ImmutableMap.of(
+                prefix1, ImmutableSet.of(
+                        prefix1 + "namespace1",
+                        prefix1 + "namespace2"),
+                prefix2, ImmutableSet.of(
+                        prefix2 + "namespace1",
+                        prefix2 + "namespace2")),
+                /*schemaMap=*/ImmutableMap.of(
+                prefix1, ImmutableMap.of(
+                        prefix1 + "typeA", configProto,
+                        prefix1 + "typeB", configProto),
+                prefix2, ImmutableMap.of(
+                        prefix2 + "typeA", configProto,
+                        prefix2 + "typeB", configProto)));
+
+        // Convert SearchSpec to proto.
+        SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
+
+        assertThat(searchSpecProto.getQuery()).isEqualTo("query");
+        assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
+                "package$database1/typeA", "package$database1/typeB", "package$database2/typeA",
+                "package$database2/typeB");
+        assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly(
+                "package$database1/namespace1", "package$database1/namespace2",
+                "package$database2/namespace1", "package$database2/namespace2");
+
+        // Assert that the joinSpecProto is set correctly in the searchSpecProto
+        assertThat(searchSpecProto.hasJoinSpec()).isTrue();
+
+        JoinSpecProto joinSpecProto = searchSpecProto.getJoinSpec();
+        assertThat(joinSpecProto.hasNestedSpec()).isTrue();
+        assertThat(joinSpecProto.getParentPropertyExpression()).isEqualTo(JoinSpec.QUALIFIED_ID);
+        assertThat(joinSpecProto.getChildPropertyExpression()).isEqualTo("childPropertyExpression");
+        assertThat(joinSpecProto.getAggregationScoringStrategy())
+                .isEqualTo(JoinSpecProto.AggregationScoringStrategy.Code.SUM);
+        assertThat(joinSpecProto.getMaxJoinedChildCount()).isEqualTo(10);
+
+        JoinSpecProto.NestedSpecProto nestedSpecProto = joinSpecProto.getNestedSpec();
+        assertThat(nestedSpecProto.getSearchSpec().getQuery()).isEqualTo("nestedQuery");
+        assertThat(nestedSpecProto.getScoringSpec().getRankBy()).isEqualTo(
+                ScoringSpecProto.RankingStrategy.Code.CREATION_TIMESTAMP);
+    }
+
+
+    @Test
     public void testToScoringSpecProto() {
         String prefix = PrefixUtil.createPrefix("package", "database1");
         String schemaType = "schemaType";
@@ -324,10 +393,10 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(prefix1),
                 /*namespaceMap=*/ImmutableMap.of(
-                        prefix1, ImmutableSet.of("package$database1/namespace1",
-                                "package$database1/namespace2"),
-                        prefix2, ImmutableSet.of("package$database2/namespace3",
-                                "package$database2/namespace4")),
+                prefix1, ImmutableSet.of("package$database1/namespace1",
+                        "package$database1/namespace2"),
+                prefix2, ImmutableSet.of("package$database2/namespace3",
+                        "package$database2/namespace4")),
                 /*schemaMap=*/ImmutableMap.of());
 
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -348,8 +417,8 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(prefix1),
                 /*namespaceMap=*/ImmutableMap.of(
-                        prefix1, ImmutableSet.of("package$database1/namespace1",
-                                "package$database1/namespace2")),
+                prefix1, ImmutableSet.of("package$database1/namespace1",
+                        "package$database1/namespace2")),
                 /*schemaMap=*/ImmutableMap.of());
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         // If the searching namespace filter is not empty, the target namespace filter will be the
@@ -371,8 +440,8 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(prefix1),
                 /*namespaceMap=*/ImmutableMap.of(
-                        prefix1, ImmutableSet.of("package$database1/namespace1",
-                                "package$database1/namespace2")),
+                prefix1, ImmutableSet.of("package$database1/namespace1",
+                        "package$database1/namespace2")),
                 /*schemaMap=*/ImmutableMap.of());
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         // If the searching namespace filter is not empty, the target namespace filter will be the
@@ -393,14 +462,14 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
                 /*namespaceMap=*/ImmutableMap.of(
-                        prefix1, ImmutableSet.of("package$database1/namespace1")),
+                prefix1, ImmutableSet.of("package$database1/namespace1")),
                 /*schemaMap=*/ImmutableMap.of(
-                        prefix1, ImmutableMap.of(
-                                "package$database1/typeA", schemaTypeConfigProto,
-                                "package$database1/typeB", schemaTypeConfigProto),
-                        prefix2, ImmutableMap.of(
-                                "package$database2/typeC", schemaTypeConfigProto,
-                                "package$database2/typeD", schemaTypeConfigProto)));
+                prefix1, ImmutableMap.of(
+                        "package$database1/typeA", schemaTypeConfigProto,
+                        "package$database1/typeB", schemaTypeConfigProto),
+                prefix2, ImmutableMap.of(
+                        "package$database2/typeC", schemaTypeConfigProto,
+                        "package$database2/typeD", schemaTypeConfigProto)));
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         // Empty searching filter will get all types for target filter
         assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
@@ -421,14 +490,14 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(prefix1),
                 /*namespaceMap=*/ImmutableMap.of(
-                        prefix1, ImmutableSet.of("package$database1/namespace1")),
+                prefix1, ImmutableSet.of("package$database1/namespace1")),
                 /*schemaMap=*/ImmutableMap.of(
-                        prefix1, ImmutableMap.of(
-                                "package$database1/typeA", schemaTypeConfigProto,
-                                "package$database1/typeB", schemaTypeConfigProto),
-                        prefix2, ImmutableMap.of(
-                                "package$database2/typeC", schemaTypeConfigProto,
-                                "package$database2/typeD", schemaTypeConfigProto)));
+                prefix1, ImmutableMap.of(
+                        "package$database1/typeA", schemaTypeConfigProto,
+                        "package$database1/typeB", schemaTypeConfigProto),
+                prefix2, ImmutableMap.of(
+                        "package$database2/typeC", schemaTypeConfigProto,
+                        "package$database2/typeD", schemaTypeConfigProto)));
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         // Only search prefix1 will return typeA and B.
         assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
@@ -448,11 +517,11 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(prefix1),
                 /*namespaceMap=*/ImmutableMap.of(
-                        prefix1, ImmutableSet.of("package$database1/namespace1")),
+                prefix1, ImmutableSet.of("package$database1/namespace1")),
                 /*schemaMap=*/ImmutableMap.of(
-                        prefix1, ImmutableMap.of(
-                                "package$database1/typeA", schemaTypeConfigProto,
-                                "package$database1/typeB", schemaTypeConfigProto)));
+                prefix1, ImmutableMap.of(
+                        "package$database1/typeA", schemaTypeConfigProto,
+                        "package$database1/typeB", schemaTypeConfigProto)));
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         // If the searching schema filter is not empty, the target schema filter will be the
         // intersection of the schema filters that users want to search over and those candidates
@@ -474,11 +543,11 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(prefix1),
                 /*namespaceMap=*/ImmutableMap.of(
-                        prefix1, ImmutableSet.of("package$database1/namespace1")),
+                prefix1, ImmutableSet.of("package$database1/namespace1")),
                 /*schemaMap=*/ImmutableMap.of(
-                        prefix1, ImmutableMap.of(
-                                "package$database1/typeA", schemaTypeConfigProto,
-                                "package$database1/typeB", schemaTypeConfigProto)));
+                prefix1, ImmutableMap.of(
+                        "package$database1/typeA", schemaTypeConfigProto,
+                        "package$database1/typeB", schemaTypeConfigProto)));
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         // If there is no intersection of the schema filters that user want to search over and
         // those filters which are stored in AppSearch, return empty.
@@ -503,12 +572,12 @@
                 new SearchSpec.Builder().build(),
                 /*prefixes=*/ImmutableSet.of(prefix),
                 /*namespaceMap=*/ImmutableMap.of(
-                        prefix, ImmutableSet.of("package$database/namespace1")),
+                prefix, ImmutableSet.of("package$database/namespace1")),
                 /*schemaMap=*/ImmutableMap.of(
-                        prefix, ImmutableMap.of(
-                                "package$database/schema1", schemaTypeConfigProto,
-                                "package$database/schema2", schemaTypeConfigProto,
-                                "package$database/schema3", schemaTypeConfigProto)));
+                prefix, ImmutableMap.of(
+                        "package$database/schema1", schemaTypeConfigProto,
+                        "package$database/schema2", schemaTypeConfigProto,
+                        "package$database/schema3", schemaTypeConfigProto)));
 
         converter.removeInaccessibleSchemaFilter(
                 new CallerAccess(/*callingPackageName=*/"otherPackageName"),
@@ -648,11 +717,11 @@
                         /*queryExpression=*/"",
                         searchSpec, /*prefixes=*/ImmutableSet.of(prefix1),
                         /*namespaceMap=*/ImmutableMap.of(
-                            prefix1,
-                            ImmutableSet.of(prefix1 + "namespace1")),
+                        prefix1,
+                        ImmutableSet.of(prefix1 + "namespace1")),
                         /*schemaMap=*/ImmutableMap.of(
-                            prefix1,
-                            ImmutableMap.of(prefix1 + "typeA", schemaTypeConfigProto)));
+                        prefix1,
+                        ImmutableMap.of(prefix1 + "typeA", schemaTypeConfigProto)));
 
         ScoringSpecProto convertedScoringSpecProto = converter.toScoringSpecProto();
 
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-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
index e985c52..7a2d909 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
@@ -24,6 +24,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResultPage;
@@ -107,6 +108,16 @@
                 }
             }
         }
+        for (int i = 0; i < proto.getJoinedResultsCount(); i++) {
+            SearchResultProto.ResultProto joinedResultProto = proto.getJoinedResults(i);
+
+            if (joinedResultProto.getJoinedResultsCount() != 0) {
+                throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
+                        "Nesting joined results within joined results not allowed.");
+            }
+
+            builder.addJoinedResult(toUnprefixedSearchResult(joinedResultProto, schemaMap));
+        }
         return builder.build();
     }
 
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
index b7aef30..69019b5 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
@@ -25,6 +25,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.JoinSpec;
 import androidx.appsearch.app.SearchSpec;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
@@ -35,6 +36,7 @@
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
+import com.google.android.icing.proto.JoinSpecProto;
 import com.google.android.icing.proto.PropertyWeight;
 import com.google.android.icing.proto.ResultSpecProto;
 import com.google.android.icing.proto.SchemaTypeConfigProto;
@@ -73,6 +75,18 @@
     private final Set<String> mTargetPrefixedSchemaFilters;
 
     /**
+     * The cached Map of {@code <Prefix, Set<PrefixedNamespace>>} stores all prefixed namespace
+     * filters which are stored in AppSearch. This is a field so that we can generate nested protos.
+     */
+    private final Map<String, Set<String>> mNamespaceMap;
+    /**
+     *The cached Map of {@code <Prefix, Map<PrefixedSchemaType, schemaProto>>} stores all
+     * prefixed schema filters which are stored inAppSearch. This is a field so that we can
+     * generated nested protos.
+     */
+    private final Map<String, Map<String, SchemaTypeConfigProto>> mSchemaMap;
+
+    /**
      * Creates a {@link SearchSpecToProtoConverter} for given {@link SearchSpec}.
      *
      * @param queryExpression                Query String to search.
@@ -92,8 +106,8 @@
         mQueryExpression = Preconditions.checkNotNull(queryExpression);
         mSearchSpec = Preconditions.checkNotNull(searchSpec);
         mPrefixes = Preconditions.checkNotNull(prefixes);
-        Preconditions.checkNotNull(namespaceMap);
-        Preconditions.checkNotNull(schemaMap);
+        mNamespaceMap = Preconditions.checkNotNull(namespaceMap);
+        mSchemaMap = Preconditions.checkNotNull(schemaMap);
         mTargetPrefixedNamespaceFilters =
                 SearchSpecToProtoConverterUtil.generateTargetNamespaceFilters(
                         prefixes, namespaceMap, searchSpec.getFilterNamespaces());
@@ -167,10 +181,60 @@
         }
         protoBuilder.setTermMatchType(termMatchCodeProto);
 
+        JoinSpec joinSpec = mSearchSpec.getJoinSpec();
+        if (joinSpec != null) {
+            SearchSpecToProtoConverter nestedConverter = new SearchSpecToProtoConverter(
+                    joinSpec.getNestedQuery(), joinSpec.getNestedSearchSpec(), mPrefixes,
+                    mNamespaceMap, mSchemaMap);
+
+            JoinSpecProto.NestedSpecProto nestedSpec = JoinSpecProto.NestedSpecProto.newBuilder()
+                    .setResultSpec(nestedConverter.toResultSpecProto(mNamespaceMap))
+                    .setScoringSpec(nestedConverter.toScoringSpecProto())
+                    .setSearchSpec(nestedConverter.toSearchSpecProto())
+                    .build();
+
+            JoinSpecProto.Builder joinSpecProtoBuilder = JoinSpecProto.newBuilder()
+                    .setNestedSpec(nestedSpec)
+                    .setParentPropertyExpression(JoinSpec.QUALIFIED_ID)
+                    .setChildPropertyExpression(joinSpec.getChildPropertyExpression())
+                    .setAggregationScoringStrategy(
+                            toAggregationScoringStrategy(joinSpec.getAggregationScoringStrategy()))
+                    .setMaxJoinedChildCount(joinSpec.getMaxJoinedResultCount());
+
+            protoBuilder.setJoinSpec(joinSpecProtoBuilder);
+        }
+
         return protoBuilder.build();
     }
 
     /**
+     * Helper to convert to JoinSpecProto.AggregationScore.
+     *
+     * <p> {@link JoinSpec#AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL} will be treated as
+     * undefined, which is the default behavior.
+     *
+     * @param aggregationScoringStrategy the scoring strategy to convert.
+     */
+    @NonNull
+    public static JoinSpecProto.AggregationScoringStrategy.Code toAggregationScoringStrategy(
+            @JoinSpec.AggregationScoringStrategy int aggregationScoringStrategy) {
+        switch (aggregationScoringStrategy) {
+            case JoinSpec.AGGREGATION_SCORING_AVG_RANKING_SIGNAL:
+                return JoinSpecProto.AggregationScoringStrategy.Code.AVG;
+            case JoinSpec.AGGREGATION_SCORING_MIN_RANKING_SIGNAL:
+                return JoinSpecProto.AggregationScoringStrategy.Code.MIN;
+            case JoinSpec.AGGREGATION_SCORING_MAX_RANKING_SIGNAL:
+                return JoinSpecProto.AggregationScoringStrategy.Code.MAX;
+            case JoinSpec.AGGREGATION_SCORING_SUM_RANKING_SIGNAL:
+                return JoinSpecProto.AggregationScoringStrategy.Code.SUM;
+            case JoinSpec.AGGREGATION_SCORING_RESULT_COUNT:
+                return JoinSpecProto.AggregationScoringStrategy.Code.COUNT;
+            default:
+                return JoinSpecProto.AggregationScoringStrategy.Code.NONE;
+        }
+    }
+
+    /**
      * Extracts {@link ResultSpecProto} information from a {@link SearchSpec}.
      *
      * @param namespaceMap    The cached Map of {@code <Prefix, Set<PrefixedNamespace>>} stores
@@ -277,6 +341,8 @@
                 return ScoringSpecProto.RankingStrategy.Code.USAGE_TYPE2_LAST_USED_TIMESTAMP;
             case SearchSpec.RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION:
                 return ScoringSpecProto.RankingStrategy.Code.ADVANCED_SCORING_EXPRESSION;
+            case SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE:
+                return ScoringSpecProto.RankingStrategy.Code.JOIN_AGGREGATE_SCORE;
             default:
                 throw new IllegalArgumentException("Invalid result ranking strategy: "
                         + rankingStrategyCode);
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java
index e774332..6fde6d6 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java
@@ -63,6 +63,11 @@
     @NonNull
     @BuildCompat.PrereleaseSdkCheck
     public ListenableFuture<List<SearchResult>> getNextPageAsync() {
+        // TODO(b/256022027): add isAtLeastU check after Android U.
+        if (mSearchSpec.getJoinSpec() != null) {
+            throw new UnsupportedOperationException("Searching with a SearchSpec containing a "
+                    + "JoinSpec is not supported on this AppSearch implementation.");
+        }
         ResolvableFuture<List<SearchResult>> future = ResolvableFuture.create();
         mPlatformResults.getNextPage(mExecutor, result -> {
             if (result.isSuccess()) {
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/app/DocumentIdUtilCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/DocumentIdUtilCtsTest.java
deleted file mode 100644
index eda327d..0000000
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/DocumentIdUtilCtsTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appsearch.app;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.appsearch.util.DocumentIdUtil;
-
-import org.junit.Test;
-
-public class DocumentIdUtilCtsTest {
-
-    @Test
-    public void testQualifiedIdCreation() {
-        final String packageName = "pkg";
-
-        // The delimiter requires just a single backslash to escape it, represented by two in Java.
-        String qualifiedId = DocumentIdUtil.createQualifiedId(packageName, "data#base",
-                "name#space", "id#entifier");
-        assertThat(qualifiedId).isEqualTo("pkg$data\\#base/name\\#space#id\\#entifier");
-
-        // The raw namespace contains a backslash followed by a delimiter. As both the backslash
-        // and the delimiter are escaped, the result will have a backslash to escape the
-        // original backslash, the original backslash, and a backslash to escape the delimiter,
-        // and finally the delimiter. It will look like \\\#, which in Java is represented with six
-        // backslashes followed by a delimiter.
-        qualifiedId = DocumentIdUtil.createQualifiedId(packageName, "data\\#base",
-                "name\\#space", "id\\#entifier");
-        assertThat(qualifiedId).isEqualTo("pkg$data\\\\\\#base/name\\\\\\#space#id\\\\\\#entifier");
-
-        // Here, the four backlashes represent two backslashes, a backslash to escape the
-        // original backslash as well as the original backslash. The number of backslashes gets
-        // doubled in both the Java representation as well as the raw String.
-        qualifiedId = DocumentIdUtil.createQualifiedId(packageName, "data\\base",
-                "name\\space", "id\\entifier");
-        assertThat(qualifiedId).isEqualTo("pkg$data\\\\base/name\\\\space#id\\\\entifier");
-
-        qualifiedId = DocumentIdUtil.createQualifiedId(packageName, "data\\\\base",
-                "name\\\\space", "id\\\\entifier");
-        assertThat(qualifiedId)
-                .isEqualTo("pkg$data\\\\\\\\base/name\\\\\\\\space#id\\\\\\\\entifier");
-
-        qualifiedId = DocumentIdUtil.createQualifiedId(packageName, "data\\\\\\base",
-                "name\\\\\\space", "id\\\\\\entifier");
-        assertThat(qualifiedId)
-                .isEqualTo("pkg$data\\\\\\\\\\\\base/name\\\\\\\\\\\\space#id\\\\\\\\\\\\entifier");
-    }
-
-    @Test
-    public void testQualifiedIdFromDocument() {
-        final String packageName = "pkg";
-        final String databaseName = "database";
-
-        GenericDocument document = new GenericDocument.Builder<>("namespace", "id", "type").build();
-        String qualifiedId = DocumentIdUtil.createQualifiedId(packageName, databaseName, document);
-        assertThat(qualifiedId).isEqualTo("pkg$database/namespace#id");
-    }
-}
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 efb2c91..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
@@ -44,6 +44,7 @@
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.JoinSpec;
 import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.PropertyPath;
 import androidx.appsearch.app.PutDocumentsRequest;
@@ -59,6 +60,7 @@
 import androidx.appsearch.cts.app.customer.EmailDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.testutil.AppSearchEmail;
+import androidx.appsearch.util.DocumentIdUtil;
 import androidx.collection.ArrayMap;
 import androidx.test.core.app.ApplicationProvider;
 
@@ -3218,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(
@@ -3481,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.
@@ -3950,6 +3965,111 @@
         assertThat(resultsWithInvalidPath.get(0).getGenericDocument()).isEqualTo(email1);
     }
 
+    @Test
+    public void testSimpleJoin() throws Exception {
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+        // A full example of how join might be used
+        AppSearchSchema actionSchema = new AppSearchSchema.Builder("BookmarkAction")
+                .addProperty(new StringPropertyConfig.Builder("entityId")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setJoinableValueType(StringPropertyConfig
+                                .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("note")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
+                        .build()).get();
+
+        // Index a document
+        // While inEmail2 has a higher document score, we will rank based on the number of joined
+        // documents. inEmail1 will have 1 joined document while inEmail2 will have 0 joined
+        // documents.
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .setScore(1)
+                        .build();
+
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .setScore(10)
+                        .build();
+
+        String qualifiedId = DocumentIdUtil.createQualifiedId(mContext.getPackageName(), DB_NAME_1,
+                "namespace", "id1");
+        GenericDocument join = new GenericDocument.Builder<>("NS", "id3", "BookmarkAction")
+                .setPropertyString("entityId", qualifiedId)
+                .setPropertyString("note", "Hi this is a joined doc").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2, join)
+                        .build()));
+
+        SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();
+
+        JoinSpec js = new JoinSpec.Builder("entityId")
+                .setNestedSearch("", nestedSearchSpec)
+                .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+                .build();
+
+        SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
+                .setJoinSpec(js)
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+
+        List<SearchResult> sr = searchResults.getNextPageAsync().get();
+
+        // Both email docs are returned, but id1 comes first due to the join
+        assertThat(sr).hasSize(2);
+
+        assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id1");
+        assertThat(sr.get(0).getJoinedResults()).hasSize(1);
+        assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(join);
+        assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
+
+        assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id2");
+        assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0);
+        assertThat(sr.get(1).getJoinedResults()).isEmpty();
+    }
+
+    @Test
+    public void testJoinWithoutSupport() throws Exception {
+        assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+        SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();
+        JoinSpec js = new JoinSpec.Builder("entityId").setNestedSearch("", nestedSearchSpec)
+                .build();
+        SearchResults searchResults = mDb1.search("", new SearchSpec.Builder()
+                .setJoinSpec(js)
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+
+        Exception e = assertThrows(UnsupportedOperationException.class, () ->
+                searchResults.getNextPageAsync().get());
+        assertThat(e).isInstanceOf(UnsupportedOperationException.class);
+        assertThat(e.getMessage()).isEqualTo("Searching with a SearchSpec containing a JoinSpec "
+                + "is not supported on this AppSearch implementation.");
+    }
+
+    @Test
     public void testSearchSuggestion() throws Exception {
         // Schema registration
         AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/DocumentIdUtilCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/DocumentIdUtilCtsTest.java
new file mode 100644
index 0000000..89d2180
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/DocumentIdUtilCtsTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.cts.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.util.DocumentIdUtil;
+
+import org.junit.Test;
+
+public class DocumentIdUtilCtsTest {
+
+    @Test
+    public void testQualifiedIdCreation() {
+        final String packageName = "pkg";
+
+        // The delimiter requires just a single backslash to escape it, represented by two in Java.
+        String qualifiedId = DocumentIdUtil.createQualifiedId(packageName, "data#base",
+                "name#space", "id#entifier");
+        assertThat(qualifiedId).isEqualTo("pkg$data\\#base/name\\#space#id\\#entifier");
+
+        // The raw namespace contains a backslash followed by a delimiter. As both the backslash
+        // and the delimiter are escaped, the result will have a backslash to escape the
+        // original backslash, the original backslash, and a backslash to escape the delimiter,
+        // and finally the delimiter. It will look like \\\#, which in Java is represented with six
+        // backslashes followed by a delimiter.
+        qualifiedId = DocumentIdUtil.createQualifiedId(packageName, "data\\#base",
+                "name\\#space", "id\\#entifier");
+        assertThat(qualifiedId).isEqualTo("pkg$data\\\\\\#base/name\\\\\\#space#id\\\\\\#entifier");
+
+        // Here, the four backlashes represent two backslashes, a backslash to escape the
+        // original backslash as well as the original backslash. The number of backslashes gets
+        // doubled in both the Java representation as well as the raw String.
+        qualifiedId = DocumentIdUtil.createQualifiedId(packageName, "data\\base",
+                "name\\space", "id\\entifier");
+        assertThat(qualifiedId).isEqualTo("pkg$data\\\\base/name\\\\space#id\\\\entifier");
+
+        qualifiedId = DocumentIdUtil.createQualifiedId(packageName, "data\\\\base",
+                "name\\\\space", "id\\\\entifier");
+        assertThat(qualifiedId)
+                .isEqualTo("pkg$data\\\\\\\\base/name\\\\\\\\space#id\\\\\\\\entifier");
+
+        qualifiedId = DocumentIdUtil.createQualifiedId(packageName, "data\\\\\\base",
+                "name\\\\\\space", "id\\\\\\entifier");
+        assertThat(qualifiedId)
+                .isEqualTo("pkg$data\\\\\\\\\\\\base/name\\\\\\\\\\\\space#id\\\\\\\\\\\\entifier");
+    }
+
+    @Test
+    public void testQualifiedIdFromDocument() {
+        final String packageName = "pkg";
+        final String databaseName = "database";
+
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id", "type").build();
+        String qualifiedId = DocumentIdUtil.createQualifiedId(packageName, databaseName, document);
+        assertThat(qualifiedId).isEqualTo("pkg$database/namespace#id");
+    }
+}
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/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
index e25c3a9..6a2a8f8 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
@@ -286,6 +286,7 @@
                 .build();
 
         SearchSpec searchSpec = new SearchSpec.Builder()
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
                 .setJoinSpec(joinSpec)
                 .build();
 
@@ -450,6 +451,17 @@
 
         assertThat(e.getMessage()).isEqualTo("Attempting to rank based on joined documents, but"
                 + " no JoinSpec provided");
+
+        e = assertThrows(IllegalStateException.class, () -> new SearchSpec.Builder()
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP)
+                .setJoinSpec(new JoinSpec.Builder("childProp")
+                        .setAggregationScoringStrategy(
+                                JoinSpec.AGGREGATION_SCORING_SUM_RANKING_SIGNAL)
+                        .build())
+                .build());
+        assertThat(e.getMessage()).isEqualTo("Aggregate scoring strategy has been set in the "
+                + "nested JoinSpec, but ranking strategy is not "
+                + "RANKING_STRATEGY_JOIN_AGGREGATE_SCORE");
     }
 
     @Test
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 084653e..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) {
@@ -1237,12 +1241,22 @@
          * @throws IllegalStateException if the ranking strategy is
          * {@link #RANKING_STRATEGY_JOIN_AGGREGATE_SCORE} and {@link #setJoinSpec} has never been
          * called.
+         * @throws IllegalStateException if the aggregation scoring strategy has been set in
+         * {@link JoinSpec#getAggregationScoringStrategy()} but the ranking strategy is not
+         * {@link #RANKING_STRATEGY_JOIN_AGGREGATE_SCORE}.
          *
          */
         @NonNull
         public SearchSpec build() {
             Bundle bundle = new Bundle();
             if (mJoinSpec != null) {
+                if (mRankingStrategy != RANKING_STRATEGY_JOIN_AGGREGATE_SCORE
+                        && mJoinSpec.getAggregationScoringStrategy()
+                        != JoinSpec.AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL) {
+                    throw new IllegalStateException("Aggregate scoring strategy has been set in "
+                            + "the nested JoinSpec, but ranking strategy is not "
+                            + "RANKING_STRATEGY_JOIN_AGGREGATE_SCORE");
+                }
                 bundle.putBundle(JOIN_SPEC, mJoinSpec.getBundle());
             } else if (mRankingStrategy == RANKING_STRATEGY_JOIN_AGGREGATE_SCORE) {
                 throw new IllegalStateException("Attempting to rank based on joined documents, but "
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/arch/core/core-common/api/2.2.0-beta01.txt b/arch/core/core-common/api/2.2.0-beta01.txt
new file mode 100644
index 0000000..43568b1
--- /dev/null
+++ b/arch/core/core-common/api/2.2.0-beta01.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.arch.core.util {
+
+  public interface Function<I, O> {
+    method public O! apply(I!);
+  }
+
+}
+
diff --git a/arch/core/core-common/api/public_plus_experimental_2.2.0-beta01.txt b/arch/core/core-common/api/public_plus_experimental_2.2.0-beta01.txt
new file mode 100644
index 0000000..43568b1
--- /dev/null
+++ b/arch/core/core-common/api/public_plus_experimental_2.2.0-beta01.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.arch.core.util {
+
+  public interface Function<I, O> {
+    method public O! apply(I!);
+  }
+
+}
+
diff --git a/arch/core/core-common/api/restricted_2.2.0-beta01.txt b/arch/core/core-common/api/restricted_2.2.0-beta01.txt
new file mode 100644
index 0000000..4fbc435
--- /dev/null
+++ b/arch/core/core-common/api/restricted_2.2.0-beta01.txt
@@ -0,0 +1,41 @@
+// Signature format: 4.0
+package androidx.arch.core.internal {
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class FastSafeIterableMap<K, V> extends androidx.arch.core.internal.SafeIterableMap<K,V> {
+    ctor public FastSafeIterableMap();
+    method public java.util.Map.Entry<K!,V!>? ceil(K!);
+    method public boolean contains(K!);
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap<K, V> implements java.lang.Iterable<java.util.Map.Entry<K,V>> {
+    ctor public SafeIterableMap();
+    method public java.util.Iterator<java.util.Map.Entry<K!,V!>!> descendingIterator();
+    method public java.util.Map.Entry<K!,V!>? eldest();
+    method protected androidx.arch.core.internal.SafeIterableMap.Entry<K!,V!>? get(K!);
+    method public java.util.Iterator<java.util.Map.Entry<K!,V!>!> iterator();
+    method public androidx.arch.core.internal.SafeIterableMap.IteratorWithAdditions iteratorWithAdditions();
+    method public java.util.Map.Entry<K!,V!>? newest();
+    method public V! putIfAbsent(K, V);
+    method public V! remove(K);
+    method public int size();
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap.IteratorWithAdditions extends androidx.arch.core.internal.SafeIterableMap.SupportRemove<K,V> implements java.util.Iterator<java.util.Map.Entry<K,V>> {
+    method public boolean hasNext();
+    method public java.util.Map.Entry<K!,V!>! next();
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract static class SafeIterableMap.SupportRemove<K, V> {
+    ctor public SafeIterableMap.SupportRemove();
+  }
+
+}
+
+package androidx.arch.core.util {
+
+  public interface Function<I, O> {
+    method public O! apply(I!);
+  }
+
+}
+
diff --git a/arch/core/core-common/api/restricted_current.ignore b/arch/core/core-common/api/restricted_current.ignore
deleted file mode 100644
index 2afd054..0000000
--- a/arch/core/core-common/api/restricted_current.ignore
+++ /dev/null
@@ -1,3 +0,0 @@
-// Baseline format: 1.0
-RemovedMethod: androidx.arch.core.internal.SafeIterableMap#put(K, V):
-    Removed method androidx.arch.core.internal.SafeIterableMap.put(K,V)
diff --git a/camera/camera-viewfinder/api/1.1.0-beta03.txt b/arch/core/core-runtime/api/2.2.0-beta01.txt
similarity index 100%
rename from camera/camera-viewfinder/api/1.1.0-beta03.txt
rename to arch/core/core-runtime/api/2.2.0-beta01.txt
diff --git a/camera/camera-viewfinder/api/1.1.0-beta03.txt b/arch/core/core-runtime/api/public_plus_experimental_2.2.0-beta01.txt
similarity index 100%
copy from camera/camera-viewfinder/api/1.1.0-beta03.txt
copy to arch/core/core-runtime/api/public_plus_experimental_2.2.0-beta01.txt
diff --git a/camera/camera-viewfinder/api/res-1.2.0-beta01.txt b/arch/core/core-runtime/api/res-2.2.0-beta01.txt
similarity index 100%
rename from camera/camera-viewfinder/api/res-1.2.0-beta01.txt
rename to arch/core/core-runtime/api/res-2.2.0-beta01.txt
diff --git a/arch/core/core-runtime/api/restricted_2.2.0-beta01.txt b/arch/core/core-runtime/api/restricted_2.2.0-beta01.txt
new file mode 100644
index 0000000..9958884
--- /dev/null
+++ b/arch/core/core-runtime/api/restricted_2.2.0-beta01.txt
@@ -0,0 +1,30 @@
+// Signature format: 4.0
+package androidx.arch.core.executor {
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class ArchTaskExecutor extends androidx.arch.core.executor.TaskExecutor {
+    method public void executeOnDiskIO(Runnable);
+    method public static java.util.concurrent.Executor getIOThreadExecutor();
+    method public static androidx.arch.core.executor.ArchTaskExecutor getInstance();
+    method public static java.util.concurrent.Executor getMainThreadExecutor();
+    method public boolean isMainThread();
+    method public void postToMainThread(Runnable);
+    method public void setDelegate(androidx.arch.core.executor.TaskExecutor?);
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class DefaultTaskExecutor extends androidx.arch.core.executor.TaskExecutor {
+    ctor public DefaultTaskExecutor();
+    method public void executeOnDiskIO(Runnable);
+    method public boolean isMainThread();
+    method public void postToMainThread(Runnable);
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class TaskExecutor {
+    ctor public TaskExecutor();
+    method public abstract void executeOnDiskIO(Runnable);
+    method public void executeOnMainThread(Runnable);
+    method public abstract boolean isMainThread();
+    method public abstract void postToMainThread(Runnable);
+  }
+
+}
+
diff --git a/arch/core/core-testing/api/2.2.0-beta01.txt b/arch/core/core-testing/api/2.2.0-beta01.txt
new file mode 100644
index 0000000..0303b8a
--- /dev/null
+++ b/arch/core/core-testing/api/2.2.0-beta01.txt
@@ -0,0 +1,16 @@
+// Signature format: 4.0
+package androidx.arch.core.executor.testing {
+
+  public class CountingTaskExecutorRule extends org.junit.rules.TestWatcher {
+    ctor public CountingTaskExecutorRule();
+    method public void drainTasks(int, java.util.concurrent.TimeUnit) throws java.lang.InterruptedException, java.util.concurrent.TimeoutException;
+    method public boolean isIdle();
+    method protected void onIdle();
+  }
+
+  public class InstantTaskExecutorRule extends org.junit.rules.TestWatcher {
+    ctor public InstantTaskExecutorRule();
+  }
+
+}
+
diff --git a/arch/core/core-testing/api/public_plus_experimental_2.2.0-beta01.txt b/arch/core/core-testing/api/public_plus_experimental_2.2.0-beta01.txt
new file mode 100644
index 0000000..0303b8a
--- /dev/null
+++ b/arch/core/core-testing/api/public_plus_experimental_2.2.0-beta01.txt
@@ -0,0 +1,16 @@
+// Signature format: 4.0
+package androidx.arch.core.executor.testing {
+
+  public class CountingTaskExecutorRule extends org.junit.rules.TestWatcher {
+    ctor public CountingTaskExecutorRule();
+    method public void drainTasks(int, java.util.concurrent.TimeUnit) throws java.lang.InterruptedException, java.util.concurrent.TimeoutException;
+    method public boolean isIdle();
+    method protected void onIdle();
+  }
+
+  public class InstantTaskExecutorRule extends org.junit.rules.TestWatcher {
+    ctor public InstantTaskExecutorRule();
+  }
+
+}
+
diff --git a/camera/camera-viewfinder/api/res-1.2.0-beta01.txt b/arch/core/core-testing/api/res-2.2.0-beta01.txt
similarity index 100%
copy from camera/camera-viewfinder/api/res-1.2.0-beta01.txt
copy to arch/core/core-testing/api/res-2.2.0-beta01.txt
diff --git a/arch/core/core-testing/api/restricted_2.2.0-beta01.txt b/arch/core/core-testing/api/restricted_2.2.0-beta01.txt
new file mode 100644
index 0000000..8113a1d
--- /dev/null
+++ b/arch/core/core-testing/api/restricted_2.2.0-beta01.txt
@@ -0,0 +1,35 @@
+// Signature format: 4.0
+package androidx.arch.core.executor {
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class JunitTaskExecutorRule implements org.junit.rules.TestRule {
+    ctor public JunitTaskExecutorRule(int, boolean);
+    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement, org.junit.runner.Description);
+    method public void drainTasks(int) throws java.lang.InterruptedException;
+    method public androidx.arch.core.executor.TaskExecutor getTaskExecutor();
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class TaskExecutorWithFakeMainThread extends androidx.arch.core.executor.TaskExecutor {
+    ctor public TaskExecutorWithFakeMainThread(int);
+    method public void drainTasks(int) throws java.lang.InterruptedException;
+    method public void executeOnDiskIO(Runnable);
+    method public boolean isMainThread();
+    method public void postToMainThread(Runnable);
+  }
+
+}
+
+package androidx.arch.core.executor.testing {
+
+  public class CountingTaskExecutorRule extends org.junit.rules.TestWatcher {
+    ctor public CountingTaskExecutorRule();
+    method public void drainTasks(int, java.util.concurrent.TimeUnit) throws java.lang.InterruptedException, java.util.concurrent.TimeoutException;
+    method public boolean isIdle();
+    method protected void onIdle();
+  }
+
+  public class InstantTaskExecutorRule extends org.junit.rules.TestWatcher {
+    ctor public InstantTaskExecutorRule();
+  }
+
+}
+
diff --git a/benchmark/baseline-profiles-gradle-plugin/build.gradle b/benchmark/baseline-profiles-gradle-plugin/build.gradle
new file mode 100644
index 0000000..a3cfd33
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/build.gradle
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+import androidx.build.*
+
+plugins {
+    id("AndroidXPlugin")
+    id("kotlin")
+    id("java-gradle-plugin")
+}
+
+dependencies {
+    implementation(gradleApi())
+    implementation(libs.androidGradlePluginz)
+    implementation(libs.kotlinGradlePluginz)
+    implementation(libs.kotlinStdlib)
+    implementation(libs.protobuf)
+    implementation(libs.agpTestingPlatformCoreProto)
+
+    testImplementation(gradleTestKit())
+    testImplementation(project(":internal-testutils-gradle-plugin"))
+    testImplementation(libs.junit)
+    testImplementation(libs.kotlinTest)
+    testImplementation(libs.truth)
+}
+
+SdkResourceGenerator.generateForHostTest(project)
+
+gradlePlugin {
+    plugins {
+        baselineProfilesProducer {
+            id = "androidx.baselineprofiles.producer"
+            implementationClass = "androidx.baselineprofiles.gradle.producer.BaselineProfilesProducerPlugin"
+        }
+        baselineProfilesConsumer {
+            id = "androidx.baselineprofiles.consumer"
+            implementationClass = "androidx.baselineprofiles.gradle.consumer.BaselineProfilesConsumerPlugin"
+        }
+        baselineProfilesBuildProvider {
+            id = "androidx.baselineprofiles.buildprovider"
+            implementationClass = "androidx.baselineprofiles.gradle.buildprovider.BaselineProfilesBuildProviderPlugin"
+        }
+    }
+}
+
+androidx {
+    name = "Android Baseline Profiles Gradle Plugin"
+    publish = Publish.SNAPSHOT_ONLY
+    type = LibraryType.GRADLE_PLUGIN
+    inceptionYear = "2022"
+    description = "Android Baseline Profiles Gradle Plugin"
+}
+
+tasks {
+    validatePlugins {
+        failOnWarning.set(true)
+        enableStricterValidation.set(true)
+    }
+}
\ No newline at end of file
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/buildprovider/BaselineProfilesBuildProviderPlugin.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/buildprovider/BaselineProfilesBuildProviderPlugin.kt
new file mode 100644
index 0000000..9f0df32
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/buildprovider/BaselineProfilesBuildProviderPlugin.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.baselineprofiles.gradle.buildprovider
+
+import androidx.baselineprofiles.gradle.utils.createNonObfuscatedBuildTypes
+import com.android.build.api.variant.ApplicationAndroidComponentsExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+
+/**
+ * This is the build provider plugin for baseline profile generation. In order to generate baseline
+ * profiles three plugins are needed: one is applied to the app or the library that should consume
+ * the baseline profile when building (consumer), one is applied to the project that should supply
+ * the test apk (build provider) and the last one is applied to a library module containing the ui
+ * test that generate the baseline profile on the device (producer).
+ *
+ * TODO (b/265438721): build provider should be changed to apk provider.
+ */
+class BaselineProfilesBuildProviderPlugin : Plugin<Project> {
+
+    override fun apply(project: Project) {
+        project.pluginManager.withPlugin("com.android.application") {
+            configureWithAndroidPlugin(project = project)
+        }
+    }
+
+    private fun configureWithAndroidPlugin(project: Project) {
+
+        // Create the non obfuscated release build types from the existing release ones.
+        // We want to extend all the current release build types based on isDebuggable flag.
+        project
+            .extensions
+            .getByType(ApplicationAndroidComponentsExtension::class.java)
+            .finalizeDsl { applicationExtension ->
+
+                val debugBuildType = applicationExtension.buildTypes.getByName("debug")
+                createNonObfuscatedBuildTypes(
+                    project = project,
+                    extension = applicationExtension,
+                    extendedBuildTypeToOriginalBuildTypeMapping = mutableMapOf(),
+                    filterBlock = { !it.isDebuggable },
+                    configureBlock = {
+                        isJniDebuggable = false
+                        isDebuggable = false
+                        isMinifyEnabled = false
+                        isShrinkResources = false
+                        isProfileable = true
+                        signingConfig = debugBuildType.signingConfig
+                        enableAndroidTestCoverage = false
+                        enableUnitTestCoverage = false
+                    }
+                )
+            }
+    }
+}
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/consumer/BaselineProfilesConsumerExtension.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/consumer/BaselineProfilesConsumerExtension.kt
new file mode 100644
index 0000000..79a547b
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/consumer/BaselineProfilesConsumerExtension.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.baselineprofiles.gradle.consumer
+
+import org.gradle.api.Project
+
+/**
+ * Allows specifying settings for the Baseline Profiles Plugin.
+ */
+open class BaselineProfilesConsumerExtension {
+
+    companion object {
+
+        private const val EXTENSION_NAME = "baselineProfilesProfileConsumer"
+
+        internal fun registerExtension(project: Project): BaselineProfilesConsumerExtension {
+            val ext = project.extensions.findByType(BaselineProfilesConsumerExtension::class.java)
+            if (ext != null) {
+                return ext
+            }
+            return project
+                .extensions.create(EXTENSION_NAME, BaselineProfilesConsumerExtension::class.java)
+        }
+    }
+
+    /**
+     * Specifies what build type should be used to generate baseline profiles. By default this build
+     * type is `release`. In general, this should be a build type used for distribution. Note that
+     * this will be deprecated when b/265438201 is fixed, as all the build types will be used to
+     * generate baseline profiles.
+     */
+    var buildTypeName: String = "release"
+}
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/consumer/BaselineProfilesConsumerPlugin.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/consumer/BaselineProfilesConsumerPlugin.kt
new file mode 100644
index 0000000..06b97d9
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/consumer/BaselineProfilesConsumerPlugin.kt
@@ -0,0 +1,241 @@
+/*
+ * 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.baselineprofiles.gradle.consumer
+
+import androidx.baselineprofiles.gradle.utils.ATTRIBUTE_BUILD_TYPE
+import androidx.baselineprofiles.gradle.utils.ATTRIBUTE_CATEGORY_BASELINE_PROFILE
+import androidx.baselineprofiles.gradle.utils.ATTRIBUTE_FLAVOR
+import androidx.baselineprofiles.gradle.utils.CONFIGURATION_NAME_BASELINE_PROFILES
+import androidx.baselineprofiles.gradle.utils.camelCase
+import com.android.build.api.variant.AndroidComponentsExtension
+import com.android.build.gradle.AppExtension
+import com.android.build.gradle.LibraryExtension
+import com.android.build.gradle.TestedExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.attributes.Category
+import org.gradle.api.tasks.StopExecutionException
+
+/**
+ * This is the consumer plugin for baseline profile generation. In order to generate baseline
+ * profiles three plugins are needed: one is applied to the app or the library that should consume
+ * the baseline profile when building (consumer), one is applied to the project that should supply
+ * the apk under test (build provider) and the last one is applied to a library module containing
+ * the ui test that generate the baseline profile on the device (producer).
+ */
+class BaselineProfilesConsumerPlugin : Plugin<Project> {
+
+    companion object {
+
+        // The output file for the HRF baseline profile file in `src/main`
+        private const val BASELINE_PROFILE_SRC_MAIN_FILENAME = "baseline-prof.txt"
+    }
+
+    override fun apply(project: Project) {
+        project.pluginManager.withPlugin("com.android.application") {
+            configureWithAndroidPlugin(project = project, isApplication = true)
+        }
+        project.pluginManager.withPlugin("com.android.library") {
+            configureWithAndroidPlugin(project = project, isApplication = false)
+        }
+    }
+
+    private fun configureWithAndroidPlugin(project: Project, isApplication: Boolean) {
+
+        // TODO (b/259737859): This code will be updated to use source sets for baseline profiles,
+        //  as soon androidx repo is updated to use AGP 8.0-beta01.
+
+        val androidComponent = project.extensions.getByType(
+            AndroidComponentsExtension::class.java
+        )
+
+        val baselineProfilesExtension = BaselineProfilesConsumerExtension.registerExtension(project)
+
+        // Creates all the configurations, one per variant.
+        // Note that for this version of the plugin is not possible to rely entirely on the variant
+        // api so the actual creation of the tasks is postponed to be executed when all the
+        // agp tasks have been created, using the old api.
+        val mainBaselineProfileConfiguration = createBaselineProfileConfigurationForVariant(
+            project,
+            variantName = "",
+            flavorName = "",
+            buildTypeName = "",
+            mainConfiguration = null
+        )
+        val baselineProfileConfigurations = mutableListOf<Configuration>()
+        val baselineProfileVariantNames = mutableListOf<String>()
+        androidComponent.apply {
+            onVariants {
+
+                // Only create configurations for the build type expressed in the baseline profiles
+                // extension. Note that this can be removed after b/265438201.
+                if (it.buildType != baselineProfilesExtension.buildTypeName) {
+                    return@onVariants
+                }
+
+                baselineProfileConfigurations.add(
+                    createBaselineProfileConfigurationForVariant(
+                        project,
+                        variantName = it.name,
+                        flavorName = it.flavorName ?: "",
+                        buildTypeName = it.buildType ?: "",
+                        mainConfiguration = mainBaselineProfileConfiguration
+                    )
+                )
+
+                // Save this variant name so later we can use it to set a dependency on the
+                // merge/prepare art profile task for it.
+                baselineProfileVariantNames.add(it.name)
+            }
+        }
+
+        // Now that the configurations are created, the tasks can be created. The consumer plugin
+        // can only be applied to either applications or libraries.
+        // Note that for this plugin does not use the new variant api as it tries to access to some
+        // AGP tasks that don't yet exist in the new variant api callback (b/262007432).
+        val extensionVariants =
+            when (val tested = project.extensions.getByType(TestedExtension::class.java)) {
+                is AppExtension -> tested.applicationVariants
+                is LibraryExtension -> tested.libraryVariants
+                else -> throw StopExecutionException(
+                    """
+                Unrecognized extension: $tested not of type AppExtension or LibraryExtension.
+                """.trimIndent()
+                )
+            }
+
+        // After variants have been resolved and the AGP tasks have been created add the plugin tasks.
+        var applied = false
+        extensionVariants.all {
+            if (applied) return@all
+            applied = true
+
+            // Currently the plugin does not support generating a baseline profile for a specific
+            // flavor: all the flavors are merged into one and copied in src/main/baseline-prof.txt.
+            // This can be changed after b/239659205 when baseline profiles become a source set.
+            val mergeBaselineProfilesTaskProvider = project.tasks.register(
+                "generateBaselineProfiles", MergeBaselineProfileTask::class.java
+            ) { task ->
+
+                // These are all the configurations this task depends on, in order to consume their
+                // artifacts.
+                task.baselineProfileFileCollection.setFrom(baselineProfileConfigurations)
+
+                // This is the output file where all the configurations will be merged in.
+                // Note that this file is overwritten.
+                task.baselineProfileFile.set(
+                    project
+                        .layout
+                        .projectDirectory
+                        .file("src/main/$BASELINE_PROFILE_SRC_MAIN_FILENAME")
+                )
+            }
+
+            // If this is an application the mergeBaselineProfilesTask must run before the
+            // tasks that handle the baseline profile packaging. Merge for applications, prepare
+            // for libraries. Note that this will change with AGP 8.0 that should support
+            // source sets for baseline profiles.
+            for (variantName in baselineProfileVariantNames) {
+                val taskProvider = if (isApplication) {
+                    project.tasks.named(camelCase("merge", variantName, "artProfile"))
+                } else {
+                    project.tasks.named(camelCase("prepare", variantName, "artProfile"))
+                }
+                taskProvider.configure { it.mustRunAfter(mergeBaselineProfilesTaskProvider) }
+            }
+        }
+    }
+
+    private fun createBaselineProfileConfigurationForVariant(
+        project: Project,
+        variantName: String,
+        flavorName: String,
+        buildTypeName: String,
+        mainConfiguration: Configuration?
+    ): Configuration {
+
+        val buildTypeConfiguration =
+            if (buildTypeName.isNotBlank() && buildTypeName != variantName) {
+                project
+                    .configurations
+                    .maybeCreate(
+                        camelCase(
+                            buildTypeName,
+                            CONFIGURATION_NAME_BASELINE_PROFILES
+                        )
+                    )
+                    .apply {
+                        if (mainConfiguration != null) extendsFrom(mainConfiguration)
+                        isCanBeResolved = true
+                        isCanBeConsumed = false
+                    }
+            } else null
+
+        val flavorConfiguration = if (flavorName.isNotBlank() && flavorName != variantName) {
+            project
+                .configurations
+                .maybeCreate(camelCase(flavorName, CONFIGURATION_NAME_BASELINE_PROFILES))
+                .apply {
+                    if (mainConfiguration != null) extendsFrom(mainConfiguration)
+                    isCanBeResolved = true
+                    isCanBeConsumed = false
+                }
+        } else null
+
+        return project
+            .configurations
+            .maybeCreate(camelCase(variantName, CONFIGURATION_NAME_BASELINE_PROFILES))
+            .apply {
+
+                // The variant specific configuration always extends from build type and flavor
+                // configurations, when existing.
+                val extendFrom = mutableListOf<Configuration>()
+                if (mainConfiguration != null) {
+                    extendFrom.add(mainConfiguration)
+                }
+                if (flavorConfiguration != null) {
+                    extendFrom.add(flavorConfiguration)
+                }
+                if (buildTypeConfiguration != null) {
+                    extendFrom.add(buildTypeConfiguration)
+                }
+                setExtendsFrom(extendFrom)
+
+                isCanBeResolved = true
+                isCanBeConsumed = false
+
+                attributes {
+                    it.attribute(
+                        Category.CATEGORY_ATTRIBUTE,
+                        project.objects.named(
+                            Category::class.java,
+                            ATTRIBUTE_CATEGORY_BASELINE_PROFILE
+                        )
+                    )
+                    it.attribute(
+                        ATTRIBUTE_BUILD_TYPE,
+                        buildTypeName
+                    )
+                    it.attribute(
+                        ATTRIBUTE_FLAVOR,
+                        flavorName
+                    )
+                }
+            }
+    }
+}
\ No newline at end of file
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/consumer/MergeBaselineProfileTask.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/consumer/MergeBaselineProfileTask.kt
new file mode 100644
index 0000000..2277e63
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/consumer/MergeBaselineProfileTask.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.baselineprofiles.gradle.consumer
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+
+/**
+ * Collects all the baseline profile artifacts generated by all the producer configurations and
+ * merges them into one, sorting and ensuring that there are no duplicated lines.
+ *
+ * The format of the profile is a simple list of classes and methods loaded in memory when
+ * executing a test, expressed in JVM format. Duplicates can arise when multiple tests cover the
+ * same code: for example when having 2 tests both covering the startup path and then doing
+ * something else, both will have startup classes and methods. There is no harm in having this
+ * duplication but mostly the profile file will be unnecessarily larger.
+ */
+@CacheableTask
+abstract class MergeBaselineProfileTask : DefaultTask() {
+
+    @get:InputFiles
+    @get:PathSensitive(PathSensitivity.NONE)
+    abstract val baselineProfileFileCollection: ConfigurableFileCollection
+
+    @get:OutputFile
+    abstract val baselineProfileFile: RegularFileProperty
+
+    init {
+        group = "Baseline Profiles"
+        description = "Merges all the baseline profiles into one, removing duplicate lines."
+    }
+
+    @TaskAction
+    fun exec() {
+        val lines = baselineProfileFileCollection.files
+            .flatMap { it.readLines() }
+            .sorted()
+            .distinct()
+
+        baselineProfileFile.get().asFile.writeText(lines.joinToString(System.lineSeparator()))
+    }
+}
\ No newline at end of file
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/producer/BaselineProfilesProducerExtension.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/producer/BaselineProfilesProducerExtension.kt
new file mode 100644
index 0000000..e1222bf
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/producer/BaselineProfilesProducerExtension.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.baselineprofiles.gradle.producer
+
+import org.gradle.api.Project
+
+/**
+ * Allows specifying settings for the Baseline Profiles Plugin.
+ */
+open class BaselineProfilesProducerExtension {
+
+    companion object {
+
+        private const val EXTENSION_NAME = "baselineProfilesProfileProducer"
+
+        internal fun registerExtension(project: Project): BaselineProfilesProducerExtension {
+            val ext = project
+                .extensions.findByType(BaselineProfilesProducerExtension::class.java)
+            if (ext != null) {
+                return ext
+            }
+            return project
+                .extensions.create(EXTENSION_NAME, BaselineProfilesProducerExtension::class.java)
+        }
+    }
+
+    /**
+     * Allows selecting the managed devices to use for generating baseline profiles.
+     * This should be a list of strings contained the names of the devices specified in the
+     * configuration for managed devices. For example, in the following configuration, the name
+     * is `pixel6Api31`.
+     * ```
+     *  testOptions.managedDevices.devices {
+     *      pixel6Api31(ManagedVirtualDevice) {
+     *          device = "Pixel 6"
+     *          apiLevel = 31
+     *          systemImageSource = "aosp"
+     *      }
+     *  }
+     * ```
+     */
+    var managedDevices = mutableListOf<String>()
+
+    /**
+     * Whether baseline profiles should be generated on connected devices.
+     */
+    var useConnectedDevices: Boolean = true
+}
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/producer/BaselineProfilesProducerPlugin.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/producer/BaselineProfilesProducerPlugin.kt
new file mode 100644
index 0000000..0f60d71
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/producer/BaselineProfilesProducerPlugin.kt
@@ -0,0 +1,286 @@
+/*
+ * 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.baselineprofiles.gradle.producer
+
+import androidx.baselineprofiles.gradle.utils.ATTRIBUTE_BUILD_TYPE
+import androidx.baselineprofiles.gradle.utils.ATTRIBUTE_CATEGORY_BASELINE_PROFILE
+import androidx.baselineprofiles.gradle.utils.ATTRIBUTE_FLAVOR
+import androidx.baselineprofiles.gradle.utils.BUILD_TYPE_BASELINE_PROFILE_PREFIX
+import androidx.baselineprofiles.gradle.utils.CONFIGURATION_NAME_BASELINE_PROFILES
+import androidx.baselineprofiles.gradle.utils.camelCase
+import androidx.baselineprofiles.gradle.utils.createBuildTypeIfNotExists
+import androidx.baselineprofiles.gradle.utils.createNonObfuscatedBuildTypes
+import com.android.build.api.variant.TestAndroidComponentsExtension
+import com.android.build.gradle.TestExtension
+import org.gradle.api.GradleException
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.UnknownTaskException
+import org.gradle.api.attributes.Category
+
+/**
+ * This is the producer plugin for baseline profile generation. In order to generate baseline
+ * profiles three plugins are needed: one is applied to the app or the library that should consume
+ * the baseline profile when building (consumer), one is applied to the project that should supply
+ * the apk under test (build provider) and the last one is applied to a library module containing
+ * the ui test that generate the baseline profile on the device (producer).
+ */
+class BaselineProfilesProducerPlugin : Plugin<Project> {
+
+    override fun apply(project: Project) {
+        project.pluginManager.withPlugin("com.android.test") {
+            configureWithAndroidPlugin(project = project)
+        }
+    }
+
+    private fun configureWithAndroidPlugin(project: Project) {
+
+        // Prepares extensions used by the plugin
+        val baselineProfilesExtension =
+            BaselineProfilesProducerExtension.registerExtension(project)
+
+        val testAndroidComponent = project.extensions.getByType(
+            TestAndroidComponentsExtension::class.java
+        )
+
+        // We need the instrumentation apk to run as a separate process
+        val testExtension = project.extensions.getByType(TestExtension::class.java)
+        testExtension.experimentalProperties["android.experimental.self-instrumenting"] = true
+
+        // Creates the new build types to match the build provider. Note that release does not
+        // exist by default so we need to create nonObfuscatedRelease and map it manually to
+        // `release`. All the existing build types beside `debug`, that is the default one, are
+        // added manually in the configuration so we can assume they've been added for the purpose
+        // of generating baseline profiles. We don't need to create a nonObfuscated build type from
+        // `debug`.
+
+        val nonObfuscatedReleaseName = camelCase(BUILD_TYPE_BASELINE_PROFILE_PREFIX, "release")
+        val extendedTypeToOriginalTypeMapping = mutableMapOf(nonObfuscatedReleaseName to "release")
+
+        testAndroidComponent.finalizeDsl { ext ->
+
+            createNonObfuscatedBuildTypes(
+                project = project,
+                extension = ext,
+                extendedBuildTypeToOriginalBuildTypeMapping = extendedTypeToOriginalTypeMapping,
+                filterBlock = {
+                    // TODO: Which build types to skip. In theory we want to skip only debug because
+                    //  it's the default one. All the ones that have been manually added should be
+                    //  considered for this.
+                    it.name != "debug"
+                },
+                configureBlock = {
+                    enableAndroidTestCoverage = false
+                    enableUnitTestCoverage = false
+                },
+            )
+
+            createBuildTypeIfNotExists(
+                project = project,
+                extension = ext,
+                buildTypeName = nonObfuscatedReleaseName,
+                configureBlock = {
+                    enableAndroidTestCoverage = false
+                    enableUnitTestCoverage = false
+                    matchingFallbacks += listOf("release")
+                }
+            )
+        }
+
+        // Makes sure that only the non obfuscated build type variant selected is enabled
+        testAndroidComponent.apply {
+            beforeVariants {
+                it.enable = it.buildType in extendedTypeToOriginalTypeMapping.keys
+            }
+        }
+
+        // Creates all the configurations, one per variant for the newly created build type.
+        // Note that for this version of the plugin is not possible to rely entirely on the variant
+        // api so the actual creation of the tasks is postponed to be executed when all the
+        // agp tasks have been created, using the old api.
+        val createTaskBlocks = mutableListOf<() -> (Unit)>()
+        testAndroidComponent.apply {
+
+            onVariants {
+
+                // Creating configurations only for the extended build types.
+                if (it.buildType == null ||
+                    it.buildType !in extendedTypeToOriginalTypeMapping.keys) {
+                    return@onVariants
+                }
+
+                // Creates the configuration to handle this variant. Note that in the attributes
+                // to match the configuration we use the original build type without `nonObfuscated`.
+                val originalBuildTypeName = extendedTypeToOriginalTypeMapping[it.buildType] ?: ""
+                val configurationName = createBaselineProfileConfigurationForVariant(
+                    project = project,
+                    variantName = it.name,
+                    flavorName = it.flavorName ?: "",
+                    originalBuildTypeName = originalBuildTypeName
+                )
+
+                // Prepares a block to execute later that creates the tasks for this variant
+                createTaskBlocks.add {
+                    createTasksForVariant(
+                        project = project,
+                        variantName = it.name,
+                        flavorName = it.flavorName ?: "",
+                        configurationName = configurationName,
+                        baselineProfilesExtension = baselineProfilesExtension
+                    )
+                }
+            }
+        }
+
+        // After variants have been resolved and the AGP tasks have been created, create the plugin
+        // tasks.
+        var applied = false
+        testExtension.applicationVariants.all {
+            if (applied) return@all
+            applied = true
+            createTaskBlocks.forEach { it() }
+        }
+    }
+
+    private fun createTasksForVariant(
+        project: Project,
+        variantName: String,
+        flavorName: String,
+        configurationName: String,
+        baselineProfilesExtension: BaselineProfilesProducerExtension
+    ) {
+
+        // Prepares the devices list to use to generate baseline profiles.
+        val devices = mutableSetOf<String>()
+            .also { it.addAll(baselineProfilesExtension.managedDevices) }
+        if (baselineProfilesExtension.useConnectedDevices) {
+            devices.add("connected")
+        }
+
+        // Determines which test tasks should run based on configuration
+        val shouldExpectConnectedOutput = devices.contains("connected")
+        val shouldExpectManagedOutput = baselineProfilesExtension.managedDevices.isNotEmpty()
+
+        // The test task runs the ui tests
+        val testTasks = devices.map {
+            try {
+                project.tasks.named(camelCase(it, variantName, "androidTest"))
+            } catch (e: UnknownTaskException) {
+                throw GradleException(
+                    """
+                    It wasn't possible to determine the test task for managed device `$it`.
+                    Please check the managed devices specified in the baseline profiles configuration.
+                """.trimIndent(), e
+                )
+            }
+        }
+
+        // Merge result protos task
+        val mergeResultProtosTask = project.tasks.named(
+            camelCase("merge", variantName, "testResultProtos")
+        )
+
+        // The collect task collects the baseline profile files from the ui test results
+        val collectTaskProvider = project.tasks.register(
+            camelCase("collect", variantName, "BaselineProfiles"),
+            CollectBaselineProfilesTask::class.java
+        ) {
+
+            // Test tasks have to run before collect
+            it.dependsOn(testTasks, mergeResultProtosTask)
+
+            // Sets flavor name
+            it.outputFile.set(
+                project
+                    .layout
+                    .buildDirectory
+                    .file("intermediates/baselineprofiles/$flavorName/baseline-prof.txt")
+            )
+
+            // Sets the connected test results location, if tests are supposed to run also on
+            // connected devices.
+            if (shouldExpectConnectedOutput) {
+                it.connectedAndroidTestOutputDir.set(
+                    if (flavorName.isEmpty()) {
+                        project.layout.buildDirectory
+                            .dir("outputs/androidTest-results/connected")
+                    } else {
+                        project.layout.buildDirectory
+                            .dir("outputs/androidTest-results/connected/flavors/$flavorName")
+                    }
+                )
+            }
+
+            // Sets the managed devices test results location, if tests are supposed to run
+            // also on managed devices.
+            if (shouldExpectManagedOutput) {
+                it.managedAndroidTestOutputDir.set(
+                    if (flavorName.isEmpty()) {
+                        project.layout.buildDirectory.dir(
+                            "outputs/androidTest-results/managedDevice"
+                        )
+                    } else {
+                        project.layout.buildDirectory.dir(
+                            "outputs/androidTest-results/managedDevice/flavors/$flavorName"
+                        )
+                    }
+                )
+            }
+        }
+
+        // The artifacts are added to the configuration that exposes the generated baseline profile
+        project.artifacts { artifactHandler ->
+            artifactHandler.add(configurationName, collectTaskProvider) { artifact ->
+                artifact.builtBy(collectTaskProvider)
+            }
+        }
+    }
+
+    private fun createBaselineProfileConfigurationForVariant(
+        project: Project,
+        variantName: String,
+        flavorName: String,
+        originalBuildTypeName: String,
+    ): String {
+        val configurationName =
+            camelCase(variantName, CONFIGURATION_NAME_BASELINE_PROFILES)
+        project.configurations
+            .maybeCreate(configurationName)
+            .apply {
+                isCanBeResolved = false
+                isCanBeConsumed = true
+                attributes {
+                    it.attribute(
+                        Category.CATEGORY_ATTRIBUTE,
+                        project.objects.named(
+                            Category::class.java,
+                            ATTRIBUTE_CATEGORY_BASELINE_PROFILE
+                        )
+                    )
+                    it.attribute(
+                        ATTRIBUTE_BUILD_TYPE,
+                        originalBuildTypeName
+                    )
+                    it.attribute(
+                        ATTRIBUTE_FLAVOR,
+                        flavorName
+                    )
+                }
+            }
+        return configurationName
+    }
+}
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/producer/CollectBaselineProfilesTask.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/producer/CollectBaselineProfilesTask.kt
new file mode 100644
index 0000000..400554d
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/producer/CollectBaselineProfilesTask.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.baselineprofiles.gradle.producer
+
+import com.google.testing.platform.proto.api.core.TestSuiteResultProto
+import java.io.File
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.work.DisableCachingByDefault
+
+/**
+ * Collects the generated baseline profiles from the instrumentation results of a previous run of
+ * the ui tests.
+ */
+@DisableCachingByDefault(because = "Not worth caching.")
+abstract class CollectBaselineProfilesTask : DefaultTask() {
+
+    @get:Optional
+    @get:InputDirectory
+    @get:PathSensitive(PathSensitivity.RELATIVE)
+    abstract val connectedAndroidTestOutputDir: DirectoryProperty
+
+    @get:Optional
+    @get:InputDirectory
+    @get:PathSensitive(PathSensitivity.RELATIVE)
+    abstract val managedAndroidTestOutputDir: DirectoryProperty
+
+    @get:OutputFile
+    abstract val outputFile: RegularFileProperty
+
+    init {
+        group = "Baseline Profiles"
+        description = "Collects baseline profiles previously generated through integration tests."
+    }
+
+    @TaskAction
+    fun exec() {
+
+        // Prepares list with test results to read
+        val testResultProtoFiles =
+            listOf(connectedAndroidTestOutputDir, managedAndroidTestOutputDir)
+                .filter { it.isPresent }
+                .map { it.file("test-result.pb").get().asFile }
+
+        // A test-result.pb file must exist as output of connected and managed device tests.
+        // If it doesn't exist it's because there were no tests to run. If there are no devices,
+        // the test task will simply fail. The following check is to give a meaningful error
+        // message if something like that happens.
+        if (testResultProtoFiles.filter { !it.exists() }.isNotEmpty()) {
+            throw GradleException(
+                """
+                Expected test results were not found. This is most likely because there are no
+                tests to run. Please check that there are ui tests to execute. You can find more
+                information at https://d.android.com/studio/test/advanced-test-setup. To create a
+                baseline profile test instead, please check the documentation at
+                https://d.android.com/baselineprofiles.
+                """.trimIndent()
+            )
+        }
+
+        val profiles = mutableSetOf<String>()
+        testResultProtoFiles
+            .map { TestSuiteResultProto.TestSuiteResult.parseFrom(it.readBytes()) }
+            .forEach { testSuiteResult ->
+                for (testResult in testSuiteResult.testResultList) {
+
+                    // Baseline profile files are extracted by the test task. Here we find their
+                    // location checking the test-result.pb proto. Note that the BaselineProfileRule
+                    // produces one baseline profile file per test.
+                    val baselineProfileFiles = testResult.outputArtifactList
+                        .filter {
+                            // The label for this artifact is `additionaltestoutput.benchmark.trace`
+                            // https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:utp/android-test-plugin-host-additional-test-output/src/main/java/com/android/tools/utp/plugins/host/additionaltestoutput/AndroidAdditionalTestOutputPlugin.kt;l=199?q=additionaltestoutput.benchmark.trace
+                            it.label.label == "additionaltestoutput.benchmark.trace" &&
+                                "-baseline-prof-" in it.sourcePath.path
+                        }
+                        .map { File(it.sourcePath.path) }
+                    if (baselineProfileFiles.isEmpty()) {
+                        continue
+                    }
+
+                    // Merge each baseline profile file from the test results into the aggregated
+                    // baseline file, removing duplicate lines.
+                    for (baselineProfileFile in baselineProfileFiles) {
+                        profiles.addAll(baselineProfileFile.readLines())
+                    }
+                }
+            }
+
+        if (profiles.isEmpty()) {
+            throw GradleException("No baseline profiles found in test outputs.")
+        }
+
+        // Saves the merged baseline profile file in the final destination
+        val file = outputFile.get().asFile
+        file.writeText(profiles.joinToString(System.lineSeparator()))
+        logger.info("Aggregated baseline profile generated at ${file.absolutePath}")
+    }
+}
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/utils/BuildTypes.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/utils/BuildTypes.kt
new file mode 100644
index 0000000..81dfc24
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/utils/BuildTypes.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.baselineprofiles.gradle.utils
+
+import com.android.build.api.dsl.BuildType
+import com.android.build.api.dsl.CommonExtension
+import org.gradle.api.GradleException
+import org.gradle.api.Project
+
+internal inline fun <reified T : BuildType> createNonObfuscatedBuildTypes(
+    project: Project,
+    extension: CommonExtension<*, T, *, *>,
+    crossinline filterBlock: (T) -> (Boolean),
+    crossinline configureBlock: T.() -> (Unit),
+    extendedBuildTypeToOriginalBuildTypeMapping: MutableMap<String, String>
+) {
+    extension.buildTypes
+        .filter { buildType ->
+            if (buildType !is T) {
+                throw GradleException(
+                    "Build type `${buildType.name}` is not of type ${T::class}"
+                )
+            }
+            filterBlock(buildType)
+        }
+        .forEach { buildType ->
+
+            val newBuildTypeName = camelCase(BUILD_TYPE_BASELINE_PROFILE_PREFIX, buildType.name)
+
+            // Check in case the build type was created manually (to allow full customization)
+            if (extension.buildTypes.findByName(newBuildTypeName) != null) {
+                project.logger.info(
+                    "Build type $newBuildTypeName won't be created because already exists."
+                )
+            } else {
+                // If the new build type doesn't exist, create it simply extending the configured
+                // one (by default release).
+                extension.buildTypes.create(newBuildTypeName).apply {
+                    initWith(buildType)
+                    matchingFallbacks += listOf(buildType.name)
+                    configureBlock(this as T)
+                }
+            }
+            // Mapping the build type to the newly created
+            extendedBuildTypeToOriginalBuildTypeMapping[newBuildTypeName] = buildType.name
+        }
+}
+
+internal inline fun <reified T : BuildType> createBuildTypeIfNotExists(
+    project: Project,
+    extension: CommonExtension<*, T, *, *>,
+    buildTypeName: String,
+    configureBlock: BuildType.() -> Unit
+) {
+    // Check in case the build type was created manually (to allow full customization)
+    if (extension.buildTypes.findByName(buildTypeName) != null) {
+        project.logger.info(
+            "Build type $buildTypeName won't be created because already exists."
+        )
+        return
+    }
+    // If the new build type doesn't exist, create it simply extending the configured
+    // one (by default release).
+    extension.buildTypes.create(buildTypeName).apply {
+        configureBlock(this)
+    }
+}
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/utils/Utils.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/utils/Utils.kt
new file mode 100644
index 0000000..6af0e36
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/utils/Utils.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.baselineprofiles.gradle.utils
+
+import org.gradle.api.attributes.Attribute
+import org.gradle.configurationcache.extensions.capitalized
+
+internal fun camelCase(vararg strings: String): String {
+    if (strings.isEmpty()) return ""
+    return StringBuilder()
+        .apply {
+            var shouldCapitalize = false
+            for (str in strings.filter { it.isNotBlank() }) {
+                append(if (shouldCapitalize) str.capitalized() else str)
+                shouldCapitalize = true
+            }
+        }.toString()
+}
+
+// Prefix for the build type baseline profiles
+internal const val BUILD_TYPE_BASELINE_PROFILE_PREFIX = "nonObfuscated"
+
+// Configuration consumed by this plugin that carries the baseline profile HRF file.
+internal const val CONFIGURATION_NAME_BASELINE_PROFILES = "baselineprofiles"
+
+// Custom category attribute to match the baseline profile configuration
+internal const val ATTRIBUTE_CATEGORY_BASELINE_PROFILE = "baselineprofile"
+
+internal val ATTRIBUTE_FLAVOR =
+    Attribute.of("androidx.baselineprofiles.gradle.attributes.Flavor", String::class.java)
+internal val ATTRIBUTE_BUILD_TYPE =
+    Attribute.of("androidx.baselineprofiles.gradle.attributes.BuildType", String::class.java)
\ No newline at end of file
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/resources/META-INF/gradle-plugins/androidx.baselineprofiles.buildprovider.gradle.properties b/benchmark/baseline-profiles-gradle-plugin/src/main/resources/META-INF/gradle-plugins/androidx.baselineprofiles.buildprovider.gradle.properties
new file mode 100644
index 0000000..863bea9
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/resources/META-INF/gradle-plugins/androidx.baselineprofiles.buildprovider.gradle.properties
@@ -0,0 +1,16 @@
+#
+# 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.
+#
+implementation-class=androidx.baselineprofiles.gradle.buildprovider.BaselineProfilesBuildProviderPlugin
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/resources/META-INF/gradle-plugins/androidx.baselineprofiles.consumer.gradle.properties b/benchmark/baseline-profiles-gradle-plugin/src/main/resources/META-INF/gradle-plugins/androidx.baselineprofiles.consumer.gradle.properties
new file mode 100644
index 0000000..46d0a3e
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/resources/META-INF/gradle-plugins/androidx.baselineprofiles.consumer.gradle.properties
@@ -0,0 +1,16 @@
+#
+# 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.
+#
+implementation-class=androidx.baselineprofiles.gradle.consumer.BaselineProfilesConsumerPlugin
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/resources/META-INF/gradle-plugins/androidx.baselineprofiles.producer.gradle.properties b/benchmark/baseline-profiles-gradle-plugin/src/main/resources/META-INF/gradle-plugins/androidx.baselineprofiles.producer.gradle.properties
new file mode 100644
index 0000000..a7c0b60
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/resources/META-INF/gradle-plugins/androidx.baselineprofiles.producer.gradle.properties
@@ -0,0 +1,16 @@
+#
+# 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.
+#
+implementation-class=androidx.baselineprofiles.gradle.producer.BaselineProfilesProducerPlugin
\ No newline at end of file
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/test/kotlin/androidx/baselineprofiles/gradle/buildprovider/BaselineProfilesBuildProviderPluginTest.kt b/benchmark/baseline-profiles-gradle-plugin/src/test/kotlin/androidx/baselineprofiles/gradle/buildprovider/BaselineProfilesBuildProviderPluginTest.kt
new file mode 100644
index 0000000..4c75385
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/test/kotlin/androidx/baselineprofiles/gradle/buildprovider/BaselineProfilesBuildProviderPluginTest.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.baselineprofiles.gradle.buildprovider
+
+import androidx.testutils.gradle.ProjectSetupRule
+import com.google.common.truth.Truth.assertThat
+import org.gradle.testkit.runner.GradleRunner
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class BaselineProfilesBuildProviderPluginTest {
+
+    @get:Rule
+    val projectSetup = ProjectSetupRule()
+
+    private lateinit var gradleRunner: GradleRunner
+
+    @Before
+    fun setUp() {
+        gradleRunner = GradleRunner.create()
+            .withProjectDir(projectSetup.rootDir)
+            .withPluginClasspath()
+    }
+
+    @Test
+    fun verifyBuildType() {
+        projectSetup.writeDefaultBuildGradle(
+            prefix = """
+                plugins {
+                    id("com.android.application")
+                    id("androidx.baselineprofiles.buildprovider")
+                }
+                android {
+                    namespace 'com.example.namespace'
+                }
+                tasks.register("printNonObfuscatedReleaseBuildType") {
+                    println(android.buildTypes.nonObfuscatedRelease)
+                }
+            """.trimIndent(),
+            suffix = ""
+        )
+
+        val buildTypeProperties = gradleRunner
+            .withArguments("printNonObfuscatedReleaseBuildType", "--stacktrace")
+            .build()
+            .output
+
+        assertThat(buildTypeProperties).contains("minifyEnabled=false")
+        assertThat(buildTypeProperties).contains("testCoverageEnabled=false")
+        assertThat(buildTypeProperties).contains("debuggable=false")
+    }
+}
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/test/kotlin/androidx/baselineprofiles/gradle/consumer/BaselineProfilesConsumerPluginTest.kt b/benchmark/baseline-profiles-gradle-plugin/src/test/kotlin/androidx/baselineprofiles/gradle/consumer/BaselineProfilesConsumerPluginTest.kt
new file mode 100644
index 0000000..6490dbb
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/test/kotlin/androidx/baselineprofiles/gradle/consumer/BaselineProfilesConsumerPluginTest.kt
@@ -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.baselineprofiles.gradle.consumer
+
+import androidx.baselineprofiles.gradle.utils.CONFIGURATION_NAME_BASELINE_PROFILES
+import androidx.baselineprofiles.gradle.utils.camelCase
+import androidx.testutils.gradle.ProjectSetupRule
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import org.gradle.testkit.runner.GradleRunner
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class BaselineProfilesConsumerPluginTest {
+
+    // To test the consumer plugin we need a module that exposes a baselineprofiles configuration
+    // to be consumed. This is why we'll be using 2 projects. The producer project build gradle
+    // is generated ad hoc in the tests that require it in order to supply mock profiles.
+
+    private val rootFolder = TemporaryFolder().also { it.create() }
+
+    @get:Rule
+    val consumerProjectSetup = ProjectSetupRule(rootFolder.root)
+
+    @get:Rule
+    val producerProjectSetup = ProjectSetupRule(rootFolder.root)
+
+    private lateinit var consumerModuleName: String
+    private lateinit var producerModuleName: String
+    private lateinit var gradleRunner: GradleRunner
+
+    @Before
+    fun setUp() {
+        consumerModuleName = consumerProjectSetup.rootDir.relativeTo(rootFolder.root).name
+        producerModuleName = producerProjectSetup.rootDir.relativeTo(rootFolder.root).name
+
+        rootFolder.newFile("settings.gradle").writeText(
+            """
+            include '$consumerModuleName'
+            include '$producerModuleName'
+        """.trimIndent()
+        )
+        gradleRunner = GradleRunner.create()
+            .withProjectDir(consumerProjectSetup.rootDir)
+            .withPluginClasspath()
+    }
+
+    @Test
+    fun testGenerateBaselineProfilesTaskWithNoFlavors() {
+        consumerProjectSetup.writeDefaultBuildGradle(
+            prefix = """
+                plugins {
+                    id("com.android.library")
+                    id("androidx.baselineprofiles.consumer")
+                }
+                android {
+                    namespace 'com.example.namespace'
+                }
+                dependencies {
+                    baselineprofiles(project(":$producerModuleName"))
+                }
+            """.trimIndent(),
+            suffix = ""
+        )
+        producerProjectSetup.writeDefaultBuildGradle(
+            prefix = MockProducerBuildGrade()
+                .withConfiguration(flavor = "", buildType = "release")
+                .withProducedBaselineProfiles(listOf("3", "2"), flavor = "", buildType = "release")
+                .withProducedBaselineProfiles(listOf("4", "1"), flavor = "", buildType = "release")
+                .build(),
+            suffix = ""
+        )
+
+        gradleRunner
+            .withArguments("generateBaselineProfiles", "--stacktrace")
+            .build()
+
+        // The expected output should have each line sorted descending
+        assertThat(
+            File(consumerProjectSetup.rootDir, "src/main/baseline-prof.txt").readLines()
+        )
+            .containsExactly("4", "3", "2", "1")
+    }
+
+    @Test
+    fun testGenerateBaselineProfilesTaskWithFlavors() {
+        consumerProjectSetup.writeDefaultBuildGradle(
+            prefix = """
+                plugins {
+                    id("com.android.application")
+                    id("androidx.baselineprofiles.consumer")
+                }
+                android {
+                    namespace 'com.example.namespace'
+                    productFlavors {
+                        flavorDimensions = ["version"]
+                        free {
+                            dimension "version"
+                        }
+                        paid {
+                            dimension "version"
+                        }
+                    }
+                }
+                dependencies {
+                    baselineprofiles(project(":$producerModuleName"))
+                }
+            """.trimIndent(),
+            suffix = ""
+        )
+        producerProjectSetup.writeDefaultBuildGradle(
+            prefix = MockProducerBuildGrade()
+                .withConfiguration(flavor = "free", buildType = "release")
+                .withConfiguration(flavor = "paid", buildType = "release")
+                .withProducedBaselineProfiles(
+                    listOf("3", "2"),
+                    flavor = "free",
+                    buildType = "release"
+                )
+                .withProducedBaselineProfiles(
+                    listOf("4", "1"),
+                    flavor = "paid",
+                    buildType = "release"
+                )
+                .build(),
+            suffix = ""
+        )
+
+        gradleRunner
+            .withArguments("generateBaselineProfiles", "--stacktrace")
+            .build()
+
+        // The expected output should have each line sorted ascending
+        val baselineProf =
+            File(consumerProjectSetup.rootDir, "src/main/baseline-prof.txt").readLines()
+        assertThat(baselineProf).containsExactly("1", "2", "3", "4")
+    }
+}
+
+private class MockProducerBuildGrade() {
+
+    private var profileIndex = 0
+    private var content = """
+        plugins { id("com.android.library") }
+        android { namespace 'com.example.namespace' }
+
+        // This task produces a file with a fixed output
+        abstract class TestProfileTask extends DefaultTask {
+            @Input abstract Property<String> getFileContent()
+            @OutputFile abstract RegularFileProperty getOutputFile()
+            @TaskAction void exec() { getOutputFile().get().asFile.write(getFileContent().get()) }
+        }
+
+    """.trimIndent()
+
+    fun withConfiguration(flavor: String, buildType: String): MockProducerBuildGrade {
+
+        content += """
+
+        configurations {
+            ${configurationName(flavor, buildType)} {
+                canBeConsumed = true
+                canBeResolved = false
+                attributes {
+                    attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, "baselineprofile"))
+                    attribute(Attribute.of("androidx.baselineprofiles.gradle.attributes.BuildType", String), "$buildType")
+                    attribute(Attribute.of("androidx.baselineprofiles.gradle.attributes.Flavor", String), "$flavor")
+                }
+            }
+        }
+
+        """.trimIndent()
+        return this
+    }
+
+    fun withProducedBaselineProfiles(
+        lines: List<String>,
+        flavor: String = "",
+        buildType: String
+    ): MockProducerBuildGrade {
+        profileIndex++
+        content += """
+
+        def task$profileIndex = tasks.register('testProfile$profileIndex', TestProfileTask)
+        task$profileIndex.configure {
+            it.outputFile.set(project.layout.buildDirectory.file("test$profileIndex"))
+            it.fileContent.set(${"\"\"\"${lines.joinToString("\n")}\"\"\""})
+        }
+        artifacts {
+            add("${configurationName(flavor, buildType)}", task$profileIndex.map { it.outputFile })
+        }
+
+        """.trimIndent()
+        return this
+    }
+
+    fun build() = content
+}
+
+private fun configurationName(flavor: String, buildType: String): String =
+    camelCase(flavor, buildType, CONFIGURATION_NAME_BASELINE_PROFILES)
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/test/kotlin/androidx/baselineprofiles/gradle/producer/BaselineProfilesProducerPluginTest.kt b/benchmark/baseline-profiles-gradle-plugin/src/test/kotlin/androidx/baselineprofiles/gradle/producer/BaselineProfilesProducerPluginTest.kt
new file mode 100644
index 0000000..2de52a4
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/test/kotlin/androidx/baselineprofiles/gradle/producer/BaselineProfilesProducerPluginTest.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.baselineprofiles.gradle.producer
+
+import androidx.testutils.gradle.ProjectSetupRule
+import kotlin.test.assertTrue
+import org.gradle.testkit.runner.GradleRunner
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class BaselineProfilesProducerPluginTest {
+
+    // Unit test will be minimal because the producer plugin is applied to an android test module,
+    // that requires a working target application. Testing will be covered only by integration tests.
+
+    private val rootFolder = TemporaryFolder().also { it.create() }
+
+    @get:Rule
+    val producerProjectSetup = ProjectSetupRule(rootFolder.root)
+
+    @get:Rule
+    val buildProviderProjectSetup = ProjectSetupRule(rootFolder.root)
+
+    private lateinit var producerModuleName: String
+    private lateinit var buildProviderModuleName: String
+    private lateinit var gradleRunner: GradleRunner
+
+    @Before
+    fun setUp() {
+        producerModuleName = producerProjectSetup.rootDir.relativeTo(rootFolder.root).name
+        buildProviderModuleName = buildProviderProjectSetup.rootDir.relativeTo(rootFolder.root).name
+
+        rootFolder.newFile("settings.gradle").writeText(
+            """
+            include '$producerModuleName'
+            include '$buildProviderModuleName'
+        """.trimIndent()
+        )
+        gradleRunner = GradleRunner.create()
+            .withProjectDir(producerProjectSetup.rootDir)
+            .withPluginClasspath()
+    }
+
+    @Test
+    fun verifyTasksWithAndroidTestPlugin() {
+        buildProviderProjectSetup.writeDefaultBuildGradle(
+            prefix = """
+                plugins {
+                    id("com.android.application")
+                    id("androidx.baselineprofiles.buildprovider")
+                }
+                android {
+                    namespace 'com.example.namespace'
+                }
+            """.trimIndent(),
+            suffix = ""
+        )
+        producerProjectSetup.writeDefaultBuildGradle(
+            prefix = """
+                plugins {
+                    id("com.android.test")
+                    id("androidx.baselineprofiles.producer")
+                }
+                android {
+                    targetProjectPath = ":$buildProviderModuleName"
+                    namespace 'com.example.namespace.test'
+                }
+                tasks.register("mergeNonObfuscatedReleaseTestResultProtos") { println("Stub") }
+            """.trimIndent(),
+            suffix = ""
+        )
+
+        val output = gradleRunner.withArguments("tasks", "--stacktrace").build().output
+        assertTrue { output.contains("collectNonObfuscatedReleaseBaselineProfiles - ") }
+    }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
index 936c42f..ce57b42 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
@@ -117,9 +117,12 @@
         project.tasks.register(
             DARWIN_BENCHMARK_RESULTS_TASK, DarwinBenchmarkResultsTask::class.java
         ) {
+            it.group = "Verification"
+            it.description = "Run Kotlin Multiplatform Benchmarks for Darwin"
             it.xcResultPath.set(runDarwinBenchmarks.flatMap { task ->
                 task.xcResultPath
             })
+            it.referenceSha.set(extension.referenceSha)
             val resultFileName = "${extension.xcodeProjectName.get()}-benchmark-result.json"
             it.outputFile.set(
                 project.layout.buildDirectory.file(
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt
index 35fec87..ba06bad 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt
@@ -18,6 +18,7 @@
 
 import org.gradle.api.file.RegularFileProperty
 import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Optional
 
 /**
  * The [DarwinBenchmarkPlugin] extension.
@@ -43,4 +44,11 @@
      * This is typically discovered by using `xcrun xctrace list devices`.
      */
     abstract val destination: Property<String>
+
+    /**
+     * The reference sha for the source code being benchmarked. This can be useful
+     * when tracking regressions.
+     */
+    @get:Optional
+    abstract val referenceSha: Property<String>
 }
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt
index dfca8e9..b596566 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt
@@ -25,7 +25,9 @@
 import org.gradle.api.DefaultTask
 import org.gradle.api.file.DirectoryProperty
 import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
 import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Input
 import org.gradle.api.tasks.InputDirectory
 import org.gradle.api.tasks.Optional
 import org.gradle.api.tasks.OutputFile
@@ -42,6 +44,10 @@
     @get:PathSensitive(PathSensitivity.RELATIVE)
     abstract val xcResultPath: DirectoryProperty
 
+    @get:Input
+    @get:Optional
+    abstract val referenceSha: Property<String>
+
     @get:OutputFile
     abstract val outputFile: RegularFileProperty
 
@@ -66,7 +72,7 @@
             }
         }
         val (record, summaries) = parser.parseResults()
-        val metrics = Metrics.buildMetrics(record, summaries)
+        val metrics = Metrics.buildMetrics(record, summaries, referenceSha.orNull)
         val output = GsonHelpers.gsonBuilder()
             .setPrettyPrinting()
             .create()
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/skia/Metric.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/skia/Metric.kt
index 6bed2de..f1c2bd8 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/skia/Metric.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/skia/Metric.kt
@@ -32,11 +32,18 @@
 )
 
 data class Metric(val key: Map<String, String>, val measurements: Measurements)
-data class Metrics(val key: Map<String, String>, val results: List<Metric>) {
+data class Metrics(
+    val key: Map<String, String>,
+    val results: List<Metric>,
+    val version: Long = 1L,
+    @SerializedName("git_hash")
+    val referenceSha: String? = null
+) {
     companion object {
         fun buildMetrics(
             record: ActionsInvocationRecord,
-            summaries: List<ActionTestSummary>
+            summaries: List<ActionTestSummary>,
+            referenceSha: String?,
         ): Metrics {
             require(record.actions.actionRecords.isNotEmpty())
             val runDestination = record.actions.actionRecords.first().runDestination
@@ -49,7 +56,7 @@
                 "modelCode" to runDestination.localComputerRecord.modelCode.value
             )
             val results = summaries.flatMap { it.toMetrics() }
-            return Metrics(metricsKeys, results)
+            return Metrics(metricsKeys, results, referenceSha = referenceSha)
         }
 
         private fun ActionTestSummary.toMetrics(): List<Metric> {
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt b/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt
index 2ce55a7..dc9d985 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt
@@ -50,7 +50,7 @@
         // Metrics typically correspond to the number of tests
         assertThat(record.metrics.size()).isEqualTo(2)
         assertThat(summaries.isNotEmpty()).isTrue()
-        val metrics = Metrics.buildMetrics(record, summaries)
+        val metrics = Metrics.buildMetrics(record, summaries, referenceSha = null)
         val json = GsonHelpers.gsonBuilder()
             .setPrettyPrinting()
             .create()
diff --git a/benchmark/benchmark-darwin-samples/build.gradle b/benchmark/benchmark-darwin-samples/build.gradle
index c946afe..c297722 100644
--- a/benchmark/benchmark-darwin-samples/build.gradle
+++ b/benchmark/benchmark-darwin-samples/build.gradle
@@ -56,6 +56,7 @@
     scheme = "testapp-ios"
     // ios 13, 15.2
     destination = "platform=iOS Simulator,name=iPhone 13,OS=15.2"
+    referenceSha.set(androidx.getReferenceSha())
 }
 
 androidx {
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
index 685451c..6357477 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
@@ -23,6 +23,7 @@
 import androidx.benchmark.Shell
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
+import androidx.test.filters.RequiresDevice
 import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.By
@@ -70,6 +71,7 @@
         assertFalse(Shell.isPackageAlive(Packages.TARGET))
     }
 
+    @RequiresDevice // b/264938965
     @SdkSuppress(minSdkVersion = 24)
     @Test
     fun compile_speedProfile() {
@@ -91,6 +93,7 @@
         assertEquals(iterations, executions)
     }
 
+    @RequiresDevice // b/264938965
     @Test
     fun compile_full() {
         val scope = MacrobenchmarkScope(Packages.TARGET, launchWithClearTask = true)
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
index bcb337f..f8eb993 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
@@ -16,13 +16,13 @@
 
 package androidx.benchmark.macro.perfetto
 
+import android.os.Build
 import androidx.benchmark.macro.FileLinkingRule
 import androidx.benchmark.macro.Packages
 import androidx.benchmark.perfetto.PerfettoCapture
 import androidx.benchmark.perfetto.PerfettoHelper
 import androidx.benchmark.perfetto.PerfettoHelper.Companion.LOWEST_BUNDLED_VERSION_SUPPORTED
 import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
-import androidx.test.filters.FlakyTest
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.testutils.verifyWithPolling
@@ -38,6 +38,7 @@
 import org.junit.runners.Parameterized
 import kotlin.test.assertEquals
 import kotlin.test.fail
+import org.junit.Ignore
 
 /**
  * Trace validation tests for PerfettoCapture
@@ -61,15 +62,27 @@
         PerfettoHelper.stopAllPerfettoProcesses()
     }
 
-    @FlakyTest(bugId = 258216025)
+    @Ignore("b/258216025")
     @SdkSuppress(minSdkVersion = LOWEST_BUNDLED_VERSION_SUPPORTED, maxSdkVersion = 33)
     @Test
-    fun captureAndValidateTrace_bundled() = captureAndValidateTrace(unbundled = false)
+    fun captureAndValidateTrace_bundled() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
 
-    @FlakyTest(bugId = 258216025)
+        captureAndValidateTrace(unbundled = false)
+    }
+
+    @Ignore("b/258216025")
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
-    fun captureAndValidateTrace_unbundled() = captureAndValidateTrace(unbundled = true)
+    fun captureAndValidateTrace_unbundled() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
+        captureAndValidateTrace(unbundled = true)
+    }
 
     private fun captureAndValidateTrace(unbundled: Boolean) {
         assumeTrue(isAbiSupported())
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..02ffea2 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,10 +62,14 @@
         assertThat(actualSlices).containsExactlyElementsIn(expectedSlices)
     }
 
-    @FlakyTest(bugId = 260715950)
+    @Ignore // b/260715950
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun test_endToEnd() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         assumeTrue(PerfettoHelper.isAbiSupported())
         StringSource.appTagTraceStrings.forEach { trace(it) { } }
         StringSource.userspaceTraceStrings.forEachIndexed { ix, str ->
diff --git a/benchmark/integration-tests/baselineprofiles-consumer/build.gradle b/benchmark/integration-tests/baselineprofiles-consumer/build.gradle
new file mode 100644
index 0000000..d3e1e45
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-consumer/build.gradle
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+    id("kotlin-android")
+    id("androidx.baselineprofiles.consumer")
+    id("androidx.baselineprofiles.buildprovider")
+}
+
+android {
+    buildTypes {
+        release {
+            minifyEnabled true
+            shrinkResources true
+            proguardFiles getDefaultProguardFile("proguard-android-optimize.txt")
+        }
+    }
+    namespace "androidx.benchmark.integration.baselineprofiles.consumer"
+}
+
+dependencies {
+    implementation(libs.kotlinStdlib)
+    implementation(libs.constraintLayout)
+    baselineprofiles(project(":benchmark:integration-tests:baselineprofiles-producer"))
+}
+
+apply(from: "../baselineprofiles-test-utils/utils.gradle")
diff --git a/benchmark/integration-tests/baselineprofiles-consumer/src/main/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-consumer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ca9827a
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-consumer/src/main/AndroidManifest.xml
@@ -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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <application
+        android:allowBackup="false"
+        android:label="Jetpack Baselineprofiles Target"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.AppCompat"
+        tools:ignore="MissingApplicationIcon">
+
+        <activity
+            android:name=".EmptyActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="androidx.benchmark.integration.baselineprofiles.consumer.EMPTY_ACTIVITY" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
+        <profileable android:shell="true" />
+    </application>
+</manifest>
diff --git a/benchmark/integration-tests/baselineprofiles-consumer/src/main/expected-baseline-prof.txt b/benchmark/integration-tests/baselineprofiles-consumer/src/main/expected-baseline-prof.txt
new file mode 100644
index 0000000..a23b650
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-consumer/src/main/expected-baseline-prof.txt
@@ -0,0 +1,301 @@
+HSPLandroidx/benchmark/integration/baselineprofiles/consumer/EmptyActivity;-><init>()V
+HSPLandroidx/benchmark/integration/baselineprofiles/consumer/EmptyActivity;->onCreate(Landroid/os/Bundle;)V
+HSPLandroidx/constraintlayout/solver/ArrayLinkedVariables;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/ArrayLinkedVariables;-><init>(Landroidx/constraintlayout/solver/ArrayRow;Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;-><init>()V
+HSPLandroidx/constraintlayout/solver/ArrayRow;-><init>(Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->addError(Landroidx/constraintlayout/solver/LinearSystem;I)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->addSingleError(Landroidx/constraintlayout/solver/SolverVariable;I)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->chooseSubject(Landroidx/constraintlayout/solver/LinearSystem;)Z
+HSPLandroidx/constraintlayout/solver/ArrayRow;->chooseSubjectInVariables(Landroidx/constraintlayout/solver/LinearSystem;)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->createRowCentering(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;IFLandroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;I)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->createRowEquals(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;I)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->createRowGreaterThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;I)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->createRowLowerThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;I)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->ensurePositiveConstant()V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->getKey()Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->hasKeyVariable()Z
+HSPLandroidx/constraintlayout/solver/ArrayRow;->isEmpty()Z
+HSPLandroidx/constraintlayout/solver/ArrayRow;->isNew(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/LinearSystem;)Z
+HSPLandroidx/constraintlayout/solver/ArrayRow;->pivot(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->reset()V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->updateFromFinalVariable(Landroidx/constraintlayout/solver/LinearSystem;Landroidx/constraintlayout/solver/SolverVariable;Z)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->updateFromRow(Landroidx/constraintlayout/solver/ArrayRow;Z)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->updateFromSystem(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/Cache;-><init>()V
+HSPLandroidx/constraintlayout/solver/LinearSystem$ValuesRow;-><init>(Landroidx/constraintlayout/solver/LinearSystem;Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;-><init>()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->acquireSolverVariable(Landroidx/constraintlayout/solver/SolverVariable$Type;Ljava/lang/String;)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addCentering(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;IFLandroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addConstraint(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addEquality(Landroidx/constraintlayout/solver/SolverVariable;I)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addEquality(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addGreaterThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addLowerThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addRow(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addSingleError(Landroidx/constraintlayout/solver/ArrayRow;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->computeValues()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createErrorVariable(ILjava/lang/String;)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createObjectVariable(Ljava/lang/Object;)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createRow()Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createSlackVariable()Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->enforceBFS(Landroidx/constraintlayout/solver/LinearSystem$Row;)I
+HSPLandroidx/constraintlayout/solver/LinearSystem;->getCache()Landroidx/constraintlayout/solver/Cache;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->getMetrics()Landroidx/constraintlayout/solver/Metrics;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->getObjectVariableValue(Ljava/lang/Object;)I
+HSPLandroidx/constraintlayout/solver/LinearSystem;->increaseTableSize()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->minimize()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->minimizeGoal(Landroidx/constraintlayout/solver/LinearSystem$Row;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->optimize(Landroidx/constraintlayout/solver/LinearSystem$Row;Z)I
+HSPLandroidx/constraintlayout/solver/LinearSystem;->releaseRows()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->reset()V
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;-><init>(I)V
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;->acquire()Ljava/lang/Object;
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;->release(Ljava/lang/Object;)Z
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;->releaseAll([Ljava/lang/Object;I)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;-><init>(Landroidx/constraintlayout/solver/PriorityGoalRow;Landroidx/constraintlayout/solver/PriorityGoalRow;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;->addToGoal(Landroidx/constraintlayout/solver/SolverVariable;F)Z
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;->init(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;->isNegative()Z
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;->reset()V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;-><init>(Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->addError(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->addToGoal(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->clear()V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->getPivotCandidate(Landroidx/constraintlayout/solver/LinearSystem;[Z)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->removeGoal(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->updateFromRow(Landroidx/constraintlayout/solver/ArrayRow;Z)V
+HSPLandroidx/constraintlayout/solver/SolverVariable$Type;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/SolverVariable$Type;-><init>(Ljava/lang/String;I)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/SolverVariable;-><init>(Landroidx/constraintlayout/solver/SolverVariable$Type;Ljava/lang/String;)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->addToRow(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->increaseErrorId()V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->removeFromRow(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->reset()V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->setFinalValue(Landroidx/constraintlayout/solver/LinearSystem;F)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->setType(Landroidx/constraintlayout/solver/SolverVariable$Type;Ljava/lang/String;)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->updateReferencesWithNewDefinition(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;-><init>(Landroidx/constraintlayout/solver/ArrayRow;Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->add(Landroidx/constraintlayout/solver/SolverVariable;FZ)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->addToHashMap(Landroidx/constraintlayout/solver/SolverVariable;I)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->addVariable(ILandroidx/constraintlayout/solver/SolverVariable;F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->clear()V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->divideByAmount(F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->findEmptySlot()I
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->get(Landroidx/constraintlayout/solver/SolverVariable;)F
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->getCurrentSize()I
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->getVariable(I)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->getVariableValue(I)F
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->indexOf(Landroidx/constraintlayout/solver/SolverVariable;)I
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->insertVariable(ILandroidx/constraintlayout/solver/SolverVariable;F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->invert()V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->put(Landroidx/constraintlayout/solver/SolverVariable;F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->remove(Landroidx/constraintlayout/solver/SolverVariable;Z)F
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->removeFromHashMap(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->use(Landroidx/constraintlayout/solver/ArrayRow;Z)F
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;-><init>(Ljava/lang/String;I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;->values()[Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->connect(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;IIZ)Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->getMargin()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->getSolverVariable()Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->getTarget()Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->isConnected()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->reset()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->resetSolverVariable(Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget$1;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;-><init>(Ljava/lang/String;I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;->values()[Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->addAnchors()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->addFirst()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->addToSolver(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->applyConstraints(Landroidx/constraintlayout/solver/LinearSystem;ZZZZLandroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;ZLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;IIIIFZZZZIIIIFZ)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->createObjectVariables(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getAnchor(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;)Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getBaselineDistance()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getCompanionWidget()Ljava/lang/Object;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getDimensionBehaviour(I)Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getHeight()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getHorizontalDimensionBehaviour()Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getMinHeight()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getMinWidth()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getParent()Landroidx/constraintlayout/solver/widgets/ConstraintWidget;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getVerticalDimensionBehaviour()Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getVisibility()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getWidth()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getX()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getY()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->immediateConnect(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;II)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->isChainHead(I)Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->isInHorizontalChain()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->isInVerticalChain()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->reset()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->resetSolverVariables(Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setBaselineDistance(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setCompanionWidget(Ljava/lang/Object;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setDimensionRatio(Ljava/lang/String;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setFrame(IIII)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHasBaseline(Z)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHeight(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHorizontalBiasPercent(F)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHorizontalChainStyle(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHorizontalDimensionBehaviour(Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHorizontalMatchStyle(IIIF)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHorizontalWeight(F)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setInBarrier(IZ)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setMaxHeight(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setMaxWidth(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setMinHeight(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setMinWidth(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setParent(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVerticalBiasPercent(F)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVerticalChainStyle(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVerticalDimensionBehaviour(Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVerticalMatchStyle(IIIF)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVerticalWeight(F)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVisibility(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setWidth(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setX(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setY(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->updateFromSolver(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->addChildrenToSolver(Landroidx/constraintlayout/solver/LinearSystem;)Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->getMeasurer()Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measurer;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->getOptimizationLevel()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->invalidateGraph()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->invalidateMeasures()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->isHeightMeasuredTooSmall()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->isWidthMeasuredTooSmall()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->layout()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->measure(IIIIIIIII)J
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->optimizeFor(I)Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->resetChains()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->setMeasurer(Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measurer;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->setOptimizationLevel(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->setRtl(Z)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->updateChildrenFromSolver(Landroidx/constraintlayout/solver/LinearSystem;[Z)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->updateHierarchy()V
+HSPLandroidx/constraintlayout/solver/widgets/Optimizer;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/Optimizer;->checkMatchParent(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;Landroidx/constraintlayout/solver/LinearSystem;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/Optimizer;->enabled(II)Z
+HSPLandroidx/constraintlayout/solver/widgets/WidgetContainer;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/WidgetContainer;->add(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/WidgetContainer;->removeAllChildren()V
+HSPLandroidx/constraintlayout/solver/widgets/WidgetContainer;->resetSolverVariables(Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measure;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;->measure(Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measurer;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Z)Z
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;->measureChildren(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;->solveLinearSystem(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;Ljava/lang/String;II)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;->solverMeasure(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;IIIIIIIII)J
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;->updateHierarchy(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;->invalidateGraph()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;->invalidateMeasures()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;->setMeasurer(Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measurer;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyNode$Type;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyNode$Type;-><init>(Ljava/lang/String;I)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyNode;-><init>(Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DimensionDependency;-><init>(Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/HorizontalWidgetRun;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/HorizontalWidgetRun;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/VerticalWidgetRun;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/WidgetRun$RunType;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/WidgetRun$RunType;-><init>(Ljava/lang/String;I)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$1;-><clinit>()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams$Table;-><clinit>()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;->resolveLayoutDirection(I)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;->validate()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$Measurer;-><init>(Landroidx/constraintlayout/widget/ConstraintLayout;Landroidx/constraintlayout/widget/ConstraintLayout;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$Measurer;->captureLayoutInfos(IIIIII)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$Measurer;->didMeasures()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$Measurer;->measure(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measure;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->access$000(Landroidx/constraintlayout/widget/ConstraintLayout;)Ljava/util/ArrayList;
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->addView(Landroid/view/View;ILandroid/view/ViewGroup$LayoutParams;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->applyConstraintsFromLayoutParams(ZLandroid/view/View;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;Landroid/util/SparseArray;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->checkLayoutParams(Landroid/view/ViewGroup$LayoutParams;)Z
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->dispatchDraw(Landroid/graphics/Canvas;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->generateLayoutParams(Landroid/util/AttributeSet;)Landroid/view/ViewGroup$LayoutParams;
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->generateLayoutParams(Landroid/util/AttributeSet;)Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->getPaddingWidth()I
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->getViewWidget(Landroid/view/View;)Landroidx/constraintlayout/solver/widgets/ConstraintWidget;
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->init(Landroid/util/AttributeSet;II)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->isRtl()Z
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->markHierarchyDirty()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->onLayout(ZIIII)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->onMeasure(II)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->onViewAdded(Landroid/view/View;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->requestLayout()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->resolveMeasuredDimension(IIIIZZ)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->resolveSystem(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;III)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->setChildrenConstraints()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->setSelfDimensionBehaviour(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;IIII)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->updateHierarchy()Z
+HSPLandroidx/constraintlayout/widget/R$styleable;-><clinit>()V
+HSPLandroidx/core/app/CoreComponentFactory;-><init>()V
+HSPLandroidx/core/app/CoreComponentFactory;->checkCompatWrapper(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/core/app/CoreComponentFactory;->instantiateActivity(Ljava/lang/ClassLoader;Ljava/lang/String;Landroid/content/Intent;)Landroid/app/Activity;
+HSPLandroidx/core/app/CoreComponentFactory;->instantiateApplication(Ljava/lang/ClassLoader;Ljava/lang/String;)Landroid/app/Application;
+Landroidx/benchmark/integration/baselineprofiles/consumer/EmptyActivity;
+Landroidx/constraintlayout/solver/ArrayLinkedVariables;
+Landroidx/constraintlayout/solver/ArrayRow$ArrayRowVariables;
+Landroidx/constraintlayout/solver/ArrayRow;
+Landroidx/constraintlayout/solver/Cache;
+Landroidx/constraintlayout/solver/LinearSystem$Row;
+Landroidx/constraintlayout/solver/LinearSystem$ValuesRow;
+Landroidx/constraintlayout/solver/LinearSystem;
+Landroidx/constraintlayout/solver/Pools$Pool;
+Landroidx/constraintlayout/solver/Pools$SimplePool;
+Landroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;
+Landroidx/constraintlayout/solver/PriorityGoalRow;
+Landroidx/constraintlayout/solver/SolverVariable$Type;
+Landroidx/constraintlayout/solver/SolverVariable;
+Landroidx/constraintlayout/solver/SolverVariableValues;
+Landroidx/constraintlayout/solver/widgets/Barrier;
+Landroidx/constraintlayout/solver/widgets/ChainHead;
+Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;
+Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;
+Landroidx/constraintlayout/solver/widgets/ConstraintWidget$1;
+Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;
+Landroidx/constraintlayout/solver/widgets/ConstraintWidget;
+Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;
+Landroidx/constraintlayout/solver/widgets/Guideline;
+Landroidx/constraintlayout/solver/widgets/Helper;
+Landroidx/constraintlayout/solver/widgets/HelperWidget;
+Landroidx/constraintlayout/solver/widgets/Optimizer;
+Landroidx/constraintlayout/solver/widgets/VirtualLayout;
+Landroidx/constraintlayout/solver/widgets/WidgetContainer;
+Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measure;
+Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measurer;
+Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;
+Landroidx/constraintlayout/solver/widgets/analyzer/Dependency;
+Landroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;
+Landroidx/constraintlayout/solver/widgets/analyzer/DependencyNode$Type;
+Landroidx/constraintlayout/solver/widgets/analyzer/DependencyNode;
+Landroidx/constraintlayout/solver/widgets/analyzer/DimensionDependency;
+Landroidx/constraintlayout/solver/widgets/analyzer/HorizontalWidgetRun;
+Landroidx/constraintlayout/solver/widgets/analyzer/VerticalWidgetRun;
+Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun$RunType;
+Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;
+Landroidx/constraintlayout/widget/ConstraintHelper;
+Landroidx/constraintlayout/widget/ConstraintLayout$1;
+Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams$Table;
+Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;
+Landroidx/constraintlayout/widget/ConstraintLayout$Measurer;
+Landroidx/constraintlayout/widget/ConstraintLayout;
+Landroidx/constraintlayout/widget/Guideline;
+Landroidx/constraintlayout/widget/Placeholder;
+Landroidx/constraintlayout/widget/R$styleable;
+Landroidx/constraintlayout/widget/VirtualLayout;
+Landroidx/core/app/CoreComponentFactory$CompatWrapped;
+Landroidx/core/app/CoreComponentFactory;
\ No newline at end of file
diff --git a/benchmark/integration-tests/baselineprofiles-consumer/src/main/java/androidx/benchmark/integration/baselineprofiles/consumer/EmptyActivity.kt b/benchmark/integration-tests/baselineprofiles-consumer/src/main/java/androidx/benchmark/integration/baselineprofiles/consumer/EmptyActivity.kt
new file mode 100644
index 0000000..9a1b40b
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-consumer/src/main/java/androidx/benchmark/integration/baselineprofiles/consumer/EmptyActivity.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.benchmark.integration.baselineprofiles.consumer
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+
+class EmptyActivity : Activity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_main)
+        findViewById<TextView>(R.id.txtNotice).setText(R.string.app_notice)
+    }
+}
diff --git a/benchmark/integration-tests/baselineprofiles-consumer/src/main/res/layout/activity_main.xml b/benchmark/integration-tests/baselineprofiles-consumer/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..7739482
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-consumer/src/main/res/layout/activity_main.xml
@@ -0,0 +1,35 @@
+<?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.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:id="@+id/txtNotice"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:text="Preview Some Text" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/benchmark/integration-tests/baselineprofiles-consumer/src/main/res/values/donottranslate-strings.xml b/benchmark/integration-tests/baselineprofiles-consumer/src/main/res/values/donottranslate-strings.xml
new file mode 100644
index 0000000..c880dc5
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-consumer/src/main/res/values/donottranslate-strings.xml
@@ -0,0 +1,19 @@
+<?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="app_notice">Baseline Profiles Integration Test App.</string>
+</resources>
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-consumer/build.gradle b/benchmark/integration-tests/baselineprofiles-flavors-consumer/build.gradle
new file mode 100644
index 0000000..8ae2e3f
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-consumer/build.gradle
@@ -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.
+ */
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+    id("kotlin-android")
+    id("androidx.baselineprofiles.buildprovider")
+    id("androidx.baselineprofiles.consumer")
+}
+
+android {
+    buildTypes {
+        release {
+            minifyEnabled true
+            shrinkResources true
+            proguardFiles getDefaultProguardFile("proguard-android-optimize.txt")
+        }
+    }
+    productFlavors {
+        flavorDimensions = ["version"]
+        free {
+            dimension "version"
+            applicationIdSuffix ".free"
+            versionNameSuffix "-free"
+        }
+        paid {
+            dimension "version"
+            applicationIdSuffix ".paid"
+            versionNameSuffix "-paid"
+        }
+    }
+    namespace "androidx.benchmark.integration.baselineprofiles.flavors.consumer"
+}
+
+dependencies {
+    implementation(libs.kotlinStdlib)
+    implementation(libs.constraintLayout)
+
+    baselineprofiles(project(":benchmark:integration-tests:baselineprofiles-flavors-producer"))
+}
+
+baselineProfilesProfileConsumer {
+    buildTypeName = "release"
+}
+
+apply(from: "../baselineprofiles-test-utils/utils.gradle")
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/free/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/free/AndroidManifest.xml
new file mode 100644
index 0000000..fb4bcd8
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/free/AndroidManifest.xml
@@ -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.
+  -->
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:node="merge">
+
+    <application
+        android:allowBackup="false"
+        android:label="Jetpack Baselineprofiles Target"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.AppCompat"
+        tools:ignore="MissingApplicationIcon">
+
+        <activity
+            android:name=".EmptyActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="androidx.benchmark.integration.baselineprofiles.flavors.consumer.free.EMPTY_ACTIVITY" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
+        <profileable android:shell="true" />
+    </application>
+</manifest>
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..118910a
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<!--
+  ~ 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:tools="http://schemas.android.com/tools"
+    tools:node="merge">
+
+    <application />
+</manifest>
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/expected-baseline-prof.txt b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/expected-baseline-prof.txt
new file mode 100644
index 0000000..75735f3
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/expected-baseline-prof.txt
@@ -0,0 +1,301 @@
+HSPLandroidx/benchmark/integration/baselineprofiles/flavors/consumer/EmptyActivity;-><init>()V
+HSPLandroidx/benchmark/integration/baselineprofiles/flavors/consumer/EmptyActivity;->onCreate(Landroid/os/Bundle;)V
+HSPLandroidx/constraintlayout/solver/ArrayLinkedVariables;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/ArrayLinkedVariables;-><init>(Landroidx/constraintlayout/solver/ArrayRow;Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;-><init>()V
+HSPLandroidx/constraintlayout/solver/ArrayRow;-><init>(Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->addError(Landroidx/constraintlayout/solver/LinearSystem;I)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->addSingleError(Landroidx/constraintlayout/solver/SolverVariable;I)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->chooseSubject(Landroidx/constraintlayout/solver/LinearSystem;)Z
+HSPLandroidx/constraintlayout/solver/ArrayRow;->chooseSubjectInVariables(Landroidx/constraintlayout/solver/LinearSystem;)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->createRowCentering(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;IFLandroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;I)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->createRowEquals(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;I)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->createRowGreaterThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;I)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->createRowLowerThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;I)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->ensurePositiveConstant()V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->getKey()Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->hasKeyVariable()Z
+HSPLandroidx/constraintlayout/solver/ArrayRow;->isEmpty()Z
+HSPLandroidx/constraintlayout/solver/ArrayRow;->isNew(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/LinearSystem;)Z
+HSPLandroidx/constraintlayout/solver/ArrayRow;->pivot(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->reset()V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->updateFromFinalVariable(Landroidx/constraintlayout/solver/LinearSystem;Landroidx/constraintlayout/solver/SolverVariable;Z)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->updateFromRow(Landroidx/constraintlayout/solver/ArrayRow;Z)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->updateFromSystem(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/Cache;-><init>()V
+HSPLandroidx/constraintlayout/solver/LinearSystem$ValuesRow;-><init>(Landroidx/constraintlayout/solver/LinearSystem;Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;-><init>()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->acquireSolverVariable(Landroidx/constraintlayout/solver/SolverVariable$Type;Ljava/lang/String;)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addCentering(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;IFLandroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addConstraint(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addEquality(Landroidx/constraintlayout/solver/SolverVariable;I)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addEquality(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addGreaterThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addLowerThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addRow(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addSingleError(Landroidx/constraintlayout/solver/ArrayRow;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->computeValues()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createErrorVariable(ILjava/lang/String;)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createObjectVariable(Ljava/lang/Object;)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createRow()Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createSlackVariable()Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->enforceBFS(Landroidx/constraintlayout/solver/LinearSystem$Row;)I
+HSPLandroidx/constraintlayout/solver/LinearSystem;->getCache()Landroidx/constraintlayout/solver/Cache;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->getMetrics()Landroidx/constraintlayout/solver/Metrics;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->getObjectVariableValue(Ljava/lang/Object;)I
+HSPLandroidx/constraintlayout/solver/LinearSystem;->increaseTableSize()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->minimize()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->minimizeGoal(Landroidx/constraintlayout/solver/LinearSystem$Row;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->optimize(Landroidx/constraintlayout/solver/LinearSystem$Row;Z)I
+HSPLandroidx/constraintlayout/solver/LinearSystem;->releaseRows()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->reset()V
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;-><init>(I)V
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;->acquire()Ljava/lang/Object;
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;->release(Ljava/lang/Object;)Z
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;->releaseAll([Ljava/lang/Object;I)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;-><init>(Landroidx/constraintlayout/solver/PriorityGoalRow;Landroidx/constraintlayout/solver/PriorityGoalRow;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;->addToGoal(Landroidx/constraintlayout/solver/SolverVariable;F)Z
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;->init(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;->isNegative()Z
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;->reset()V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;-><init>(Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->addError(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->addToGoal(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->clear()V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->getPivotCandidate(Landroidx/constraintlayout/solver/LinearSystem;[Z)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->removeGoal(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->updateFromRow(Landroidx/constraintlayout/solver/ArrayRow;Z)V
+HSPLandroidx/constraintlayout/solver/SolverVariable$Type;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/SolverVariable$Type;-><init>(Ljava/lang/String;I)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/SolverVariable;-><init>(Landroidx/constraintlayout/solver/SolverVariable$Type;Ljava/lang/String;)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->addToRow(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->increaseErrorId()V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->removeFromRow(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->reset()V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->setFinalValue(Landroidx/constraintlayout/solver/LinearSystem;F)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->setType(Landroidx/constraintlayout/solver/SolverVariable$Type;Ljava/lang/String;)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->updateReferencesWithNewDefinition(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;-><init>(Landroidx/constraintlayout/solver/ArrayRow;Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->add(Landroidx/constraintlayout/solver/SolverVariable;FZ)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->addToHashMap(Landroidx/constraintlayout/solver/SolverVariable;I)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->addVariable(ILandroidx/constraintlayout/solver/SolverVariable;F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->clear()V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->divideByAmount(F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->findEmptySlot()I
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->get(Landroidx/constraintlayout/solver/SolverVariable;)F
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->getCurrentSize()I
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->getVariable(I)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->getVariableValue(I)F
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->indexOf(Landroidx/constraintlayout/solver/SolverVariable;)I
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->insertVariable(ILandroidx/constraintlayout/solver/SolverVariable;F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->invert()V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->put(Landroidx/constraintlayout/solver/SolverVariable;F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->remove(Landroidx/constraintlayout/solver/SolverVariable;Z)F
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->removeFromHashMap(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->use(Landroidx/constraintlayout/solver/ArrayRow;Z)F
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;-><init>(Ljava/lang/String;I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;->values()[Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->connect(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;IIZ)Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->getMargin()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->getSolverVariable()Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->getTarget()Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->isConnected()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->reset()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->resetSolverVariable(Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget$1;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;-><init>(Ljava/lang/String;I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;->values()[Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->addAnchors()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->addFirst()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->addToSolver(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->applyConstraints(Landroidx/constraintlayout/solver/LinearSystem;ZZZZLandroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;ZLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;IIIIFZZZZIIIIFZ)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->createObjectVariables(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getAnchor(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;)Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getBaselineDistance()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getCompanionWidget()Ljava/lang/Object;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getDimensionBehaviour(I)Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getHeight()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getHorizontalDimensionBehaviour()Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getMinHeight()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getMinWidth()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getParent()Landroidx/constraintlayout/solver/widgets/ConstraintWidget;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getVerticalDimensionBehaviour()Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getVisibility()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getWidth()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getX()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getY()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->immediateConnect(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;II)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->isChainHead(I)Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->isInHorizontalChain()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->isInVerticalChain()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->reset()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->resetSolverVariables(Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setBaselineDistance(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setCompanionWidget(Ljava/lang/Object;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setDimensionRatio(Ljava/lang/String;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setFrame(IIII)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHasBaseline(Z)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHeight(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHorizontalBiasPercent(F)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHorizontalChainStyle(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHorizontalDimensionBehaviour(Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHorizontalMatchStyle(IIIF)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHorizontalWeight(F)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setInBarrier(IZ)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setMaxHeight(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setMaxWidth(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setMinHeight(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setMinWidth(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setParent(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVerticalBiasPercent(F)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVerticalChainStyle(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVerticalDimensionBehaviour(Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVerticalMatchStyle(IIIF)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVerticalWeight(F)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVisibility(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setWidth(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setX(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setY(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->updateFromSolver(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->addChildrenToSolver(Landroidx/constraintlayout/solver/LinearSystem;)Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->getMeasurer()Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measurer;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->getOptimizationLevel()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->invalidateGraph()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->invalidateMeasures()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->isHeightMeasuredTooSmall()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->isWidthMeasuredTooSmall()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->layout()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->measure(IIIIIIIII)J
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->optimizeFor(I)Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->resetChains()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->setMeasurer(Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measurer;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->setOptimizationLevel(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->setRtl(Z)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->updateChildrenFromSolver(Landroidx/constraintlayout/solver/LinearSystem;[Z)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->updateHierarchy()V
+HSPLandroidx/constraintlayout/solver/widgets/Optimizer;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/Optimizer;->checkMatchParent(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;Landroidx/constraintlayout/solver/LinearSystem;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/Optimizer;->enabled(II)Z
+HSPLandroidx/constraintlayout/solver/widgets/WidgetContainer;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/WidgetContainer;->add(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/WidgetContainer;->removeAllChildren()V
+HSPLandroidx/constraintlayout/solver/widgets/WidgetContainer;->resetSolverVariables(Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measure;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;->measure(Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measurer;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Z)Z
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;->measureChildren(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;->solveLinearSystem(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;Ljava/lang/String;II)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;->solverMeasure(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;IIIIIIIII)J
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;->updateHierarchy(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;->invalidateGraph()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;->invalidateMeasures()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;->setMeasurer(Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measurer;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyNode$Type;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyNode$Type;-><init>(Ljava/lang/String;I)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyNode;-><init>(Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DimensionDependency;-><init>(Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/HorizontalWidgetRun;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/HorizontalWidgetRun;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/VerticalWidgetRun;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/WidgetRun$RunType;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/WidgetRun$RunType;-><init>(Ljava/lang/String;I)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$1;-><clinit>()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams$Table;-><clinit>()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;->resolveLayoutDirection(I)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;->validate()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$Measurer;-><init>(Landroidx/constraintlayout/widget/ConstraintLayout;Landroidx/constraintlayout/widget/ConstraintLayout;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$Measurer;->captureLayoutInfos(IIIIII)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$Measurer;->didMeasures()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$Measurer;->measure(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measure;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->access$000(Landroidx/constraintlayout/widget/ConstraintLayout;)Ljava/util/ArrayList;
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->addView(Landroid/view/View;ILandroid/view/ViewGroup$LayoutParams;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->applyConstraintsFromLayoutParams(ZLandroid/view/View;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;Landroid/util/SparseArray;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->checkLayoutParams(Landroid/view/ViewGroup$LayoutParams;)Z
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->dispatchDraw(Landroid/graphics/Canvas;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->generateLayoutParams(Landroid/util/AttributeSet;)Landroid/view/ViewGroup$LayoutParams;
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->generateLayoutParams(Landroid/util/AttributeSet;)Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->getPaddingWidth()I
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->getViewWidget(Landroid/view/View;)Landroidx/constraintlayout/solver/widgets/ConstraintWidget;
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->init(Landroid/util/AttributeSet;II)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->isRtl()Z
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->markHierarchyDirty()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->onLayout(ZIIII)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->onMeasure(II)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->onViewAdded(Landroid/view/View;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->requestLayout()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->resolveMeasuredDimension(IIIIZZ)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->resolveSystem(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;III)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->setChildrenConstraints()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->setSelfDimensionBehaviour(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;IIII)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->updateHierarchy()Z
+HSPLandroidx/constraintlayout/widget/R$styleable;-><clinit>()V
+HSPLandroidx/core/app/CoreComponentFactory;-><init>()V
+HSPLandroidx/core/app/CoreComponentFactory;->checkCompatWrapper(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/core/app/CoreComponentFactory;->instantiateActivity(Ljava/lang/ClassLoader;Ljava/lang/String;Landroid/content/Intent;)Landroid/app/Activity;
+HSPLandroidx/core/app/CoreComponentFactory;->instantiateApplication(Ljava/lang/ClassLoader;Ljava/lang/String;)Landroid/app/Application;
+Landroidx/benchmark/integration/baselineprofiles/flavors/consumer/EmptyActivity;
+Landroidx/constraintlayout/solver/ArrayLinkedVariables;
+Landroidx/constraintlayout/solver/ArrayRow$ArrayRowVariables;
+Landroidx/constraintlayout/solver/ArrayRow;
+Landroidx/constraintlayout/solver/Cache;
+Landroidx/constraintlayout/solver/LinearSystem$Row;
+Landroidx/constraintlayout/solver/LinearSystem$ValuesRow;
+Landroidx/constraintlayout/solver/LinearSystem;
+Landroidx/constraintlayout/solver/Pools$Pool;
+Landroidx/constraintlayout/solver/Pools$SimplePool;
+Landroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;
+Landroidx/constraintlayout/solver/PriorityGoalRow;
+Landroidx/constraintlayout/solver/SolverVariable$Type;
+Landroidx/constraintlayout/solver/SolverVariable;
+Landroidx/constraintlayout/solver/SolverVariableValues;
+Landroidx/constraintlayout/solver/widgets/Barrier;
+Landroidx/constraintlayout/solver/widgets/ChainHead;
+Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;
+Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;
+Landroidx/constraintlayout/solver/widgets/ConstraintWidget$1;
+Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;
+Landroidx/constraintlayout/solver/widgets/ConstraintWidget;
+Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;
+Landroidx/constraintlayout/solver/widgets/Guideline;
+Landroidx/constraintlayout/solver/widgets/Helper;
+Landroidx/constraintlayout/solver/widgets/HelperWidget;
+Landroidx/constraintlayout/solver/widgets/Optimizer;
+Landroidx/constraintlayout/solver/widgets/VirtualLayout;
+Landroidx/constraintlayout/solver/widgets/WidgetContainer;
+Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measure;
+Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measurer;
+Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;
+Landroidx/constraintlayout/solver/widgets/analyzer/Dependency;
+Landroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;
+Landroidx/constraintlayout/solver/widgets/analyzer/DependencyNode$Type;
+Landroidx/constraintlayout/solver/widgets/analyzer/DependencyNode;
+Landroidx/constraintlayout/solver/widgets/analyzer/DimensionDependency;
+Landroidx/constraintlayout/solver/widgets/analyzer/HorizontalWidgetRun;
+Landroidx/constraintlayout/solver/widgets/analyzer/VerticalWidgetRun;
+Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun$RunType;
+Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;
+Landroidx/constraintlayout/widget/ConstraintHelper;
+Landroidx/constraintlayout/widget/ConstraintLayout$1;
+Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams$Table;
+Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;
+Landroidx/constraintlayout/widget/ConstraintLayout$Measurer;
+Landroidx/constraintlayout/widget/ConstraintLayout;
+Landroidx/constraintlayout/widget/Guideline;
+Landroidx/constraintlayout/widget/Placeholder;
+Landroidx/constraintlayout/widget/R$styleable;
+Landroidx/constraintlayout/widget/VirtualLayout;
+Landroidx/core/app/CoreComponentFactory$CompatWrapped;
+Landroidx/core/app/CoreComponentFactory;
\ No newline at end of file
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/java/androidx/benchmark/integration/baselineprofiles/flavors/consumer/EmptyActivity.kt b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/java/androidx/benchmark/integration/baselineprofiles/flavors/consumer/EmptyActivity.kt
new file mode 100644
index 0000000..5119859
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/java/androidx/benchmark/integration/baselineprofiles/flavors/consumer/EmptyActivity.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.benchmark.integration.baselineprofiles.flavors.consumer
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+
+class EmptyActivity : Activity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_main)
+        findViewById<TextView>(R.id.txtNotice).setText(R.string.app_notice)
+    }
+}
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/res/layout/activity_main.xml b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..7739482
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/res/layout/activity_main.xml
@@ -0,0 +1,35 @@
+<?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.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:id="@+id/txtNotice"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:text="Preview Some Text" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/res/values/donottranslate-strings.xml b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/res/values/donottranslate-strings.xml
new file mode 100644
index 0000000..c880dc5
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/res/values/donottranslate-strings.xml
@@ -0,0 +1,19 @@
+<?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="app_notice">Baseline Profiles Integration Test App.</string>
+</resources>
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/paid/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/paid/AndroidManifest.xml
new file mode 100644
index 0000000..514b52c
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/paid/AndroidManifest.xml
@@ -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.
+  -->
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:node="merge">
+
+    <application
+        android:allowBackup="false"
+        android:label="Jetpack Baselineprofiles Target"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.AppCompat"
+        tools:ignore="MissingApplicationIcon">
+
+        <activity
+            android:name=".EmptyActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="androidx.benchmark.integration.baselineprofiles.flavors.consumer.paid.EMPTY_ACTIVITY" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
+        <profileable android:shell="true" />
+    </application>
+</manifest>
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-producer/build.gradle b/benchmark/integration-tests/baselineprofiles-flavors-producer/build.gradle
new file mode 100644
index 0000000..d3e6cdb
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-producer/build.gradle
@@ -0,0 +1,68 @@
+import com.android.build.api.dsl.ManagedVirtualDevice
+
+/*
+ * 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.
+ */
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.test")
+    id("kotlin-android")
+    id("androidx.baselineprofiles.producer")
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 23
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+    testOptions.managedDevices.devices {
+        pixel6Api31(ManagedVirtualDevice) {
+            device = "Pixel 6"
+            apiLevel = 31
+            systemImageSource = "aosp"
+        }
+    }
+    buildTypes {
+        release { }
+    }
+    productFlavors {
+        flavorDimensions = ["version"]
+        free { dimension "version" }
+        paid { dimension "version" }
+    }
+    targetProjectPath = ":benchmark:integration-tests:baselineprofiles-flavors-consumer"
+    namespace "androidx.benchmark.integration.baselineprofiles.flavors.producer"
+}
+
+dependencies {
+    implementation(project(":benchmark:benchmark-junit4"))
+    implementation(project(":benchmark:benchmark-macro-junit4"))
+    implementation(libs.testRules)
+    implementation(libs.testExtJunit)
+    implementation(libs.testCore)
+    implementation(libs.testRunner)
+    implementation(libs.testUiautomator)
+    implementation(libs.testExtTruth)
+}
+
+baselineProfilesProfileProducer {
+    managedDevices += "pixel6Api31"
+    useConnectedDevices = false
+}
+
+androidx {
+    disableDeviceTests = true
+}
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-producer/src/free/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/free/AndroidManifest.xml
new file mode 100644
index 0000000..bae036b
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/free/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest />
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-producer/src/free/java/androidx/benchmark/integration/baselineprofiles/flavors/producer/BaselineProfileTest.kt b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/free/java/androidx/benchmark/integration/baselineprofiles/flavors/producer/BaselineProfileTest.kt
new file mode 100644
index 0000000..1fd878f
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/free/java/androidx/benchmark/integration/baselineprofiles/flavors/producer/BaselineProfileTest.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.benchmark.integration.baselineprofiles.flavors.producer
+
+import android.content.Intent
+import android.os.Build
+import androidx.benchmark.DeviceInfo
+import androidx.benchmark.macro.junit4.BaselineProfileRule
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Assume.assumeTrue
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+@SdkSuppress(minSdkVersion = 29)
+class BaselineProfileTest {
+
+    @get:Rule
+    val baselineRule = BaselineProfileRule()
+
+    @Test
+    fun startupBaselineProfile() {
+        assumeTrue(DeviceInfo.isRooted || Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
+
+        // Collects the baseline profile
+        baselineRule.collectBaselineProfile(
+            packageName = PACKAGE_NAME,
+            profileBlock = {
+                startActivityAndWait(Intent(ACTION))
+                device.waitForIdle()
+            }
+        )
+    }
+
+    companion object {
+        private const val PACKAGE_NAME =
+            "androidx.benchmark.integration.baselineprofiles.flavors.consumer.free"
+        private const val ACTION =
+            "androidx.benchmark.integration.baselineprofiles.flavors.consumer.free.EMPTY_ACTIVITY"
+    }
+}
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-producer/src/main/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d4c1970
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest />
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-producer/src/paid/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/paid/AndroidManifest.xml
new file mode 100644
index 0000000..bae036b
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/paid/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest />
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-producer/src/paid/java/androidx/benchmark/integration/baselineprofiles/flavors/producer/BaselineProfileTest.kt b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/paid/java/androidx/benchmark/integration/baselineprofiles/flavors/producer/BaselineProfileTest.kt
new file mode 100644
index 0000000..6a96947
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/paid/java/androidx/benchmark/integration/baselineprofiles/flavors/producer/BaselineProfileTest.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.benchmark.integration.baselineprofiles.flavors.producer
+
+import android.content.Intent
+import android.os.Build
+import androidx.benchmark.DeviceInfo
+import androidx.benchmark.macro.junit4.BaselineProfileRule
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Assume.assumeTrue
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+@SdkSuppress(minSdkVersion = 29)
+class BaselineProfileTest {
+
+    @get:Rule
+    val baselineRule = BaselineProfileRule()
+
+    @Test
+    fun startupBaselineProfile() {
+        assumeTrue(DeviceInfo.isRooted || Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
+
+        // Collects the baseline profile
+        baselineRule.collectBaselineProfile(
+            packageName = PACKAGE_NAME,
+            profileBlock = {
+                startActivityAndWait(Intent(ACTION))
+                device.waitForIdle()
+            }
+        )
+    }
+
+    companion object {
+        private const val PACKAGE_NAME =
+            "androidx.benchmark.integration.baselineprofiles.flavors.consumer.paid"
+        private const val ACTION =
+            "androidx.benchmark.integration.baselineprofiles.flavors.consumer.paid.EMPTY_ACTIVITY"
+    }
+}
diff --git a/benchmark/integration-tests/baselineprofiles-library-build-provider/build.gradle b/benchmark/integration-tests/baselineprofiles-library-build-provider/build.gradle
new file mode 100644
index 0000000..9c8061f
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-build-provider/build.gradle
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+    id("kotlin-android")
+    id("androidx.baselineprofiles.buildprovider")
+}
+
+android {
+    buildTypes {
+        release {
+            minifyEnabled true
+            shrinkResources true
+            proguardFiles getDefaultProguardFile("proguard-android-optimize.txt")
+        }
+    }
+    namespace "androidx.benchmark.integration.baselineprofiles.library.buildprovider"
+}
+
+dependencies {
+    implementation(libs.kotlinStdlib)
+    implementation(libs.constraintLayout)
+}
diff --git a/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..fbb0bc7
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/AndroidManifest.xml
@@ -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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <application
+        android:allowBackup="false"
+        android:label="Jetpack Baselineprofiles Target"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.AppCompat"
+        tools:ignore="MissingApplicationIcon">
+
+        <activity
+            android:name=".EmptyActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="androidx.benchmark.integration.baselineprofiles.EMPTY_ACTIVITY" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
+        <profileable android:shell="true" />
+    </application>
+</manifest>
diff --git a/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/java/androidx/benchmark/integration/baselineprofiles/library/buildprovider/EmptyActivity.kt b/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/java/androidx/benchmark/integration/baselineprofiles/library/buildprovider/EmptyActivity.kt
new file mode 100644
index 0000000..64b8ed7
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/java/androidx/benchmark/integration/baselineprofiles/library/buildprovider/EmptyActivity.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.benchmark.integration.baselineprofiles.library.buildprovider
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+
+class EmptyActivity : Activity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_main)
+        findViewById<TextView>(R.id.txtNotice).setText(R.string.app_notice)
+    }
+}
diff --git a/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/res/layout/activity_main.xml b/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..7739482
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/res/layout/activity_main.xml
@@ -0,0 +1,35 @@
+<?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.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:id="@+id/txtNotice"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:text="Preview Some Text" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/res/values/donottranslate-strings.xml b/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/res/values/donottranslate-strings.xml
new file mode 100644
index 0000000..c880dc5
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/res/values/donottranslate-strings.xml
@@ -0,0 +1,19 @@
+<?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="app_notice">Baseline Profiles Integration Test App.</string>
+</resources>
diff --git a/benchmark/integration-tests/baselineprofiles-library-consumer/build.gradle b/benchmark/integration-tests/baselineprofiles-library-consumer/build.gradle
new file mode 100644
index 0000000..e314c41
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-consumer/build.gradle
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("kotlin-android")
+    id("androidx.baselineprofiles.consumer")
+}
+
+android {
+    namespace "androidx.benchmark.integration.baselineprofiles.library.consumer"
+}
+
+dependencies {
+    baselineprofiles(project(":benchmark:integration-tests:baselineprofiles-library-producer"))
+}
+
+dependencies {
+    implementation(libs.kotlinStdlib)
+}
+
+apply(from: "../baselineprofiles-test-utils/utils.gradle")
diff --git a/benchmark/integration-tests/baselineprofiles-library-consumer/src/main/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-library-consumer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..7c52910
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-consumer/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<!--
+  ~ 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 />
diff --git a/benchmark/integration-tests/baselineprofiles-library-consumer/src/main/expected-baseline-prof.txt b/benchmark/integration-tests/baselineprofiles-library-consumer/src/main/expected-baseline-prof.txt
new file mode 100644
index 0000000..2dde749
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-consumer/src/main/expected-baseline-prof.txt
@@ -0,0 +1,301 @@
+HSPLandroidx/benchmark/integration/baselineprofiles/library/buildprovider/EmptyActivity;-><init>()V
+HSPLandroidx/benchmark/integration/baselineprofiles/library/buildprovider/EmptyActivity;->onCreate(Landroid/os/Bundle;)V
+HSPLandroidx/constraintlayout/solver/ArrayLinkedVariables;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/ArrayLinkedVariables;-><init>(Landroidx/constraintlayout/solver/ArrayRow;Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;-><init>()V
+HSPLandroidx/constraintlayout/solver/ArrayRow;-><init>(Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->addError(Landroidx/constraintlayout/solver/LinearSystem;I)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->addSingleError(Landroidx/constraintlayout/solver/SolverVariable;I)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->chooseSubject(Landroidx/constraintlayout/solver/LinearSystem;)Z
+HSPLandroidx/constraintlayout/solver/ArrayRow;->chooseSubjectInVariables(Landroidx/constraintlayout/solver/LinearSystem;)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->createRowCentering(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;IFLandroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;I)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->createRowEquals(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;I)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->createRowGreaterThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;I)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->createRowLowerThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;I)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->ensurePositiveConstant()V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->getKey()Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/ArrayRow;->hasKeyVariable()Z
+HSPLandroidx/constraintlayout/solver/ArrayRow;->isEmpty()Z
+HSPLandroidx/constraintlayout/solver/ArrayRow;->isNew(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/LinearSystem;)Z
+HSPLandroidx/constraintlayout/solver/ArrayRow;->pivot(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->reset()V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->updateFromFinalVariable(Landroidx/constraintlayout/solver/LinearSystem;Landroidx/constraintlayout/solver/SolverVariable;Z)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->updateFromRow(Landroidx/constraintlayout/solver/ArrayRow;Z)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->updateFromSystem(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/Cache;-><init>()V
+HSPLandroidx/constraintlayout/solver/LinearSystem$ValuesRow;-><init>(Landroidx/constraintlayout/solver/LinearSystem;Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;-><init>()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->acquireSolverVariable(Landroidx/constraintlayout/solver/SolverVariable$Type;Ljava/lang/String;)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addCentering(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;IFLandroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addConstraint(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addEquality(Landroidx/constraintlayout/solver/SolverVariable;I)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addEquality(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addGreaterThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addLowerThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addRow(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addSingleError(Landroidx/constraintlayout/solver/ArrayRow;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->computeValues()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createErrorVariable(ILjava/lang/String;)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createObjectVariable(Ljava/lang/Object;)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createRow()Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createSlackVariable()Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->enforceBFS(Landroidx/constraintlayout/solver/LinearSystem$Row;)I
+HSPLandroidx/constraintlayout/solver/LinearSystem;->getCache()Landroidx/constraintlayout/solver/Cache;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->getMetrics()Landroidx/constraintlayout/solver/Metrics;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->getObjectVariableValue(Ljava/lang/Object;)I
+HSPLandroidx/constraintlayout/solver/LinearSystem;->increaseTableSize()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->minimize()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->minimizeGoal(Landroidx/constraintlayout/solver/LinearSystem$Row;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->optimize(Landroidx/constraintlayout/solver/LinearSystem$Row;Z)I
+HSPLandroidx/constraintlayout/solver/LinearSystem;->releaseRows()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->reset()V
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;-><init>(I)V
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;->acquire()Ljava/lang/Object;
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;->release(Ljava/lang/Object;)Z
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;->releaseAll([Ljava/lang/Object;I)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;-><init>(Landroidx/constraintlayout/solver/PriorityGoalRow;Landroidx/constraintlayout/solver/PriorityGoalRow;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;->addToGoal(Landroidx/constraintlayout/solver/SolverVariable;F)Z
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;->init(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;->isNegative()Z
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;->reset()V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;-><init>(Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->addError(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->addToGoal(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->clear()V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->getPivotCandidate(Landroidx/constraintlayout/solver/LinearSystem;[Z)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->removeGoal(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->updateFromRow(Landroidx/constraintlayout/solver/ArrayRow;Z)V
+HSPLandroidx/constraintlayout/solver/SolverVariable$Type;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/SolverVariable$Type;-><init>(Ljava/lang/String;I)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/SolverVariable;-><init>(Landroidx/constraintlayout/solver/SolverVariable$Type;Ljava/lang/String;)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->addToRow(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->increaseErrorId()V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->removeFromRow(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->reset()V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->setFinalValue(Landroidx/constraintlayout/solver/LinearSystem;F)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->setType(Landroidx/constraintlayout/solver/SolverVariable$Type;Ljava/lang/String;)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->updateReferencesWithNewDefinition(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;-><init>(Landroidx/constraintlayout/solver/ArrayRow;Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->add(Landroidx/constraintlayout/solver/SolverVariable;FZ)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->addToHashMap(Landroidx/constraintlayout/solver/SolverVariable;I)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->addVariable(ILandroidx/constraintlayout/solver/SolverVariable;F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->clear()V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->divideByAmount(F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->findEmptySlot()I
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->get(Landroidx/constraintlayout/solver/SolverVariable;)F
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->getCurrentSize()I
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->getVariable(I)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->getVariableValue(I)F
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->indexOf(Landroidx/constraintlayout/solver/SolverVariable;)I
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->insertVariable(ILandroidx/constraintlayout/solver/SolverVariable;F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->invert()V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->put(Landroidx/constraintlayout/solver/SolverVariable;F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->remove(Landroidx/constraintlayout/solver/SolverVariable;Z)F
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->removeFromHashMap(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->use(Landroidx/constraintlayout/solver/ArrayRow;Z)F
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;-><init>(Ljava/lang/String;I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;->values()[Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->connect(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;IIZ)Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->getMargin()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->getSolverVariable()Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->getTarget()Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->isConnected()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->reset()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->resetSolverVariable(Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget$1;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;-><init>(Ljava/lang/String;I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;->values()[Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->addAnchors()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->addFirst()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->addToSolver(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->applyConstraints(Landroidx/constraintlayout/solver/LinearSystem;ZZZZLandroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;ZLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;IIIIFZZZZIIIIFZ)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->createObjectVariables(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getAnchor(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;)Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getBaselineDistance()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getCompanionWidget()Ljava/lang/Object;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getDimensionBehaviour(I)Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getHeight()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getHorizontalDimensionBehaviour()Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getMinHeight()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getMinWidth()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getParent()Landroidx/constraintlayout/solver/widgets/ConstraintWidget;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getVerticalDimensionBehaviour()Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getVisibility()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getWidth()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getX()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getY()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->immediateConnect(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;II)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->isChainHead(I)Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->isInHorizontalChain()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->isInVerticalChain()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->reset()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->resetSolverVariables(Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setBaselineDistance(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setCompanionWidget(Ljava/lang/Object;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setDimensionRatio(Ljava/lang/String;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setFrame(IIII)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHasBaseline(Z)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHeight(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHorizontalBiasPercent(F)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHorizontalChainStyle(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHorizontalDimensionBehaviour(Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHorizontalMatchStyle(IIIF)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHorizontalWeight(F)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setInBarrier(IZ)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setMaxHeight(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setMaxWidth(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setMinHeight(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setMinWidth(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setParent(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVerticalBiasPercent(F)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVerticalChainStyle(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVerticalDimensionBehaviour(Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVerticalMatchStyle(IIIF)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVerticalWeight(F)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVisibility(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setWidth(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setX(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setY(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->updateFromSolver(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->addChildrenToSolver(Landroidx/constraintlayout/solver/LinearSystem;)Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->getMeasurer()Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measurer;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->getOptimizationLevel()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->invalidateGraph()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->invalidateMeasures()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->isHeightMeasuredTooSmall()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->isWidthMeasuredTooSmall()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->layout()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->measure(IIIIIIIII)J
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->optimizeFor(I)Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->resetChains()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->setMeasurer(Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measurer;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->setOptimizationLevel(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->setRtl(Z)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->updateChildrenFromSolver(Landroidx/constraintlayout/solver/LinearSystem;[Z)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->updateHierarchy()V
+HSPLandroidx/constraintlayout/solver/widgets/Optimizer;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/Optimizer;->checkMatchParent(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;Landroidx/constraintlayout/solver/LinearSystem;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/Optimizer;->enabled(II)Z
+HSPLandroidx/constraintlayout/solver/widgets/WidgetContainer;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/WidgetContainer;->add(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/WidgetContainer;->removeAllChildren()V
+HSPLandroidx/constraintlayout/solver/widgets/WidgetContainer;->resetSolverVariables(Landroidx/constraintlayout/solver/Cache;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measure;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;->measure(Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measurer;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Z)Z
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;->measureChildren(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;->solveLinearSystem(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;Ljava/lang/String;II)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;->solverMeasure(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;IIIIIIIII)J
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;->updateHierarchy(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;->invalidateGraph()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;->invalidateMeasures()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;->setMeasurer(Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measurer;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyNode$Type;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyNode$Type;-><init>(Ljava/lang/String;I)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyNode;-><init>(Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DimensionDependency;-><init>(Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/HorizontalWidgetRun;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/HorizontalWidgetRun;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/VerticalWidgetRun;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/WidgetRun$RunType;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/WidgetRun$RunType;-><init>(Ljava/lang/String;I)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$1;-><clinit>()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams$Table;-><clinit>()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;->resolveLayoutDirection(I)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;->validate()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$Measurer;-><init>(Landroidx/constraintlayout/widget/ConstraintLayout;Landroidx/constraintlayout/widget/ConstraintLayout;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$Measurer;->captureLayoutInfos(IIIIII)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$Measurer;->didMeasures()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$Measurer;->measure(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measure;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->access$000(Landroidx/constraintlayout/widget/ConstraintLayout;)Ljava/util/ArrayList;
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->addView(Landroid/view/View;ILandroid/view/ViewGroup$LayoutParams;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->applyConstraintsFromLayoutParams(ZLandroid/view/View;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;Landroid/util/SparseArray;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->checkLayoutParams(Landroid/view/ViewGroup$LayoutParams;)Z
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->dispatchDraw(Landroid/graphics/Canvas;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->generateLayoutParams(Landroid/util/AttributeSet;)Landroid/view/ViewGroup$LayoutParams;
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->generateLayoutParams(Landroid/util/AttributeSet;)Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->getPaddingWidth()I
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->getViewWidget(Landroid/view/View;)Landroidx/constraintlayout/solver/widgets/ConstraintWidget;
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->init(Landroid/util/AttributeSet;II)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->isRtl()Z
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->markHierarchyDirty()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->onLayout(ZIIII)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->onMeasure(II)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->onViewAdded(Landroid/view/View;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->requestLayout()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->resolveMeasuredDimension(IIIIZZ)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->resolveSystem(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;III)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->setChildrenConstraints()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->setSelfDimensionBehaviour(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;IIII)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->updateHierarchy()Z
+HSPLandroidx/constraintlayout/widget/R$styleable;-><clinit>()V
+HSPLandroidx/core/app/CoreComponentFactory;-><init>()V
+HSPLandroidx/core/app/CoreComponentFactory;->checkCompatWrapper(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/core/app/CoreComponentFactory;->instantiateActivity(Ljava/lang/ClassLoader;Ljava/lang/String;Landroid/content/Intent;)Landroid/app/Activity;
+HSPLandroidx/core/app/CoreComponentFactory;->instantiateApplication(Ljava/lang/ClassLoader;Ljava/lang/String;)Landroid/app/Application;
+Landroidx/benchmark/integration/baselineprofiles/library/buildprovider/EmptyActivity;
+Landroidx/constraintlayout/solver/ArrayLinkedVariables;
+Landroidx/constraintlayout/solver/ArrayRow$ArrayRowVariables;
+Landroidx/constraintlayout/solver/ArrayRow;
+Landroidx/constraintlayout/solver/Cache;
+Landroidx/constraintlayout/solver/LinearSystem$Row;
+Landroidx/constraintlayout/solver/LinearSystem$ValuesRow;
+Landroidx/constraintlayout/solver/LinearSystem;
+Landroidx/constraintlayout/solver/Pools$Pool;
+Landroidx/constraintlayout/solver/Pools$SimplePool;
+Landroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;
+Landroidx/constraintlayout/solver/PriorityGoalRow;
+Landroidx/constraintlayout/solver/SolverVariable$Type;
+Landroidx/constraintlayout/solver/SolverVariable;
+Landroidx/constraintlayout/solver/SolverVariableValues;
+Landroidx/constraintlayout/solver/widgets/Barrier;
+Landroidx/constraintlayout/solver/widgets/ChainHead;
+Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;
+Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;
+Landroidx/constraintlayout/solver/widgets/ConstraintWidget$1;
+Landroidx/constraintlayout/solver/widgets/ConstraintWidget$DimensionBehaviour;
+Landroidx/constraintlayout/solver/widgets/ConstraintWidget;
+Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;
+Landroidx/constraintlayout/solver/widgets/Guideline;
+Landroidx/constraintlayout/solver/widgets/Helper;
+Landroidx/constraintlayout/solver/widgets/HelperWidget;
+Landroidx/constraintlayout/solver/widgets/Optimizer;
+Landroidx/constraintlayout/solver/widgets/VirtualLayout;
+Landroidx/constraintlayout/solver/widgets/WidgetContainer;
+Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measure;
+Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measurer;
+Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure;
+Landroidx/constraintlayout/solver/widgets/analyzer/Dependency;
+Landroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;
+Landroidx/constraintlayout/solver/widgets/analyzer/DependencyNode$Type;
+Landroidx/constraintlayout/solver/widgets/analyzer/DependencyNode;
+Landroidx/constraintlayout/solver/widgets/analyzer/DimensionDependency;
+Landroidx/constraintlayout/solver/widgets/analyzer/HorizontalWidgetRun;
+Landroidx/constraintlayout/solver/widgets/analyzer/VerticalWidgetRun;
+Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun$RunType;
+Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;
+Landroidx/constraintlayout/widget/ConstraintHelper;
+Landroidx/constraintlayout/widget/ConstraintLayout$1;
+Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams$Table;
+Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;
+Landroidx/constraintlayout/widget/ConstraintLayout$Measurer;
+Landroidx/constraintlayout/widget/ConstraintLayout;
+Landroidx/constraintlayout/widget/Guideline;
+Landroidx/constraintlayout/widget/Placeholder;
+Landroidx/constraintlayout/widget/R$styleable;
+Landroidx/constraintlayout/widget/VirtualLayout;
+Landroidx/core/app/CoreComponentFactory$CompatWrapped;
+Landroidx/core/app/CoreComponentFactory;
\ No newline at end of file
diff --git a/benchmark/integration-tests/baselineprofiles-library-consumer/src/main/java/androidx/benchmark/integration/baselineprofiles/library/consumer/EmptyClass.kt b/benchmark/integration-tests/baselineprofiles-library-consumer/src/main/java/androidx/benchmark/integration/baselineprofiles/library/consumer/EmptyClass.kt
new file mode 100644
index 0000000..47a198a
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-consumer/src/main/java/androidx/benchmark/integration/baselineprofiles/library/consumer/EmptyClass.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.benchmark.integration.baselineprofiles.library.consumer
+
+import android.util.Log
+
+object EmptyClass {
+
+    fun doSomething() {
+        Log.d("EmptyClass", "Done.")
+    }
+}
diff --git a/benchmark/integration-tests/baselineprofiles-library-producer/build.gradle b/benchmark/integration-tests/baselineprofiles-library-producer/build.gradle
new file mode 100644
index 0000000..c797473
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-producer/build.gradle
@@ -0,0 +1,60 @@
+import com.android.build.api.dsl.ManagedVirtualDevice
+
+/*
+ * 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.
+ */
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.test")
+    id("kotlin-android")
+    id("androidx.baselineprofiles.producer")
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 23
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+    testOptions.managedDevices.devices {
+        pixel6Api31(ManagedVirtualDevice) {
+            device = "Pixel 6"
+            apiLevel = 31
+            systemImageSource = "aosp"
+        }
+    }
+    targetProjectPath = ":benchmark:integration-tests:baselineprofiles-library-build-provider"
+    namespace "androidx.benchmark.integration.baselineprofiles.library.producer"
+}
+
+dependencies {
+    implementation(project(":benchmark:benchmark-junit4"))
+    implementation(project(":benchmark:benchmark-macro-junit4"))
+    implementation(libs.testRules)
+    implementation(libs.testExtJunit)
+    implementation(libs.testCore)
+    implementation(libs.testRunner)
+    implementation(libs.testUiautomator)
+    implementation(libs.testExtTruth)
+}
+
+baselineProfilesProfileProducer {
+    managedDevices += "pixel6Api31"
+    useConnectedDevices = false
+}
+
+androidx {
+    disableDeviceTests = true
+}
diff --git a/benchmark/integration-tests/baselineprofiles-library-producer/src/main/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-library-producer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..bae036b
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-producer/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest />
diff --git a/benchmark/integration-tests/baselineprofiles-library-producer/src/main/java/androidx/benchmark/integration/baselineprofiles/library/producer/BaselineProfileTest.kt b/benchmark/integration-tests/baselineprofiles-library-producer/src/main/java/androidx/benchmark/integration/baselineprofiles/library/producer/BaselineProfileTest.kt
new file mode 100644
index 0000000..1d5cdec
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-producer/src/main/java/androidx/benchmark/integration/baselineprofiles/library/producer/BaselineProfileTest.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.benchmark.integration.baselineprofiles.library.producer
+
+import android.content.Intent
+import android.os.Build
+import androidx.benchmark.DeviceInfo
+import androidx.benchmark.macro.junit4.BaselineProfileRule
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Assume.assumeTrue
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+@SdkSuppress(minSdkVersion = 29)
+class BaselineProfileTest {
+
+    @get:Rule
+    val baselineRule = BaselineProfileRule()
+
+    @Test
+    fun startupBaselineProfile() {
+        assumeTrue(DeviceInfo.isRooted || Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
+
+        // Collects the baseline profile
+        baselineRule.collectBaselineProfile(
+            packageName = PACKAGE_NAME,
+            profileBlock = {
+                startActivityAndWait(Intent(ACTION))
+                device.waitForIdle()
+            }
+        )
+    }
+
+    companion object {
+        private const val PACKAGE_NAME =
+            "androidx.benchmark.integration.baselineprofiles.library.buildprovider"
+        private const val ACTION =
+            "androidx.benchmark.integration.baselineprofiles.EMPTY_ACTIVITY"
+    }
+}
diff --git a/benchmark/integration-tests/baselineprofiles-producer/build.gradle b/benchmark/integration-tests/baselineprofiles-producer/build.gradle
new file mode 100644
index 0000000..c49546c
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-producer/build.gradle
@@ -0,0 +1,60 @@
+import com.android.build.api.dsl.ManagedVirtualDevice
+
+/*
+ * 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.
+ */
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.test")
+    id("kotlin-android")
+    id("androidx.baselineprofiles.producer")
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 23
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+    testOptions.managedDevices.devices {
+        pixel6Api31(ManagedVirtualDevice) {
+            device = "Pixel 6"
+            apiLevel = 31
+            systemImageSource = "aosp"
+        }
+    }
+    targetProjectPath = ":benchmark:integration-tests:baselineprofiles-consumer"
+    namespace "androidx.benchmark.integration.baselineprofiles.producer"
+}
+
+dependencies {
+    implementation(project(":benchmark:benchmark-junit4"))
+    implementation(project(":benchmark:benchmark-macro-junit4"))
+    implementation(libs.testRules)
+    implementation(libs.testExtJunit)
+    implementation(libs.testCore)
+    implementation(libs.testRunner)
+    implementation(libs.testUiautomator)
+    implementation(libs.testExtTruth)
+}
+
+baselineProfilesProfileProducer {
+    managedDevices += "pixel6Api31"
+    useConnectedDevices = false
+}
+
+androidx {
+    disableDeviceTests = true
+}
diff --git a/benchmark/integration-tests/baselineprofiles-producer/src/main/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-producer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..bae036b
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-producer/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest />
diff --git a/benchmark/integration-tests/baselineprofiles-producer/src/main/java/androidx/benchmark/integration/baselineprofiles/producer/BaselineProfileTest.kt b/benchmark/integration-tests/baselineprofiles-producer/src/main/java/androidx/benchmark/integration/baselineprofiles/producer/BaselineProfileTest.kt
new file mode 100644
index 0000000..3fbcfea
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-producer/src/main/java/androidx/benchmark/integration/baselineprofiles/producer/BaselineProfileTest.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.benchmark.integration.baselineprofiles.producer
+
+import android.content.Intent
+import android.os.Build
+import androidx.benchmark.DeviceInfo
+import androidx.benchmark.macro.junit4.BaselineProfileRule
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Assume.assumeTrue
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+@SdkSuppress(minSdkVersion = 29)
+class BaselineProfileTest {
+
+    @get:Rule
+    val baselineRule = BaselineProfileRule()
+
+    @Test
+    fun startupBaselineProfile() {
+        assumeTrue(DeviceInfo.isRooted || Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
+
+        // Collects the baseline profile
+        baselineRule.collectBaselineProfile(
+            packageName = PACKAGE_NAME,
+            profileBlock = {
+                startActivityAndWait(Intent(ACTION))
+                device.waitForIdle()
+            }
+        )
+    }
+
+    companion object {
+        private const val PACKAGE_NAME =
+            "androidx.benchmark.integration.baselineprofiles.consumer"
+        private const val ACTION =
+            "androidx.benchmark.integration.baselineprofiles.consumer.EMPTY_ACTIVITY"
+    }
+}
diff --git a/benchmark/integration-tests/baselineprofiles-test-utils/utils.gradle b/benchmark/integration-tests/baselineprofiles-test-utils/utils.gradle
new file mode 100644
index 0000000..4a0a8dd
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-test-utils/utils.gradle
@@ -0,0 +1,26 @@
+// To trigger the baseline profile generation using the different modules the test will call
+// the base generation task `generateBaselineProfiles`. The `AssertEqualsAndCleanUpTask` asserts
+// that the final output is the expected one and if there are no failures cleans up the
+// generated baseline-prof.txt.
+@CacheableTask
+abstract class AssertEqualsAndCleanUpTask extends DefaultTask {
+    @InputFile
+    @PathSensitive(PathSensitivity.NONE)
+    abstract RegularFileProperty getExpectedFile()
+
+    @InputFile
+    @PathSensitive(PathSensitivity.NONE)
+    abstract RegularFileProperty getActualFile()
+
+    @TaskAction
+    void exec() {
+        assert getExpectedFile().get().asFile.text == getActualFile().get().asFile.text
+
+        // This deletes the actual file since it's a test artifact
+        getActualFile().get().asFile.delete()
+    }
+}
+tasks.register("testBaselineProfilesGeneration", AssertEqualsAndCleanUpTask).configure {
+    it.expectedFile.set(project.layout.projectDirectory.file("src/main/expected-baseline-prof.txt"))
+    it.actualFile.set(tasks.named("generateBaselineProfiles").flatMap { it.baselineProfileFile })
+}
diff --git a/buildSrc-tests/build.gradle b/buildSrc-tests/build.gradle
index 443b0ef..5f63c3b 100644
--- a/buildSrc-tests/build.gradle
+++ b/buildSrc-tests/build.gradle
@@ -53,6 +53,7 @@
     testImplementation(libs.junit)
     testImplementation(libs.truth)
     testImplementation(project(":internal-testutils-gradle-plugin"))
+    testImplementation(project(":internal-testutils-truth"))
     testImplementation(gradleTestKit())
     testImplementation(libs.checkmark)
     testImplementation(libs.kotlinGradlePluginz)
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/buildSrc-tests/src/test/kotlin/androidx/build/LibraryVersionsServiceTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/LibraryVersionsServiceTest.kt
index 892ef6b..fd1a7c1 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/LibraryVersionsServiceTest.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/LibraryVersionsServiceTest.kt
@@ -16,7 +16,10 @@
 
 package androidx.build
 
+import androidx.testutils.assertThrows
 import com.google.common.truth.Truth.assertThat
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.create
 import org.gradle.testfixtures.ProjectBuilder
 import org.junit.Rule
 import org.junit.Test
@@ -57,6 +60,24 @@
     }
 
     @Test
+    fun invalidToml() {
+        val service = createLibraryVersionsService(
+            """
+            [versions]
+            V1 = "1.2.3"
+            [groups]
+            G1 = { group = "g.g1", atomicGroupVersion = "versions.V1" }
+            G1 = { group = "g.g1"}
+        """.trimIndent()
+        )
+        assertThrows<Exception> {
+            service.libraryGroups["G1"]
+        }.hasMessageThat().contains(
+            "libraryversions.toml:line 5, column 1: G1 previously defined at line 4, column 1"
+        )
+    }
+
+    @Test
     fun withMultiplatformVersion() {
         val toml = """
             [versions]
@@ -237,19 +258,168 @@
         ).isEqualTo(null)
     }
 
-    private fun createLibraryVersionsService(
+    @Test
+    fun androidxExtension_noAtomicGroup() {
+        runAndroidExtensionTest(
+            projectPath = "myGroup:project1",
+            tomlFile = """
+                [versions]
+                [groups]
+                G1 = { group = "androidx.myGroup" }
+            """.trimIndent(),
+            validateWithKmp = { extension ->
+                extension.mavenVersion = Version("1.0.0")
+                extension.mavenMultiplatformVersion = Version("1.0.0-dev01")
+                assertThat(
+                    extension.mavenGroup
+                ).isEqualTo(
+                    LibraryGroup("androidx.myGroup", atomicGroupVersion = null)
+                )
+                assertThat(
+                    extension.project.version
+                ).isEqualTo(Version("1.0.0-dev01"))
+                extension.validateMavenVersion()
+            },
+            validateWithoutKmp = { extension ->
+                extension.mavenVersion = Version("1.0.0")
+                extension.mavenMultiplatformVersion = Version("1.0.0-dev01")
+                assertThat(
+                    extension.mavenGroup
+                ).isEqualTo(
+                    LibraryGroup("androidx.myGroup", atomicGroupVersion = null)
+                )
+                assertThat(
+                    extension.project.version
+                ).isEqualTo(Version("1.0.0"))
+                extension.validateMavenVersion()
+            }
+        )
+    }
+
+    @Test
+    fun androidxExtension_noAtomicGroup_setKmpVersionFirst() {
+        runAndroidExtensionTest(
+            projectPath = "myGroup:project1",
+            tomlFile = """
+                [versions]
+                [groups]
+                G1 = { group = "androidx.myGroup" }
+            """.trimIndent(),
+            validateWithKmp = { extension ->
+                extension.mavenMultiplatformVersion = Version("1.0.0-dev01")
+                extension.mavenVersion = Version("1.0.0")
+                assertThat(
+                    extension.mavenGroup
+                ).isEqualTo(
+                    LibraryGroup("androidx.myGroup", atomicGroupVersion = null)
+                )
+                assertThat(
+                    extension.project.version
+                ).isEqualTo(Version("1.0.0-dev01"))
+                extension.validateMavenVersion()
+            },
+            validateWithoutKmp = { extension ->
+                extension.mavenMultiplatformVersion = Version("1.0.0-dev01")
+                extension.mavenVersion = Version("1.0.0")
+                assertThat(
+                    extension.mavenGroup
+                ).isEqualTo(
+                    LibraryGroup("androidx.myGroup", atomicGroupVersion = null)
+                )
+                assertThat(
+                    extension.project.version
+                ).isEqualTo(Version("1.0.0"))
+                extension.validateMavenVersion()
+            }
+        )
+    }
+
+    @Test
+    fun androidxExtension_withAtomicGroup() {
+        runAndroidExtensionTest(
+            projectPath = "myGroup:project1",
+            tomlFile = """
+                [versions]
+                V1 = "1.0.0"
+                V1_KMP = "1.0.0-dev01"
+                [groups]
+                G1 = { group = "androidx.myGroup", atomicGroupVersion = "versions.V1", multiplatformGroupVersion = "versions.V1_KMP" }
+            """.trimIndent(),
+            validateWithKmp = { extension ->
+                assertThat(
+                    extension.mavenGroup
+                ).isEqualTo(
+                    LibraryGroup("androidx.myGroup", atomicGroupVersion = Version("1.0.0-dev01"))
+                )
+                assertThat(
+                    extension.project.version
+                ).isEqualTo(Version("1.0.0-dev01"))
+                extension.validateMavenVersion()
+            },
+            validateWithoutKmp = { extension ->
+                assertThat(
+                    extension.mavenGroup
+                ).isEqualTo(
+                    LibraryGroup("androidx.myGroup", atomicGroupVersion = Version("1.0.0"))
+                )
+                assertThat(
+                    extension.project.version
+                ).isEqualTo(Version("1.0.0"))
+                extension.validateMavenVersion()
+            }
+        )
+    }
+
+    private fun runAndroidExtensionTest(
+        projectPath: String,
         tomlFile: String,
+        validateWithoutKmp: (AndroidXExtension) -> Unit,
+        validateWithKmp: (AndroidXExtension) -> Unit
+    ) {
+        listOf(false, true).forEach { useKmpVersions ->
+            val rootProjectDir = tempDir.newFolder()
+            val rootProject = ProjectBuilder.builder().withProjectDir(
+                rootProjectDir
+            ).build()
+            val subject = ProjectBuilder.builder()
+                .withParent(rootProject)
+                .withName(projectPath)
+                .build()
+            // create the service before extensions are created so that they'll use the test service
+            // we've created.
+            createLibraryVersionsService(
+                tomlFileContents = tomlFile,
+                project = rootProject,
+                useMultiplatformGroupVersions = useKmpVersions
+            )
+            // needed for AndroidXExtension initialization
+            rootProject.setSupportRootFolder(rootProjectDir)
+            // create androidx extensions
+            val extension = subject.extensions
+                .create<AndroidXExtension>(AndroidXImplPlugin.EXTENSION_NAME)
+            if (useKmpVersions) {
+                validateWithKmp(extension)
+            } else {
+                validateWithoutKmp(extension)
+            }
+        }
+    }
+
+    private fun createLibraryVersionsService(
+        tomlFileContents: String,
+        tomlFileName: String = "libraryversions.toml",
         composeCustomVersion: String? = null,
         composeCustomGroup: String? = null,
-        useMultiplatformGroupVersions: Boolean = false
+        useMultiplatformGroupVersions: Boolean = false,
+        project: Project = ProjectBuilder.builder().withProjectDir(tempDir.newFolder()).build()
     ): LibraryVersionsService {
-        val project = ProjectBuilder.builder().withProjectDir(tempDir.newFolder()).build()
         val serviceProvider = project.gradle.sharedServices.registerIfAbsent(
             "libraryVersionsService", LibraryVersionsService::class.java
         ) { spec ->
-            spec.parameters.tomlFile = project.provider {
-                tomlFile
+            spec.parameters.tomlFileContents = project.provider {
+                tomlFileContents
             }
+            spec.parameters.tomlFileName = tomlFileName
             spec.parameters.composeCustomVersion = project.provider {
                 composeCustomVersion
             }
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilderTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilderTest.kt
index ccc54b9..4485180 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilderTest.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilderTest.kt
@@ -126,6 +126,15 @@
     }
 
     @Test
+    fun testValidTestConfigXml_disableDeviceTests() {
+        builder.disableDeviceTests(true)
+        MatcherAssert.assertThat(
+            builder.build(),
+            CoreMatchers.`is`(disableDeviceTestsConfig)
+        )
+    }
+
+    @Test
     fun testValidMediaConfigXml_default() {
         validate(mediaBuilder.build())
     }
@@ -149,6 +158,21 @@
     }
 }
 
+private val disableDeviceTestsConfig = """
+    <?xml version="1.0" encoding="utf-8"?>
+    <!-- Copyright (C) 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.-->
+
+""".trimIndent()
+
 private val goldenDefaultConfig = """
     <?xml version="1.0" encoding="utf-8"?>
     <!-- Copyright (C) 2020 The Android Open Source Project
@@ -168,6 +192,7 @@
     <option name="test-suite-tag" value="placeholder_tag" />
     <option name="config-descriptor:metadata" key="applicationId" value="com.androidx.placeholder.Placeholder" />
     <option name="wifi:disable" value="true" />
+    <option name="instrumentation-arg" key="notAnnotation" value="androidx.test.filters.FlakyTest" />
     <include name="google/unbundled/common/setup" />
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
     <option name="cleanup-apks" value="true" />
@@ -201,6 +226,7 @@
     <option name="test-suite-tag" value="media_compat" />
     <option name="config-descriptor:metadata" key="applicationId" value="com.androidx.client.Placeholder;com.androidx.service.Placeholder" />
     <option name="wifi:disable" value="true" />
+    <option name="instrumentation-arg" key="notAnnotation" value="androidx.test.filters.FlakyTest" />
     <include name="google/unbundled/common/setup" />
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
     <option name="cleanup-apks" value="true" />
diff --git a/buildSrc/jetpad-integration/build.gradle b/buildSrc/jetpad-integration/build.gradle
index 995d9c5..fc0c5cc 100644
--- a/buildSrc/jetpad-integration/build.gradle
+++ b/buildSrc/jetpad-integration/build.gradle
@@ -1 +1,6 @@
 apply plugin:"java"
+
+java {
+    sourceCompatibility = JavaVersion.VERSION_11
+    targetCompatibility = JavaVersion.VERSION_11
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
index be0f9f3..d6ec9a5 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
@@ -16,15 +16,15 @@
 
 package androidx.build
 
+import androidx.build.buildInfo.CreateLibraryBuildInfoFileTask.Companion.getFrameworksSupportCommitShaAtHead
 import androidx.build.checkapi.shouldConfigureApiTasks
 import androidx.build.transform.configureAarAsJarForConfiguration
-import com.android.build.gradle.internal.crash.afterEvaluate
 import groovy.lang.Closure
+import java.io.File
 import org.gradle.api.GradleException
 import org.gradle.api.Project
 import org.gradle.api.provider.Property
 import org.gradle.api.provider.Provider
-import java.io.File
 
 /**
  * Extension for [AndroidXImplPlugin] that's responsible for holding configuration options.
@@ -32,6 +32,7 @@
 open class AndroidXExtension(val project: Project) {
     @JvmField
     val LibraryVersions: Map<String, Version>
+
     @JvmField
     val AllLibraryGroups: List<LibraryGroup>
 
@@ -42,8 +43,11 @@
 
     val listProjectsService: Provider<ListProjectsService>
 
+    private val versionService: LibraryVersionsService
+
     init {
-        val toml = lazyReadFile("libraryversions.toml")
+        val tomlFileName = "libraryversions.toml"
+        val toml = lazyReadFile(tomlFileName)
 
         // These parameters are used when building pre-release binaries for androidxdev.
         // These parameters are only expected to be compatible with :compose:compiler:compiler .
@@ -52,21 +56,19 @@
         // `./gradlew :compose:compiler:compiler:publishToMavenLocal -Pandroidx.versionExtraCheckEnabled=false`
         val composeCustomVersion = project.providers.environmentVariable("COMPOSE_CUSTOM_VERSION")
         val composeCustomGroup = project.providers.environmentVariable("COMPOSE_CUSTOM_GROUP")
-        val useMultiplatformVersions = project.provider {
-            Multiplatform.isKotlinNativeEnabled(project)
-        }
-
         // service that can compute group/version for a project
-        val versionServiceProvider = project.gradle.sharedServices.registerIfAbsent(
+        versionService = project.gradle.sharedServices.registerIfAbsent(
             "libraryVersionsService",
             LibraryVersionsService::class.java
         ) { spec ->
-            spec.parameters.tomlFile = toml
+            spec.parameters.tomlFileName = tomlFileName
+            spec.parameters.tomlFileContents = toml
             spec.parameters.composeCustomVersion = composeCustomVersion
             spec.parameters.composeCustomGroup = composeCustomGroup
-            spec.parameters.useMultiplatformGroupVersions = useMultiplatformVersions
-        }
-        val versionService = versionServiceProvider.get()
+            spec.parameters.useMultiplatformGroupVersions = project.provider {
+                Multiplatform.isKotlinNativeEnabled(project)
+            }
+        }.get()
         AllLibraryGroups = versionService.libraryGroups.values.toList()
         LibraryVersions = versionService.libraryVersions
         libraryGroupsByGroupId = versionService.libraryGroupsByGroupId
@@ -86,12 +88,40 @@
     }
 
     var name: Property<String?> = project.objects.property(String::class.java)
-    fun setName(newName: String) { name.set(newName) }
+    fun setName(newName: String) {
+        name.set(newName)
+    }
+
+    /**
+     * Maven version of the library.
+     *
+     * Note that, setting this is an error if the library group sets an atomic version.
+     * If the build is a multiplatform build, this value will be overridden by
+     * the [mavenMultiplatformVersion] property when it is provided.
+     *
+     * @see mavenMultiplatformVersion
+     */
     var mavenVersion: Version? = null
         set(value) {
             field = value
             chooseProjectVersion()
         }
+        get() = if (versionService.useMultiplatformGroupVersions) {
+            mavenMultiplatformVersion ?: field
+        } else {
+            field
+        }
+
+    /**
+     * If set, this will override the [mavenVersion] property in multiplatform builds.
+     *
+     * @see mavenVersion
+     */
+    var mavenMultiplatformVersion: Version? = null
+        set(value) {
+            field = value
+            chooseProjectVersion()
+        }
 
     fun getAllProjectPathsInSameGroup(): List<String> {
         val allProjectPaths = listProjectsService.get().allPossibleProjectPaths
@@ -149,7 +179,7 @@
         val groupIdText = if (projectPath.startsWith(":external")) {
             projectPath.replace(":external:", "")
         } else {
-	    "androidx.${parentPath.substring(1).replace(':', '.')}"
+            "androidx.${parentPath.substring(1).replace(':', '.')}"
         }
 
         // get the library group having that text
@@ -231,8 +261,10 @@
     fun isVersionSet(): Boolean {
         return versionIsSet
     }
+
     var description: String? = null
     var inceptionYear: String? = null
+
     /**
      * targetsJavaConsumers = true, if project is intended to be accessed from Java-language
      * source code.
@@ -285,7 +317,7 @@
     }
 
     internal fun isPublishConfigured(): Boolean = (
-            publish != Publish.UNSET ||
+        publish != Publish.UNSET ||
             type.publish != Publish.UNSET
         )
 
@@ -308,6 +340,8 @@
 
     var metalavaK2UastEnabled = false
 
+    var disableDeviceTests = false
+
     fun shouldEnforceKotlinStrictApiMode(): Boolean {
         return !legacyDisableKotlinStrictApiMode &&
             shouldConfigureApiTasks()
@@ -327,6 +361,12 @@
         configureAarAsJarForConfiguration(project, name)
     }
 
+    fun getReferenceSha(): Provider<String> {
+        return project.providers.provider {
+            project.getFrameworksSupportCommitShaAtHead()
+        }
+    }
+
     companion object {
         const val DEFAULT_UNSPECIFIED_VERSION = "unspecified"
     }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index cea59f9..edf3185 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -29,6 +29,7 @@
 import androidx.build.checkapi.KmpApiTaskConfig
 import androidx.build.checkapi.LibraryApiTaskConfig
 import androidx.build.checkapi.configureProjectForApiTasks
+import androidx.build.dependencies.KOTLIN_VERSION
 import androidx.build.dependencyTracker.AffectedModuleDetector
 import androidx.build.docs.AndroidXKmpDocsImplPlugin
 import androidx.build.gradle.isRoot
@@ -125,6 +126,7 @@
         }
 
         project.configureKtlint()
+        project.configureKotlinStdlibVersion()
 
         // Configure all Jar-packing tasks for hermetic builds.
         project.tasks.withType(Zip::class.java).configureEach { it.configureForHermeticBuild() }
@@ -381,6 +383,20 @@
         }
     }
 
+    fun Project.configureKotlinStdlibVersion() {
+        project.configurations.all { configuration ->
+            configuration.resolutionStrategy { strategy ->
+                strategy.eachDependency { details ->
+                    if (details.requested.group == "org.jetbrains.kotlin" &&
+                        (details.requested.name == "kotlin-stdlib-jdk7" ||
+                            details.requested.name == "kotlin-stdlib-jdk8")) {
+                        details.useVersion(KOTLIN_VERSION)
+                    }
+                }
+            }
+        }
+    }
+
     @Suppress("UnstableApiUsage", "DEPRECATION") // AGP DSL APIs
     private fun configureWithLibraryPlugin(
         project: Project,
@@ -670,6 +686,7 @@
         }
 
         project.configureTestConfigGeneration(this)
+        project.configureFtlRunner()
 
         val buildTestApksTask = project.rootProject.tasks.named(BUILD_TEST_APKS_TASK)
         when (this) {
@@ -824,6 +841,7 @@
         }
 
         project.addAppApkToTestConfigGeneration()
+        project.addAppApkToFtlRunner()
 
         val buildTestApksTask = project.rootProject.tasks.named(BUILD_TEST_APKS_TASK)
         applicationVariants.all { variant ->
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt b/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt
new file mode 100644
index 0000000..b5b8118
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build
+
+import com.android.build.api.artifact.Artifacts
+import com.android.build.api.artifact.SingleArtifact
+import com.android.build.api.variant.AndroidComponentsExtension
+import com.android.build.api.variant.ApplicationAndroidComponentsExtension
+import com.android.build.api.variant.BuiltArtifactsLoader
+import com.android.build.api.variant.HasAndroidTest
+import javax.inject.Inject
+import org.gradle.api.DefaultTask
+import org.gradle.api.Project
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.api.tasks.options.Option
+import org.gradle.kotlin.dsl.getByType
+import org.gradle.process.ExecOperations
+import org.gradle.work.DisableCachingByDefault
+
+@DisableCachingByDefault(because = "Expected to rerun every time")
+abstract class FtlRunner : DefaultTask() {
+    init {
+        group = "Verification"
+        description = "Runs devices tests in Firebase Test Lab filtered by --className"
+    }
+
+    @get:Inject
+    abstract val execOperations: ExecOperations
+
+    @get:InputFiles
+    @get:PathSensitive(PathSensitivity.RELATIVE)
+    abstract val testFolder: DirectoryProperty
+
+    @get:Internal
+    abstract val testLoader: Property<BuiltArtifactsLoader>
+
+    @get:InputFiles
+    @get:PathSensitive(PathSensitivity.RELATIVE)
+    @get:Optional
+    abstract val appFolder: DirectoryProperty
+
+    @get:Internal
+    abstract val appLoader: Property<BuiltArtifactsLoader>
+
+    @get:Optional
+    @get:Input
+    @get:Option(option = "className", description = "Fully qualified class name of a class to run")
+    abstract val className: Property<String>
+
+    @get:Optional
+    @get:Input
+    @get:Option(option = "packageName", description = "Package name test classes to run")
+    abstract val packageName: Property<String>
+
+    @get:Input
+    abstract val device: Property<String>
+
+    @TaskAction
+    fun execThings() {
+        val testApk = testLoader.get().load(testFolder.get())
+            ?: throw RuntimeException("Cannot load required APK for task: $name")
+        val testApkPath = testApk.elements.single().outputFile
+        val appApkPath = if (appLoader.isPresent) {
+            val appApk = appLoader.get().load(appFolder.get())
+                ?: throw RuntimeException("Cannot load required APK for task: $name")
+            appApk.elements.single().outputFile
+        } else {
+            "gs://androidx-ftl-test-results/github-ci-action/placeholderApp/" +
+                "aadb5e0219ce132e73236ef1e06bb50dd60217e20e803ea00d57a1cf1cea902c.apk"
+        }
+        try {
+            execOperations.exec {
+                    it.commandLine("gcloud", "--version")
+            }
+        } catch (exception: Exception) {
+            throw Exception(
+                "Missing gcloud, please follow go/androidx-dev#remote-build-cache to set it up"
+            )
+        }
+        val hasFilters = className.isPresent || packageName.isPresent
+        val filters = listOfNotNull(
+            if (className.isPresent) "class ${className.get()}" else null,
+            if (packageName.isPresent) "package ${packageName.get()}" else null,
+        ).joinToString(separator = ",")
+        execOperations.exec {
+            it.commandLine(
+                listOfNotNull(
+                    "gcloud",
+                    "--project",
+                    "androidx-dev-prod",
+                    "firebase",
+                    "test",
+                    "android",
+                    "run",
+                    "--type",
+                    "instrumentation",
+                    "--no-performance-metrics",
+                    "--no-auto-google-login",
+                    "--device",
+                    "model=${device.get()},locale=en_US,orientation=portrait",
+                    "--app",
+                    appApkPath,
+                    "--test",
+                    testApkPath,
+                    if (hasFilters) "--test-targets" else null,
+                    if (hasFilters) filters else null,
+                )
+            )
+        }
+    }
+}
+
+private val devicesToRunOn = listOf(
+    "ftlpixel2api33" to "Pixel2.arm,version=33",
+    "ftlpixel2api30" to "Pixel2.arm,version=30",
+    "ftlnexus4api21" to "Nexus4,version=21",
+)
+
+fun Project.configureFtlRunner() {
+    extensions.getByType(AndroidComponentsExtension::class.java).apply {
+        onVariants { variant ->
+            var name: String? = null
+            var artifacts: Artifacts? = null
+            when {
+                variant is HasAndroidTest -> {
+                    name = variant.androidTest?.name
+                    artifacts = variant.androidTest?.artifacts
+                }
+
+                project.plugins.hasPlugin("com.android.test") -> {
+                    name = variant.name
+                    artifacts = variant.artifacts
+                }
+            }
+            if (name == null || artifacts == null) {
+                return@onVariants
+            }
+            devicesToRunOn.forEach { (taskPrefix, model) ->
+                tasks.register("$taskPrefix$name", FtlRunner::class.java) { task ->
+                    task.device.set(model)
+                    task.testFolder.set(artifacts.get(SingleArtifact.APK))
+                    task.testLoader.set(artifacts.getBuiltArtifactsLoader())
+                }
+            }
+        }
+    }
+}
+
+fun Project.addAppApkToFtlRunner() {
+    extensions.getByType<ApplicationAndroidComponentsExtension>().apply {
+        onVariants(selector().withBuildType("debug")) { appVariant ->
+            devicesToRunOn.forEach { (taskPrefix, _) ->
+                tasks.named("$taskPrefix${appVariant.name}AndroidTest") { configTask ->
+                    configTask as FtlRunner
+                    configTask.appFolder.set(appVariant.artifacts.get(SingleArtifact.APK))
+                    configTask.appLoader.set(appVariant.artifacts.getBuiltArtifactsLoader())
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt b/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt
index 2885e3b..dbd9266 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt
@@ -29,16 +29,27 @@
  */
 abstract class LibraryVersionsService : BuildService<LibraryVersionsService.Parameters> {
     interface Parameters : BuildServiceParameters {
-        var tomlFile: Provider<String>
+        var tomlFileName: String
+        var tomlFileContents: Provider<String>
         var composeCustomVersion: Provider<String>
         var composeCustomGroup: Provider<String>
         var useMultiplatformGroupVersions: Provider<Boolean>
     }
 
     private val parsedTomlFile: TomlParseResult by lazy {
-        Toml.parse(parameters.tomlFile.get())
+        val result = Toml.parse(parameters.tomlFileContents.get())
+        if (result.hasErrors()) {
+            val issues = result.errors().map {
+                "${parameters.tomlFileName}:${it.position()}: ${it.message}"
+            }.joinToString(separator = "\n")
+            throw Exception("${parameters.tomlFileName} file has issues.\n$issues")
+        }
+        result
     }
 
+    val useMultiplatformGroupVersions
+        get() = parameters.useMultiplatformGroupVersions.get()
+
     private fun getTable(key: String): TomlTable {
         return parsedTomlFile.getTable(key)
             ?: throw GradleException("Library versions toml file is missing [$key] table")
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaPartialDocsTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaPartialDocsTask.kt
index d6f7f70..51dd0a1 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaPartialDocsTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaPartialDocsTask.kt
@@ -255,7 +255,7 @@
                             // This is a workaround for https://youtrack.jetbrains.com/issue/KT-33893
                             @Suppress("DEPRECATION") // for compatibility
                             (compilation.compileKotlinTask as
-                                org.jetbrains.kotlin.gradle.tasks.KotlinCompile).classpath
+                                org.jetbrains.kotlin.gradle.tasks.KotlinCompile).libraries
                         } else {
                             compilation.compileDependencyFiles
                         }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt
index 9ad420c..d5481b8 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt
@@ -20,6 +20,7 @@
     var appApkName: String? = null
     lateinit var applicationId: String
     var isBenchmark: Boolean = false
+    var disableDeviceTests: Boolean = false
     var isPostsubmit: Boolean = true
     lateinit var minSdk: String
     var runAllTests: Boolean = true
@@ -31,6 +32,8 @@
     fun appApkName(appApkName: String) = apply { this.appApkName = appApkName }
     fun applicationId(applicationId: String) = apply { this.applicationId = applicationId }
     fun isBenchmark(isBenchmark: Boolean) = apply { this.isBenchmark = isBenchmark }
+    fun disableDeviceTests(disableDeviceTests: Boolean) =
+        apply { this.disableDeviceTests = disableDeviceTests }
     fun isPostsubmit(isPostsubmit: Boolean) = apply { this.isPostsubmit = isPostsubmit }
     fun minSdk(minSdk: String) = apply { this.minSdk = minSdk }
     fun runAllTests(runAllTests: Boolean) = apply { this.runAllTests = runAllTests }
@@ -42,13 +45,18 @@
     fun build(): String {
         val sb = StringBuilder()
         sb.append(XML_HEADER_AND_LICENSE)
-            .append(CONFIGURATION_OPEN)
+        if (disableDeviceTests) {
+            return sb.toString()
+        }
+
+        sb.append(CONFIGURATION_OPEN)
             .append(MIN_API_LEVEL_CONTROLLER_OBJECT.replace("MIN_SDK", minSdk))
         tags.forEach { tag ->
             sb.append(TEST_SUITE_TAG_OPTION.replace("TEST_SUITE_TAG", tag))
         }
         sb.append(MODULE_METADATA_TAG_OPTION.replace("APPLICATION_ID", applicationId))
             .append(WIFI_DISABLE_OPTION)
+            .append(FLAKY_TEST_OPTION)
         if (isBenchmark) {
             if (isPostsubmit) {
                 sb.append(BENCHMARK_POSTSUBMIT_OPTIONS)
@@ -77,20 +85,11 @@
             .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
             .append(PACKAGE_OPTION.replace("APPLICATION_ID", applicationId))
         if (runAllTests) {
-            if (!isPostsubmit) {
-                sb.append(FLAKY_TEST_OPTION)
-            }
             sb.append(TEST_BLOCK_CLOSE)
         } else {
-            if (!isPostsubmit) {
-                sb.append(FLAKY_TEST_OPTION)
-            }
             sb.append(SMALL_TEST_OPTIONS)
                 .append(TEST_BLOCK_CLOSE)
                 .append(TEST_BLOCK_OPEN)
-            if (!isPostsubmit) {
-                sb.append(FLAKY_TEST_OPTION)
-            }
             sb.append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
                 .append(PACKAGE_OPTION.replace("APPLICATION_ID", applicationId))
                 .append(MEDIUM_TEST_OPTIONS)
@@ -162,33 +161,25 @@
             )
         )
             .append(WIFI_DISABLE_OPTION)
+            .append(FLAKY_TEST_OPTION)
             .append(SETUP_INCLUDE)
             .append(MEDIA_TARGET_PREPARER_OPEN)
             .append(APK_INSTALL_OPTION.replace("APK_NAME", clientApkName))
             .append(APK_INSTALL_OPTION.replace("APK_NAME", serviceApkName))
-        sb.append(TARGET_PREPARER_CLOSE)
+            .append(TARGET_PREPARER_CLOSE)
             .append(TEST_BLOCK_OPEN)
             .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
             .append(PACKAGE_OPTION.replace("APPLICATION_ID", clientApplicationId))
             .append(mediaInstrumentationArgs())
         if (runAllTests) {
-            if (!isPostsubmit) {
-                sb.append(FLAKY_TEST_OPTION)
-            }
             sb.append(TEST_BLOCK_CLOSE)
                 .append(TEST_BLOCK_OPEN)
                 .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
                 .append(PACKAGE_OPTION.replace("APPLICATION_ID", serviceApplicationId))
                 .append(mediaInstrumentationArgs())
-            if (!isPostsubmit) {
-                sb.append(FLAKY_TEST_OPTION)
-            }
             sb.append(TEST_BLOCK_CLOSE)
         } else {
             // add the small and medium test runners for both client and service apps
-            if (!isPostsubmit) {
-                sb.append(FLAKY_TEST_OPTION)
-            }
             sb.append(SMALL_TEST_OPTIONS)
                 .append(TEST_BLOCK_CLOSE)
                 .append(TEST_BLOCK_OPEN)
@@ -196,28 +187,19 @@
                 .append(PACKAGE_OPTION.replace("APPLICATION_ID", clientApplicationId))
                 .append(mediaInstrumentationArgs())
                 .append(MEDIUM_TEST_OPTIONS)
-            if (!isPostsubmit) {
-                sb.append(FLAKY_TEST_OPTION)
-            }
-            sb.append(TEST_BLOCK_CLOSE)
+                .append(TEST_BLOCK_CLOSE)
                 .append(TEST_BLOCK_OPEN)
                 .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
                 .append(PACKAGE_OPTION.replace("APPLICATION_ID", serviceApplicationId))
                 .append(mediaInstrumentationArgs())
                 .append(SMALL_TEST_OPTIONS)
-            if (!isPostsubmit) {
-                sb.append(FLAKY_TEST_OPTION)
-            }
-            sb.append(TEST_BLOCK_CLOSE)
+                .append(TEST_BLOCK_CLOSE)
                 .append(TEST_BLOCK_OPEN)
                 .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
                 .append(PACKAGE_OPTION.replace("APPLICATION_ID", serviceApplicationId))
                 .append(mediaInstrumentationArgs())
                 .append(MEDIUM_TEST_OPTIONS)
-            if (!isPostsubmit) {
-                sb.append(FLAKY_TEST_OPTION)
-            }
-            sb.append(TEST_BLOCK_CLOSE)
+                .append(TEST_BLOCK_CLOSE)
         }
         sb.append(CONFIGURATION_CLOSE)
         return sb.toString()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
index 0d58d11..5247066 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
@@ -72,6 +72,9 @@
     abstract val hasBenchmarkPlugin: Property<Boolean>
 
     @get:Input
+    abstract val disableDeviceTests: Property<Boolean>
+
+    @get:Input
     @get:Optional
     abstract val benchmarkRunAlsoInterpreted: Property<Boolean>
 
@@ -176,41 +179,44 @@
             }
         }
         // This section adds metadata tags that will help filter runners to specific modules.
-        if (hasBenchmarkPlugin.get()) {
-            configBuilder.isBenchmark(true)
-            if (configBuilder.isPostsubmit) {
-                if (benchmarkRunAlsoInterpreted.get()) {
-                    configBuilder.tag("microbenchmarks_interpreted")
-                }
-                configBuilder.tag("microbenchmarks")
-            } else {
-                // in presubmit, we treat micro benchmarks as regular correctness tests as
-                // they run with dryRunMode to check crashes don't happen, without measurement
-                configBuilder.tag("androidx_unit_tests")
-            }
-        } else if (testProjectPath.get().endsWith("macrobenchmark")) {
-            // macro benchmarks do not have a dryRunMode, so we don't run them in presubmit
-            configBuilder.tag("macrobenchmarks")
+        if (disableDeviceTests.get()) {
+            configBuilder.disableDeviceTests(true)
         } else {
-            configBuilder.tag("androidx_unit_tests")
-            if (testProjectPath.get().startsWith(":compose:")) {
-                configBuilder.tag("compose")
-            } else if (testProjectPath.get().startsWith(":wear:")) {
-                configBuilder.tag("wear")
+            if (hasBenchmarkPlugin.get()) {
+                configBuilder.isBenchmark(true)
+                if (configBuilder.isPostsubmit) {
+                    if (benchmarkRunAlsoInterpreted.get()) {
+                        configBuilder.tag("microbenchmarks_interpreted")
+                    }
+                    configBuilder.tag("microbenchmarks")
+                } else {
+                    // in presubmit, we treat micro benchmarks as regular correctness tests as
+                    // they run with dryRunMode to check crashes don't happen, without measurement
+                    configBuilder.tag("androidx_unit_tests")
+                }
+            } else if (testProjectPath.get().endsWith("macrobenchmark")) {
+                // macro benchmarks do not have a dryRunMode, so we don't run them in presubmit
+                configBuilder.tag("macrobenchmarks")
+            } else {
+                configBuilder.tag("androidx_unit_tests")
+                if (testProjectPath.get().startsWith(":compose:")) {
+                    configBuilder.tag("compose")
+                } else if (testProjectPath.get().startsWith(":wear:")) {
+                    configBuilder.tag("wear")
+                }
             }
+            val testApk = testLoader.get().load(testFolder.get())
+                ?: throw RuntimeException("Cannot load required APK for task: $name")
+            val testApkBuiltArtifact = testApk.elements.single()
+            val testName = testApkBuiltArtifact.outputFile
+                .substringAfterLast("/")
+                .renameApkForTesting(testProjectPath.get(), hasBenchmarkPlugin.get())
+            configBuilder.testApkName(testName)
+                .applicationId(testApk.applicationId)
+                .minSdk(minSdk.get().toString())
+                .testRunner(testRunner.get())
+            testApkSha256Report.addFile(testName, testApkBuiltArtifact)
         }
-        val testApk = testLoader.get().load(testFolder.get())
-            ?: throw RuntimeException("Cannot load required APK for task: $name")
-        val testApkBuiltArtifact = testApk.elements.single()
-        val testName = testApkBuiltArtifact.outputFile
-            .substringAfterLast("/")
-            .renameApkForTesting(testProjectPath.get(), hasBenchmarkPlugin.get())
-        configBuilder.testApkName(testName)
-            .applicationId(testApk.applicationId)
-            .minSdk(minSdk.get().toString())
-            .testRunner(testRunner.get())
-        testApkSha256Report.addFile(testName, testApkBuiltArtifact)
-
         val resolvedOutputFile: File = outputFile.asFile.get()
         if (!resolvedOutputFile.exists()) {
             if (!resolvedOutputFile.createNewFile()) {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index 5f200c6..040bfcc 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -76,6 +76,8 @@
         "${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}$variantName",
         GenerateTestConfigurationTask::class.java
     ) { task ->
+        val androidXExtension = extensions.getByType<AndroidXExtension>()
+
         task.testFolder.set(artifacts.get(SingleArtifact.APK))
         task.testLoader.set(artifacts.getBuiltArtifactsLoader())
         task.outputXml.fileValue(File(getTestConfigDirectory(), xmlName))
@@ -91,12 +93,11 @@
         } else {
             task.minSdk.set(minSdk)
         }
+        task.disableDeviceTests.set(androidXExtension.disableDeviceTests)
         val hasBenchmarkPlugin = hasBenchmarkPlugin()
         task.hasBenchmarkPlugin.set(hasBenchmarkPlugin)
         if (hasBenchmarkPlugin) {
-            task.benchmarkRunAlsoInterpreted.set(
-                extensions.getByType<AndroidXExtension>().benchmarkRunAlsoInterpreted
-            )
+            task.benchmarkRunAlsoInterpreted.set(androidXExtension.benchmarkRunAlsoInterpreted)
         }
         task.testRunner.set(testRunner)
         task.testProjectPath.set(path)
@@ -333,6 +334,8 @@
     val configTask = getOrCreateMacrobenchmarkConfigTask(variantName)
     if (path.endsWith("macrobenchmark")) {
         configTask.configure { task ->
+            val androidXExtension = extensions.getByType<AndroidXExtension>()
+
             task.testFolder.set(artifacts.get(SingleArtifact.APK))
             task.testLoader.set(artifacts.getBuiltArtifactsLoader())
             task.outputXml.fileValue(
@@ -360,6 +363,7 @@
                 )
             )
             task.minSdk.set(minSdk)
+            task.disableDeviceTests.set(androidXExtension.disableDeviceTests)
             task.hasBenchmarkPlugin.set(this.hasBenchmarkPlugin())
             task.testRunner.set(testRunner)
             task.testProjectPath.set(this.path)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt b/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
index 5d0f712..52c0b20 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
@@ -118,7 +118,16 @@
     ":lint-checks:integration-tests:copyDebugAndroidLintReports",
 
     // https://youtrack.jetbrains.com/issue/KT-49933
-    "generateProjectStructureMetadata"
+    "generateProjectStructureMetadata",
+
+    // https://github.com/google/protobuf-gradle-plugin/issues/667
+    ":datastore:datastore-preferences-proto:extractIncludeTestProto",
+    ":glance:glance-appwidget-proto:extractIncludeTestProto",
+    ":health:connect:connect-client-proto:extractIncludeTestProto",
+    ":privacysandbox:tools:tools-core:extractIncludeTestProto",
+    ":test:screenshot:screenshot-proto:extractIncludeTestProto",
+    ":wear:protolayout:protolayout-proto:extractIncludeTestProto",
+    ":wear:tiles:tiles-proto:extractIncludeTestProto"
 )
 
 // Additional tasks that are expected to be temporarily out-of-date after running once
diff --git a/buildSrc/public/build.gradle b/buildSrc/public/build.gradle
index e37e156..9ccda13 100644
--- a/buildSrc/public/build.gradle
+++ b/buildSrc/public/build.gradle
@@ -1,19 +1,31 @@
 apply from: "../shared.gradle"
 
 sourceSets {
+
+    // Benchmark
     main.java.srcDirs += "${supportRootFolder}/benchmark/gradle-plugin/src/main/kotlin"
     main.resources.srcDirs += "${supportRootFolder}/benchmark/gradle-plugin/src/main/resources"
 
+    // Benchmark darwin
     main.java.srcDirs += "${supportRootFolder}/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin"
     main.resources.srcDirs += "${supportRootFolder}/benchmark/benchmark-darwin-gradle-plugin/src/main/resources"
 
+    // Baseline profile
+    main.java.srcDirs += "${supportRootFolder}" +
+            "/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin"
+    main.resources.srcDirs += "${supportRootFolder}" +
+            "/benchmark/baseline-profiles-gradle-plugin/src/main/resources"
+
+    // Inspection
     main.java.srcDirs += "${supportRootFolder}/inspection/inspection-gradle-plugin/src/main/kotlin"
     main.resources.srcDirs += "${supportRootFolder}/inspection/inspection-gradle-plugin/src/main" +
             "/resources"
 
+    // Compose
     main.java.srcDirs += "${supportRootFolder}/compose/material/material/icons/generator/src/main" +
             "/kotlin"
 
+    // Glance
     main.java.srcDirs += "${supportRootFolder}/glance/glance-appwidget/glance-layout-generator/" +
             "src/main/kotlin"
 }
@@ -29,6 +41,18 @@
             id = "androidx.benchmark"
             implementationClass = "androidx.benchmark.gradle.BenchmarkPlugin"
         }
+        baselineProfilesProducer {
+            id = "androidx.baselineprofiles.producer"
+            implementationClass = "androidx.baselineprofiles.gradle.producer.BaselineProfilesProducerPlugin"
+        }
+        baselineProfilesConsumer {
+            id = "androidx.baselineprofiles.consumer"
+            implementationClass = "androidx.baselineprofiles.gradle.consumer.BaselineProfilesConsumerPlugin"
+        }
+        baselineProfilesBuildProvider {
+            id = "androidx.baselineprofiles.buildprovider"
+            implementationClass = "androidx.baselineprofiles.gradle.buildprovider.BaselineProfilesBuildProviderPlugin"
+        }
         inspection {
             id = "androidx.inspection"
             implementationClass = "androidx.inspection.gradle.InspectionPlugin"
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt b/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
index abc2e63..51626cc 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
@@ -33,7 +33,7 @@
      * Either an integer value or a pre-release platform code, prefixed with "android-" (ex.
      * "android-28" or "android-Q") as you would see within the SDK's platforms directory.
      */
-    const val COMPILE_SDK_VERSION = "android-33"
+    const val COMPILE_SDK_VERSION = "android-33-ext4"
 
     /**
      * The Android SDK version to use for targetSdkVersion meta-data.
diff --git a/buildSrc/shared.gradle b/buildSrc/shared.gradle
index c14eb49..c96dfa4 100644
--- a/buildSrc/shared.gradle
+++ b/buildSrc/shared.gradle
@@ -46,6 +46,9 @@
     implementation(libs.kotlinPoet) // needed to compile material-icon-generator
     implementation(libs.xmlpull) // needed to compile material-icon-generator
 
+    implementation(libs.protobuf) // needed to compile baseline-profile gradle plugins
+    implementation(libs.agpTestingPlatformCoreProto) // needed to compile baseline-profile gradle plugins
+
     // dependencies that aren't used by buildSrc directly but that we resolve here so that the
     // root project doesn't need to re-resolve them and their dependencies on every build
     runtimeOnly(libs.hiltAndroidGradlePluginz)
@@ -54,6 +57,11 @@
     runtimeOnly(libs.wireGradlePluginz)
 }
 
+java {
+    sourceCompatibility = JavaVersion.VERSION_11
+    targetCompatibility = JavaVersion.VERSION_11
+}
+
 project.tasks.withType(Jar) { task ->
     task.reproducibleFileOrder = true
     task.preserveFileTimestamps = false
diff --git a/busytown/androidx_multiplatform_mac.sh b/busytown/androidx_multiplatform_mac.sh
index d7f9541..128e691 100755
--- a/busytown/androidx_multiplatform_mac.sh
+++ b/busytown/androidx_multiplatform_mac.sh
@@ -15,7 +15,7 @@
 # Setup simulators
 impl/androidx-native-mac-simulator-setup.sh
 
-impl/build.sh buildOnServer allTests :docs-kmp:zipCombinedKmpDocs --no-configuration-cache -Pandroidx.displayTestOutput=false
+impl/build.sh buildOnServer :docs-kmp:zipCombinedKmpDocs --no-configuration-cache -Pandroidx.displayTestOutput=false
 
 # run a separate createArchive task to prepare a repository
 # folder in DIST.
diff --git a/busytown/androidx_multiplatform_mac_host_tests.sh b/busytown/androidx_multiplatform_mac_host_tests.sh
new file mode 120000
index 0000000..aa12d53
--- /dev/null
+++ b/busytown/androidx_multiplatform_mac_host_tests.sh
@@ -0,0 +1 @@
+androidx_multiplatform_host_tests_mac.sh
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/OWNERS b/camera/camera-camera2-pipe-integration/OWNERS
index 871060e..c445688 100644
--- a/camera/camera-camera2-pipe-integration/OWNERS
+++ b/camera/camera-camera2-pipe-integration/OWNERS
@@ -1,3 +1,4 @@
+# Bug component: 575599
 codelogic@google.com
 sushilnath@google.com
 lnishan@google.com
diff --git a/camera/camera-camera2-pipe-integration/build.gradle b/camera/camera-camera2-pipe-integration/build.gradle
index 77c3f3f..0161db3 100644
--- a/camera/camera-camera2-pipe-integration/build.gradle
+++ b/camera/camera-camera2-pipe-integration/build.gradle
@@ -71,6 +71,7 @@
     testImplementation(libs.junit)
     testImplementation(libs.truth)
     testImplementation(libs.mockitoCore4)
+    testImplementation(libs.mockitoKotlin4)
     testImplementation(libs.robolectric)
     testImplementation(libs.kotlinCoroutinesTest)
     testImplementation(project(":camera:camera-camera2-pipe-testing"))
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt
index e2e53b7..e5bccd9 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt
@@ -44,6 +44,7 @@
 import androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions
 import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
 import androidx.camera.camera2.pipe.testing.VerifyResultListener
+import androidx.camera.core.CameraControl
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.FocusMeteringAction
 import androidx.camera.core.ImageAnalysis
@@ -59,6 +60,8 @@
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.concurrent.ExecutionException
 import java.util.concurrent.TimeUnit
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.asExecutor
@@ -67,6 +70,7 @@
 import org.hamcrest.CoreMatchers.equalTo
 import org.hamcrest.CoreMatchers.notNullValue
 import org.junit.After
+import org.junit.Assert
 import org.junit.Assume
 import org.junit.Assume.assumeThat
 import org.junit.Before
@@ -336,12 +340,25 @@
         )
     }
 
+    @Test
+    fun setZoomRatio_operationCanceledExceptionIfNoUseCase() {
+        assertFutureFailedWithOperationCancellation(cameraControl.setZoomRatio(1.5f))
+    }
+
+    private fun <T> assertFutureFailedWithOperationCancellation(future: ListenableFuture<T>) {
+        Assert.assertThrows(ExecutionException::class.java) {
+            future[3, TimeUnit.SECONDS]
+        }.apply {
+            Truth.assertThat(cause)
+                .isInstanceOf(CameraControl.OperationCanceledException::class.java)
+        }
+    }
+
     private fun CameraCharacteristics.getMaxRegionCount(
         option_max_regions: CameraCharacteristics.Key<Int>
     ) = get(option_max_regions) ?: 0
 
     private suspend fun arrangeRequestOptions() {
-        cameraControl.setExposureCompensationIndex(1)
         cameraControl.setZoomRatio(1.0f)
         cameraControl.camera2cameraControl.setCaptureRequestOptions(
             CaptureRequestOptions.Builder().setCaptureRequestOption(
@@ -349,6 +366,7 @@
                 CONTROL_CAPTURE_INTENT_CUSTOM
             ).build()
         ).await()
+        cameraControl.setExposureCompensationIndex(1)[5, TimeUnit.SECONDS]
 
         // Ensure the requests are already set to the CaptureRequest.
         waitForResult().verify(
@@ -418,4 +436,4 @@
         )
         cameraControl = camera.cameraControl as CameraControlAdapter
     }
-}
\ No newline at end of file
+}
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/androidTest/java/androidx/camera/camera2/pipe/testing/VerifyResultListener.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/VerifyResultListener.kt
index 7469d0e..1c65179 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/VerifyResultListener.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/VerifyResultListener.kt
@@ -68,8 +68,12 @@
             signal.completeExceptionally(failureException)
             return
         }
-        if (_verifyBlock(requestMetadata, result)) {
-            signal.complete(Unit)
+        try {
+            if (_verifyBlock(requestMetadata, result)) {
+                signal.complete(Unit)
+            }
+        } catch (e: Throwable) {
+            signal.completeExceptionally(e)
         }
     }
 
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
index d19ee81..9f5dbc4 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
@@ -47,11 +47,9 @@
 import androidx.camera.core.impl.utils.futures.Futures
 import com.google.common.util.concurrent.ListenableFuture
 import javax.inject.Inject
-import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.async
 import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.launch
 
 /**
  * Adapt the [CameraControlInternal] interface to [CameraPipe].
@@ -118,19 +116,8 @@
         )
     }
 
-    override fun setZoomRatio(ratio: Float): ListenableFuture<Void> {
-        return threads.scope.launch(start = CoroutineStart.UNDISPATCHED) {
-            useCaseManager.camera?.let {
-                zoomControl.zoomRatio = ratio
-                val zoomValue = ZoomValue(
-                    ratio,
-                    zoomControl.minZoom,
-                    zoomControl.maxZoom
-                )
-                cameraControlStateAdapter.setZoomState(zoomValue)
-            }
-        }.asListenableFuture()
-    }
+    override fun setZoomRatio(ratio: Float): ListenableFuture<Void> =
+        zoomControl.setZoomRatioAsync(ratio)
 
     override fun setLinearZoom(linearZoom: Float): ListenableFuture<Void> {
         val ratio = zoomControl.toZoomRatio(linearZoom)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlStateAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlStateAdapter.kt
index 6e3e98c..6abf608 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlStateAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlStateAdapter.kt
@@ -25,10 +25,7 @@
 import androidx.camera.core.ExposureState
 import androidx.camera.core.ZoomState
 import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
 import javax.inject.Inject
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
 
 /**
  * [CameraControlStateAdapter] caches and updates based on callbacks from the active CameraGraph.
@@ -44,24 +41,9 @@
     val torchStateLiveData: LiveData<Int>
         get() = torchControl.torchStateLiveData
 
-    private val _zoomState by lazy {
-        MutableLiveData<ZoomState>(
-            ZoomValue(
-                zoomControl.zoomRatio,
-                zoomControl.minZoom,
-                zoomControl.maxZoom
-            )
-        )
-    }
     val zoomStateLiveData: LiveData<ZoomState>
-        get() = _zoomState
-
-    suspend fun setZoomState(value: ZoomState) {
-        withContext(Dispatchers.Main) {
-            _zoomState.value = value
-        }
-    }
+        get() = zoomControl.zoomStateLiveData
 
     val exposureState: ExposureState
         get() = evCompControl.exposureState
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
index 9c7b54f..4c03b51 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
@@ -27,10 +27,9 @@
 import androidx.camera.camera2.pipe.integration.config.CameraAppComponent
 import androidx.camera.core.impl.AttachedSurfaceInfo
 import androidx.camera.core.impl.CameraDeviceSurfaceManager
+import androidx.camera.core.impl.StreamSpec
 import androidx.camera.core.impl.SurfaceConfig
 import androidx.camera.core.impl.UseCaseConfig
-import androidx.core.util.Preconditions
-import kotlinx.coroutines.runBlocking
 
 /**
  * Adapt the [CameraDeviceSurfaceManager] interface to [CameraPipe].
@@ -59,13 +58,13 @@
         context: Context,
         availableCameraIds: Set<String>
     ) {
-        Preconditions.checkNotNull(context)
         for (cameraId in availableCameraIds) {
+            val cameraMetadata =
+                component.getCameraDevices().awaitCameraMetadata(CameraId(cameraId))
             supportedSurfaceCombinationMap[cameraId] =
                 SupportedSurfaceCombination(
                     context,
-                    runBlocking { component.getCameraDevices().awaitMetadata(CameraId(cameraId)) },
-                    cameraId,
+                    checkNotNull(cameraMetadata),
                     CamcorderProfileProviderAdapter(cameraId)
                 )
         }
@@ -122,25 +121,24 @@
     }
 
     /**
-     * Retrieves a map of suggested resolutions for the given list of use cases.
+     * Retrieves a map of suggested stream specifications for the given list of use cases.
      *
      * @param cameraId          the camera id of the camera device used by the use cases
      * @param existingSurfaces  list of surfaces already configured and used by the camera. The
      *                          resolutions for these surface can not change.
      * @param newUseCaseConfigs list of configurations of the use cases that will be given a
-     *                          suggested resolution
-     * @return map of suggested resolutions for given use cases
+     *                          suggested stream specification
+     * @return map of suggested stream specifications for given use cases
      * @throws IllegalArgumentException if {@code newUseCaseConfigs} is an empty list, if
      *                                  there isn't a supported combination of surfaces
      *                                  available, or if the {@code cameraId}
      *                                  is not a valid id.
      */
-    override fun getSuggestedResolutions(
+    override fun getSuggestedStreamSpecs(
         cameraId: String,
         existingSurfaces: List<AttachedSurfaceInfo>,
         newUseCaseConfigs: List<UseCaseConfig<*>>
-    ): Map<UseCaseConfig<*>, Size> {
-        checkIfSupportedCombinationExist(cameraId)
+    ): Map<UseCaseConfig<*>, StreamSpec> {
 
         if (!checkIfSupportedCombinationExist(cameraId)) {
             throw IllegalArgumentException(
@@ -148,7 +146,7 @@
             )
         }
 
-        return supportedSurfaceCombinationMap[cameraId]!!.getSuggestedResolutions(
+        return supportedSurfaceCombinationMap[cameraId]!!.getSuggestedStreamSpecifications(
             existingSurfaces,
             newUseCaseConfigs
         )
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
index 33cf96c..05cb202 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
@@ -37,6 +37,7 @@
 import androidx.camera.core.impl.CamcorderProfileProxy
 import androidx.camera.core.impl.ImageFormatConstants
 import androidx.camera.core.impl.ImageOutputConfig
+import androidx.camera.core.impl.StreamSpec
 import androidx.camera.core.impl.SurfaceCombination
 import androidx.camera.core.impl.SurfaceConfig
 import androidx.camera.core.impl.SurfaceSizeDefinition
@@ -68,13 +69,10 @@
 // TODO(b/200306659): Remove and replace with annotation on package-info.java
 class SupportedSurfaceCombination(
     context: Context,
-    cameraMetadata: CameraMetadata,
-    cameraId: String,
-    camcorderProfileProviderAdapter: CamcorderProfileProviderAdapter
+    private val cameraMetadata: CameraMetadata,
+    private val camcorderProfileProviderAdapter: CamcorderProfileProviderAdapter
 ) {
-    private val cameraMetadata = cameraMetadata
-    private val cameraId = cameraId
-    private val camcorderProfileProviderAdapter = camcorderProfileProviderAdapter
+    private val cameraId = cameraMetadata.camera.value
     private val hardwareLevel =
         cameraMetadata[CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL]
             ?: CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
@@ -123,20 +121,19 @@
     }
 
     /**
-     * Finds the suggested resolutions of the newly added UseCaseConfig.
+     * Finds the suggested stream specification of the newly added UseCaseConfig.
      *
      * @param existingSurfaces  the existing surfaces.
      * @param newUseCaseConfigs newly added UseCaseConfig.
-     * @return the suggested resolutions, which is a mapping from UseCaseConfig to the suggested
-     * resolution.
+     * @return the suggested stream specs, which is a mapping from UseCaseConfig to the suggested
+     * stream specification.
      * @throws IllegalArgumentException if the suggested solution for newUseCaseConfigs cannot be
-     * found. This may be due to no available output size or no
-     * available surface combination.
+     * found. This may be due to no available output size or no available surface combination.
      */
-    fun getSuggestedResolutions(
+    fun getSuggestedStreamSpecifications(
         existingSurfaces: List<AttachedSurfaceInfo>,
         newUseCaseConfigs: List<UseCaseConfig<*>>
-    ): Map<UseCaseConfig<*>, Size> {
+    ): Map<UseCaseConfig<*>, StreamSpec> {
         refreshPreviewSize()
         val surfaceConfigs: MutableList<SurfaceConfig> = ArrayList()
         for (scc in existingSurfaces) {
@@ -179,7 +176,7 @@
             supportedOutputSizesList
         )
 
-        var suggestedResolutionsMap: Map<UseCaseConfig<*>, Size>? = null
+        var suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec>? = null
         // Transform use cases to SurfaceConfig list and find the first (best) workable combination
         for (possibleSizeList in allPossibleSizeArrangements) {
             // Attach SurfaceConfig of original use cases since it will impact the new use cases
@@ -203,19 +200,19 @@
 
             // Check whether the SurfaceConfig combination can be supported
             if (checkSupported(surfaceConfigList)) {
-                suggestedResolutionsMap = HashMap()
+                suggestedStreamSpecMap = HashMap()
                 for (useCaseConfig in newUseCaseConfigs) {
-                    suggestedResolutionsMap.put(
+                    suggestedStreamSpecMap.put(
                         useCaseConfig,
-                        possibleSizeList[useCasesPriorityOrder.indexOf(
+                        StreamSpec.builder(possibleSizeList[useCasesPriorityOrder.indexOf(
                             newUseCaseConfigs.indexOf(useCaseConfig)
-                        )]
+                        )]).build()
                     )
                 }
                 break
             }
         }
-        if (suggestedResolutionsMap == null) {
+        if (suggestedStreamSpecMap == null) {
             throw java.lang.IllegalArgumentException(
                 "No supported surface combination is found for camera device - Id : " +
                     cameraId + " and Hardware level: " + hardwareLevel +
@@ -224,7 +221,7 @@
                     " New configs: " + newUseCaseConfigs
             )
         }
-        return suggestedResolutionsMap
+        return suggestedStreamSpecMap
     }
 
     // Utility classes and methods:
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/CameraConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
index cb3474e..4c1ef19 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
@@ -113,7 +113,7 @@
 
         @Provides
         fun provideCameraMetadata(cameraPipe: CameraPipe, config: CameraConfig): CameraMetadata =
-            cameraPipe.cameras().awaitMetadata(config.cameraId)
+            checkNotNull(cameraPipe.cameras().awaitCameraMetadata(config.cameraId))
     }
 
     @Binds
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 f4ed9b6..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
@@ -42,6 +43,7 @@
 import dagger.Module
 import dagger.Provides
 import dagger.Subcomponent
+import java.util.concurrent.CancellationException
 import javax.inject.Scope
 
 @Scope
@@ -84,6 +86,7 @@
         cameraPipe: CameraPipe,
         requestListener: ComboRequestListener,
         useCaseSurfaceManager: UseCaseSurfaceManager,
+        cameraInteropStateCallbackRepository: CameraInteropStateCallbackRepository
     ): UseCaseGraphConfig {
         val streamConfigMap = mutableMapOf<CameraStream.Config, DeferrableSurface>()
 
@@ -91,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(
@@ -135,11 +139,15 @@
 
         if (sessionConfigAdapter.isSessionConfigValid()) {
             useCaseSurfaceManager.setupAsync(graph, sessionConfigAdapter, surfaceToStreamMap)
-                .invokeOnCompletion {
-                    it?.let { Log.error(it) { "Surface setup error!" } }
+                .invokeOnCompletion { throwable ->
+                    // Only show logs for error cases, ignore CancellationException since the task
+                    // could be cancelled by UseCaseSurfaceManager#stopAsync().
+                    if (throwable != null && throwable !is CancellationException) {
+                        Log.error(throwable) { "Surface setup error!" }
+                    }
                 }
         } else {
-            Log.debug {
+            Log.error {
                 "Unable to create capture session due to conflicting configurations"
             }
         }
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/main/java/androidx/camera/camera2/pipe/integration/impl/CameraProperties.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraProperties.kt
index 72d8538..2e06fbf 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraProperties.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraProperties.kt
@@ -41,5 +41,7 @@
 ) : CameraProperties {
     override val cameraId: CameraId
         get() = cameraConfig.cameraId
-    override val metadata: CameraMetadata by lazy { cameraPipe.cameras().awaitMetadata(cameraId) }
+    override val metadata: CameraMetadata by lazy {
+        checkNotNull(cameraPipe.cameras().awaitCameraMetadata(cameraId))
+    }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
index 23f0b97..43b6750 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
@@ -32,7 +32,9 @@
 import androidx.camera.core.FocusMeteringResult
 import androidx.camera.core.MeteringPoint
 import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
 import androidx.camera.core.impl.CameraControlInternal
+import androidx.lifecycle.Observer
 import com.google.common.util.concurrent.ListenableFuture
 import dagger.Binds
 import dagger.Module
@@ -40,7 +42,10 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withTimeoutOrNull
 
 /**
@@ -53,20 +58,37 @@
     val threads: UseCaseThreads,
 ) : UseCaseCameraControl {
     private var _useCaseCamera: UseCaseCamera? = null
+
+    private val useCaseObserver = Observer<Set<UseCase>> { useCases ->
+        // reset to null since preview use case may not be active in current runningUseCases
+        previewAspectRatio = null
+        useCases.forEach { useCase ->
+            if (useCase is Preview) {
+                useCase.attachedSurfaceResolution?.apply {
+                    previewAspectRatio = Rational(width, height)
+                }
+            }
+        }
+    }
+
     override var useCaseCamera: UseCaseCamera?
         get() = _useCaseCamera
         set(value) {
+            if (_useCaseCamera != value) {
+                previewAspectRatio = null
+                // withContext does not switch thread properly with scope.launch so async is used
+                threads.sequentialScope.async {
+                    withContext(Dispatchers.Main) {
+                        _useCaseCamera?.runningUseCasesLiveData?.removeObserver(useCaseObserver)
+                    }
+                }
+            }
+
             _useCaseCamera = value
 
-            // reset to null since preview ratio may not be applicable for current runningUseCases
-            previewAspectRatio = null
-            _useCaseCamera?.runningUseCasesLiveData?.observeForever { useCases ->
-                useCases.forEach { useCase ->
-                    if (useCase is Preview) {
-                        useCase.attachedSurfaceResolution?.apply {
-                            previewAspectRatio = Rational(width, height)
-                        }
-                    }
+            threads.sequentialScope.async {
+                withContext(Dispatchers.Main) {
+                    _useCaseCamera?.runningUseCasesLiveData?.observeForever(useCaseObserver)
                 }
             }
         }
@@ -75,6 +97,7 @@
         cancelFocusAndMeteringAsync()
     }
 
+    @Volatile
     private var previewAspectRatio: Rational? = null
     private val sensorRect by lazy {
         // TODO("b/262225455"): use the actual crop sensor region like in camera-camera2
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
index 754ee01..d49b7a9 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
@@ -39,6 +39,7 @@
 import androidx.camera.core.impl.ImmediateSurface
 import androidx.camera.core.impl.MutableOptionsBundle
 import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.StreamSpec
 import androidx.camera.core.impl.UseCaseConfig
 import androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER
 import androidx.camera.core.impl.UseCaseConfigFactory
@@ -72,10 +73,10 @@
     override fun getUseCaseConfigBuilder(config: Config) =
         Builder(cameraProperties, displayInfoManager)
 
-    override fun onSuggestedResolutionUpdated(suggestedResolution: Size): Size {
+    override fun onSuggestedStreamSpecUpdated(suggestedStreamSpec: StreamSpec): StreamSpec {
         updateSessionConfig(createPipeline().build())
         notifyActive()
-        return meteringSurfaceSize
+        return StreamSpec.builder(meteringSurfaceSize).build()
     }
 
     override fun onUnbind() {
@@ -87,9 +88,9 @@
 
     /** Sets up the use case's session configuration, mainly its [DeferrableSurface]. */
     fun setupSession() {
-        // The suggested resolution passed to `updateSuggestedResolution` doesn't matter since
+        // The suggested stream spec passed to `updateSuggestedStreamSpec` doesn't matter since
         // this use case uses the min preview size.
-        updateSuggestedResolution(DEFAULT_PREVIEW_SIZE)
+        updateSuggestedStreamSpec(StreamSpec.builder(DEFAULT_PREVIEW_SIZE).build())
     }
 
     private fun createPipeline(): SessionConfig.Builder {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
index 86749f7..cfb3151 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
@@ -71,6 +71,7 @@
 /**
  * API for interacting with a [CameraGraph] that has been configured with a set of [UseCase]'s
  */
+@RequiresApi(21)
 @UseCaseCameraScope
 class UseCaseCameraImpl @Inject constructor(
     private val useCaseGraphConfig: UseCaseGraphConfig,
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index e49ea46..0826055 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -31,6 +31,8 @@
 import androidx.camera.core.UseCase
 import androidx.camera.core.impl.DeferrableSurface
 import androidx.camera.core.impl.SessionConfig.ValidatingBuilder
+import androidx.camera.core.impl.utils.Threads
+import androidx.lifecycle.MutableLiveData
 import javax.inject.Inject
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.joinAll
@@ -114,6 +116,11 @@
             !attachedUseCases.contains(useCase)
         }
 
+        // Notify state attached to use cases
+        for (useCase in unattachedUseCases) {
+            useCase.onStateAttached()
+        }
+
         if (attachedUseCases.addAll(useCases)) {
             if (shouldAddRepeatingUseCase(getRunningUseCases())) {
                 addRepeatingUseCase()
@@ -122,9 +129,9 @@
             }
         }
 
-        // Notify state attached to use cases that weren't attached before
-        for (useCase in unattachedUseCases) {
-            useCase.onStateAttached()
+        unattachedUseCases.forEach { useCase ->
+            // Notify CameraControl is ready after the UseCaseCamera is created
+            useCase.onCameraControlReady()
         }
     }
 
@@ -218,7 +225,7 @@
         when {
             shouldAddRepeatingUseCase(runningUseCases) -> addRepeatingUseCase()
             shouldRemoveRepeatingUseCase(runningUseCases) -> removeRepeatingUseCase()
-            else -> camera?.runningUseCasesLiveData?.value = runningUseCases
+            else -> camera?.runningUseCasesLiveData?.setLiveDataValue(runningUseCases)
         }
     }
 
@@ -324,4 +331,12 @@
         val captureConfig = sessionConfig.repeatingCaptureConfig
         return predicate(captureConfig.surfaces, sessionConfig.surfaces)
     }
+
+    private fun <T> MutableLiveData<T>.setLiveDataValue(value: T?) {
+        if (Threads.isMainThread()) {
+            this.value = value
+        } else {
+            this.postValue(value)
+        }
+    }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/ZoomControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/ZoomControl.kt
index 7a9a5be..698da1b 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/ZoomControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/ZoomControl.kt
@@ -17,31 +17,50 @@
 package androidx.camera.camera2.pipe.integration.impl
 
 import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.adapter.ZoomValue
+import androidx.camera.camera2.pipe.integration.adapter.asListenableFuture
 import androidx.camera.camera2.pipe.integration.compat.ZoomCompat
 import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.core.CameraControl
+import androidx.camera.core.ZoomState
+import androidx.camera.core.impl.utils.futures.Futures
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.google.common.util.concurrent.ListenableFuture
 import dagger.Binds
 import dagger.Module
 import dagger.multibindings.IntoSet
 import javax.inject.Inject
+import kotlin.math.abs
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+const val DEFAULT_ZOOM_RATIO = 1.0f
 
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 @CameraScope
-class ZoomControl @Inject constructor(private val zoomCompat: ZoomCompat) : UseCaseCameraControl {
-    private var _zoomRatio = 1.0f
-    var zoomRatio: Float
-        get() = _zoomRatio
-        set(value) {
-            // TODO: Make this a suspend function?
-            _zoomRatio = value
-            update()
-        }
-
+class ZoomControl @Inject constructor(
+    private val threads: UseCaseThreads,
+    private val zoomCompat: ZoomCompat,
+) : UseCaseCameraControl {
     // NOTE: minZoom may be lower than 1.0
-    // NOTE: Default zoom ratio is 1.0
-    // NOTE: Linear zoom is
+    // NOTE: Default zoom ratio is 1.0 (DEFAULT_ZOOM_RATIO)
     val minZoom: Float = zoomCompat.minZoom
     val maxZoom: Float = zoomCompat.maxZoom
 
+    val defaultZoomState by lazy {
+        ZoomValue(DEFAULT_ZOOM_RATIO, minZoom, maxZoom)
+    }
+
+    private val _zoomState by lazy {
+        MutableLiveData<ZoomState>(defaultZoomState)
+    }
+
+    val zoomStateLiveData: LiveData<ZoomState>
+        get() = _zoomState
+
     /** Linear zoom is between 0.0f and 1.0f */
     fun toLinearZoom(zoomRatio: Float): Float {
         val range = zoomCompat.maxZoom - zoomCompat.minZoom
@@ -57,7 +76,13 @@
         if (range > 0) {
             return linearZoom * range + zoomCompat.minZoom
         }
-        return 1.0f
+
+        // if minZoom = maxZoom = 2.0f, 2.0f should be returned instead of default 1.0f
+        if (nearZero(range)) {
+            return zoomCompat.minZoom
+        }
+
+        return DEFAULT_ZOOM_RATIO
     }
 
     private var _useCaseCamera: UseCaseCamera? = null
@@ -69,17 +94,52 @@
         }
 
     override fun reset() {
-        // TODO: 1.0 may not be a reasonable value to reset the zoom state too.
-        zoomRatio = 1.0f
+        // TODO: 1.0 may not be a reasonable value to reset the zoom state to.
+        threads.sequentialScope.launch(start = CoroutineStart.UNDISPATCHED) {
+            setZoomState(defaultZoomState)
+        }
+
         update()
     }
 
     private fun update() {
         _useCaseCamera?.let {
-            zoomCompat.apply(_zoomRatio, it)
+            zoomCompat.apply(_zoomState.value?.zoomRatio ?: DEFAULT_ZOOM_RATIO, it)
         }
     }
 
+    private suspend fun setZoomState(value: ZoomState) {
+        // TODO: camera-camera2 updates livedata with setValue if calling thread is main thread,
+        //  and updates with postValue otherwise. Need to consider if always using setValue
+        //  via main thread is alright in camera-pipe.
+        withContext(Dispatchers.Main) {
+            _zoomState.value = value
+        }
+    }
+
+    fun setZoomRatioAsync(ratio: Float): ListenableFuture<Void> {
+        // TODO: report IllegalArgumentException if ratio not in range
+        return Futures.nonCancellationPropagating(
+            useCaseCamera?.let {
+                threads.scope.launch(start = CoroutineStart.UNDISPATCHED) {
+                    val zoomValue = ZoomValue(
+                        ratio,
+                        minZoom,
+                        maxZoom
+                    )
+                    setZoomState(zoomValue)
+                    update()
+                }.asListenableFuture()
+            } ?: Futures.immediateFailedFuture(
+                CameraControl.OperationCanceledException("Camera is not active.")
+            )
+        )
+    }
+
+    private fun nearZero(num: Float): Boolean {
+        return abs(num) < 2.0 * Math.ulp(abs(num))
+    }
+
     @Module
     abstract class Bindings {
         @Binds
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/CameraCompatibilityFilter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/CameraCompatibilityFilter.kt
index 782077f..763675f 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/CameraCompatibilityFilter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/CameraCompatibilityFilter.kt
@@ -63,10 +63,11 @@
             return true
         }
         try {
+            val cameraMetadata = checkNotNull(
+                cameraDevices.awaitCameraMetadata(CameraId(cameraId))
+            )
             val availableCapabilities =
-                cameraDevices.awaitMetadata(CameraId(cameraId))[
-                    CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES
-                ]
+                cameraMetadata[CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES]
             if (availableCapabilities != null) {
                 return availableCapabilities.contains(
                     CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/CameraSelectionOptimizer.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/CameraSelectionOptimizer.kt
index 37cc816..a25e88c 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/CameraSelectionOptimizer.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/CameraSelectionOptimizer.kt
@@ -27,7 +27,6 @@
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.InitializationException
 import androidx.camera.core.impl.CameraInfoInternal
-import kotlinx.coroutines.runBlocking
 
 /**
  * The [CameraSelectionOptimizer] is responsible for determining available camera Ids based on
@@ -47,8 +46,7 @@
                 val cameraAppComponent = cameraFactory.cameraManager as CameraAppComponent
                 val cameraDevices = cameraAppComponent.getCameraDevices()
 
-                val cameraIdList =
-                    runBlocking { cameraDevices.ids().map { it.value } }
+                val cameraIdList = checkNotNull(cameraDevices.awaitCameraIds()).map { it.value }
                 if (availableCamerasSelector == null) {
                     return cameraIdList
                 }
@@ -103,7 +101,8 @@
                 return null
             }
             if (lensFacingInteger.toInt() == CameraSelector.LENS_FACING_BACK) {
-                val camera0Metadata = cameraDevices.awaitMetadata(CameraId("0"))
+                val camera0Metadata = cameraDevices.awaitCameraMetadata(CameraId("0"))
+                checkNotNull(camera0Metadata)
                 if (camera0Metadata[CameraCharacteristics.LENS_FACING]?.equals(
                         CameraCharacteristics.LENS_FACING_BACK
                     )!!
@@ -113,7 +112,8 @@
                     skippedCameraId = "1"
                 }
             } else if (lensFacingInteger.toInt() == CameraSelector.LENS_FACING_FRONT) {
-                val camera1Metadata = cameraDevices.awaitMetadata(CameraId("1"))
+                val camera1Metadata = cameraDevices.awaitCameraMetadata(CameraId("1"))
+                checkNotNull(camera1Metadata)
                 if (camera1Metadata[CameraCharacteristics.LENS_FACING]?.equals(
                         CameraCharacteristics.LENS_FACING_FRONT
                     )!!
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt
index 6432d83..fe14c55 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt
@@ -18,12 +18,23 @@
 
 import android.os.Build
 import android.util.Size
+import androidx.camera.camera2.pipe.integration.impl.ZoomControl
 import androidx.camera.camera2.pipe.integration.testing.FakeCameraInfoAdapterCreator.createCameraInfoAdapter
+import androidx.camera.camera2.pipe.integration.testing.FakeCameraInfoAdapterCreator.useCaseThreads
+import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
+import androidx.camera.camera2.pipe.integration.testing.FakeZoomCompat
 import androidx.camera.core.FocusMeteringAction
 import androidx.camera.core.SurfaceOrientedMeteringPointFactory
+import androidx.camera.core.ZoomState
 import androidx.camera.core.impl.ImageFormatConstants
+import androidx.testutils.MainDispatcherRule
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import com.google.common.util.concurrent.MoreExecutors
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
@@ -33,7 +44,11 @@
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 class CameraInfoAdapterTest {
-    private val cameraInfoAdapter = createCameraInfoAdapter()
+    private val zoomControl = ZoomControl(useCaseThreads, FakeZoomCompat())
+    private val cameraInfoAdapter = createCameraInfoAdapter(zoomControl = zoomControl)
+
+    @get:Rule
+    val dispatcherRule = MainDispatcherRule(MoreExecutors.directExecutor().asCoroutineDispatcher())
 
     @Test
     fun getSupportedResolutions() {
@@ -61,4 +76,51 @@
             .that(cameraInfoAdapter.isFocusMeteringSupported(action))
             .isAnyOf(true, false)
     }
+
+    @Test
+    fun canReturnDefaultZoomState() {
+        // make new ZoomControl to test first-time initialization scenario
+        val zoomControl = ZoomControl(useCaseThreads, FakeZoomCompat())
+        val cameraInfoAdapter = createCameraInfoAdapter(zoomControl = zoomControl)
+
+        assertWithMessage("zoomState did not return default zoom ratio successfully")
+            .that(cameraInfoAdapter.zoomState.value)
+            .isEqualTo(zoomControl.defaultZoomState)
+    }
+
+    @Test
+    fun canObserveZoomStateUpdate(): Unit = runBlocking {
+        var currentZoomState: ZoomState = ZoomValue(-1.0f, -1.0f, -1.0f)
+        cameraInfoAdapter.zoomState.observeForever {
+            currentZoomState = it
+        }
+
+        // if useCaseCamera is null, zoom setting operation will be cancelled
+        zoomControl.useCaseCamera = FakeUseCaseCamera()
+
+        zoomControl.setZoomRatioAsync(3.0f)[3, TimeUnit.SECONDS]
+
+        // minZoom and maxZoom will be set as 0 due to FakeZoomCompat using those values
+        assertWithMessage("zoomState did not return default zoom ratio successfully")
+            .that(currentZoomState)
+            .isEqualTo(ZoomValue(3.0f, zoomControl.minZoom, zoomControl.maxZoom))
+    }
+
+    @Test
+    fun canObserveZoomStateReset(): Unit = runBlocking {
+        var currentZoomState: ZoomState = ZoomValue(-1.0f, -1.0f, -1.0f)
+        cameraInfoAdapter.zoomState.observeForever {
+            currentZoomState = it
+        }
+
+        // if useCaseCamera is null, zoom setting operation will be cancelled
+        zoomControl.useCaseCamera = FakeUseCaseCamera()
+
+        zoomControl.reset()
+
+        // minZoom and maxZoom will be set as 0 due to FakeZoomCompat using those values
+        assertWithMessage("zoomState did not return default zoom state successfully")
+            .that(currentZoomState)
+            .isEqualTo(zoomControl.defaultZoomState)
+    }
 }
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 50afb50..8ca5d39 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
@@ -45,12 +45,14 @@
 import androidx.camera.core.Preview
 import androidx.camera.core.SurfaceOrientedMeteringPointFactory
 import androidx.camera.core.UseCase
+import androidx.camera.core.impl.StreamSpec
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.testing.SurfaceTextureProvider
 import androidx.camera.testing.fakes.FakeCamera
 import androidx.camera.testing.fakes.FakeUseCase
 import androidx.lifecycle.MutableLiveData
 import androidx.test.filters.MediumTest
+import androidx.testutils.MainDispatcherRule
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import com.google.common.util.concurrent.FutureCallback
@@ -64,15 +66,22 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.asExecutor
 import kotlinx.coroutines.async
+import kotlinx.coroutines.cancel
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.withContext
+import org.junit.After
 import org.junit.Assert.assertThrows
 import org.junit.Before
 import org.junit.Ignore
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
@@ -101,11 +110,27 @@
     SENSOR_WIDTH,
     SENSOR_HEIGHT
 )
+// the following rectangles are for metering point (0, 0)
+private val M_RECT_PVIEW_RATIO_16x9_SENSOR_640x480 = Rect(
+    0, 60 - AREA_HEIGHT / 2,
+    AREA_WIDTH / 2, 60 + AREA_HEIGHT / 2
+)
+private val M_RECT_PVIEW_RATIO_4x3_SENSOR_1920x1080 = Rect(
+    240 - AREA_WIDTH_2 / 2, 0,
+    240 + AREA_WIDTH_2 / 2, AREA_HEIGHT_2 / 2
+)
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 @DoNotInstrument
 class FocusMeteringControlTest {
+    private val testScope = TestScope()
+    private val testDispatcher = StandardTestDispatcher(testScope.testScheduler)
+
+    @get:Rule
+    val mainDispatcherRule = MainDispatcherRule(testDispatcher)
+
     private val pointFactory = SurfaceOrientedMeteringPointFactory(1f, 1f)
     private lateinit var focusMeteringControl: FocusMeteringControl
 
@@ -128,6 +153,15 @@
         )
     }
 
+    // testUseCaseThreads is used to ensure that all coroutines are executed before some specific
+    // point. But fakeUseCaseThreads can not be replaced in all tests because testDispatcher
+    // does not work well with withTimeoutOrNull()
+    private val testUseCaseThreads = UseCaseThreads(
+        testScope,
+        testDispatcher.asExecutor(),
+        testDispatcher
+    )
+
     @Before
     fun setUp() {
         loadCameraProperties()
@@ -136,6 +170,18 @@
         focusMeteringControl = initFocusMeteringControl(CAMERA_ID_0)
     }
 
+    @After
+    fun tearDown() {
+        // CoroutineScope#cancel can throw exception if the scope has no job left
+        try {
+            // fakeUseCaseThreads may still be using Main dispatcher which sometimes
+            // causes Dispatchers.resetMain() to throw an exception:
+            // "IllegalStateException: Dispatchers.Main is used concurrently with setting it"
+            fakeUseCaseThreads.scope.cancel()
+            fakeUseCaseThreads.sequentialScope.cancel()
+        } catch (_: Exception) {}
+    }
+
     @Test
     fun meteringRegionsFromMeteringPoint_fovAspectRatioEqualToCropAspectRatio() {
         val meteringPoint = FakeMeteringPointFactory().createPoint(0.0f, 0.0f)
@@ -308,6 +354,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
@@ -389,17 +436,22 @@
         // use 16:9 preview aspect ratio with sensor region of 4:3 (camera 0)
         focusMeteringControl = initFocusMeteringControl(
             CAMERA_ID_0,
-            setOf(createPreview(Size(1920, 1080)))
+            setOf(createPreview(Size(1920, 1080))),
+            testUseCaseThreads,
         )
 
+        testDispatcher.scheduler.advanceUntilIdle()
+
         startFocusMeteringAndAwait(
             FocusMeteringAction.Builder(point1).build()
         )
 
-        val adjustedRect = Rect(0, 60 - AREA_HEIGHT / 2, AREA_WIDTH / 2, 60 + AREA_HEIGHT / 2)
+        testDispatcher.scheduler.advanceUntilIdle()
+
         with(fakeRequestControl.focusMeteringCalls.last()) {
             assertWithMessage("Wrong number of AF regions").that(afRegions.size).isEqualTo(1)
-            assertWithMessage("Wrong AF region").that(afRegions[0].rect).isEqualTo(adjustedRect)
+            assertWithMessage("Wrong AF region")
+                .that(afRegions[0].rect).isEqualTo(M_RECT_PVIEW_RATIO_16x9_SENSOR_640x480)
         }
     }
 
@@ -408,20 +460,22 @@
         // use 4:3 preview aspect ratio with sensor region of 16:9 (camera 1)
         focusMeteringControl = initFocusMeteringControl(
             CAMERA_ID_1,
-            setOf(createPreview(Size(640, 480)))
+            setOf(createPreview(Size(640, 480))),
+            testUseCaseThreads,
         )
 
+        testDispatcher.scheduler.advanceUntilIdle()
+
         startFocusMeteringAndAwait(
             FocusMeteringAction.Builder(point1).build()
         )
 
-        val adjustedRect = Rect(
-            240 - AREA_WIDTH_2 / 2, 0,
-            240 + AREA_WIDTH_2 / 2, AREA_HEIGHT_2 / 2
-        )
+        testDispatcher.scheduler.advanceUntilIdle()
+
         with(fakeRequestControl.focusMeteringCalls.last()) {
             assertWithMessage("Wrong number of AF regions").that(afRegions.size).isEqualTo(1)
-            assertWithMessage("Wrong AF region").that(afRegions[0].rect).isEqualTo(adjustedRect)
+            assertWithMessage("Wrong AF region")
+                .that(afRegions[0].rect).isEqualTo(M_RECT_PVIEW_RATIO_4x3_SENSOR_1920x1080)
         }
     }
 
@@ -429,24 +483,29 @@
     fun customFovAdjusted() {
         // 16:9 to 4:3
         val useCase = FakeUseCase()
-        useCase.updateSuggestedResolution(Size(1920, 1080))
+        useCase.updateSuggestedStreamSpec(StreamSpec.builder(Size(1920, 1080)).build())
 
         val factory = SurfaceOrientedMeteringPointFactory(1.0f, 1.0f, useCase)
         val point = factory.createPoint(0f, 0f)
 
         focusMeteringControl = initFocusMeteringControl(
             CAMERA_ID_0,
-            setOf(createPreview(Size(640, 480)))
+            setOf(createPreview(Size(640, 480))),
+            testUseCaseThreads,
         )
 
+        testDispatcher.scheduler.advanceUntilIdle()
+
         startFocusMeteringAndAwait(
             FocusMeteringAction.Builder(point).build()
         )
 
-        val adjustedRect = Rect(0, 60 - AREA_HEIGHT / 2, AREA_WIDTH / 2, 60 + AREA_HEIGHT / 2)
+        testDispatcher.scheduler.advanceUntilIdle()
+
         with(fakeRequestControl.focusMeteringCalls.last()) {
             assertWithMessage("Wrong number of AF regions").that(afRegions.size).isEqualTo(1)
-            assertWithMessage("Wrong AF region").that(afRegions[0].rect).isEqualTo(adjustedRect)
+            assertWithMessage("Wrong AF region")
+                .that(afRegions[0].rect).isEqualTo(M_RECT_PVIEW_RATIO_16x9_SENSOR_640x480)
         }
     }
 
@@ -455,18 +514,57 @@
         // add 16:9 aspect ratio Preview with sensor region of 4:3 (camera 0), then remove Preview
         focusMeteringControl = initFocusMeteringControl(
             CAMERA_ID_0,
-            setOf(createPreview(Size(1920, 1080)))
+            setOf(createPreview(Size(1920, 1080))),
+            testUseCaseThreads,
         )
         fakeUseCaseCamera.runningUseCasesLiveData.value = emptySet()
 
+        testDispatcher.scheduler.advanceUntilIdle()
+
         startFocusMeteringAndAwait(
             FocusMeteringAction.Builder(point1).build()
         )
 
-        val adjustedRect = Rect(0, 60 - AREA_HEIGHT / 2, AREA_WIDTH / 2, 60 + AREA_HEIGHT / 2)
+        testDispatcher.scheduler.advanceUntilIdle()
+
+        // point1 = (0, 0) is considered as center point of metering rectangle.
+        // Since previewAspectRatio is not set, it will be same as cropRegionAspectRatio
+        // which is the size of SENSOR_1 in this test. So the point is not adjusted,
+        // and simply M_RECT_1 (metering rectangle of point1 with SENSOR_1) should be used.
         with(fakeRequestControl.focusMeteringCalls.last()) {
             assertWithMessage("Wrong number of AF regions").that(afRegions.size).isEqualTo(1)
-            assertWithMessage("Wrong AF region").that(afRegions[0].rect).isEqualTo(adjustedRect)
+            assertWithMessage("Wrong AF region").that(afRegions[0].rect).isEqualTo(M_RECT_1)
+        }
+    }
+
+    @Test
+    fun previewRatioIsNotChanged_whenSameUseCaseCameraSetTwice() {
+        // use 16:9 aspect ratio Preview with sensor region of 4:3 (camera 0)
+        val useCases = setOf<UseCase>(createPreview(Size(1920, 1080)))
+
+        focusMeteringControl = initFocusMeteringControl(
+            CAMERA_ID_0,
+            useCases,
+            UseCaseThreads(
+                testScope,
+                testDispatcher.asExecutor(),
+                testDispatcher
+            ),
+        )
+        focusMeteringControl.useCaseCamera = fakeUseCaseCamera
+
+        testDispatcher.scheduler.advanceUntilIdle()
+
+        startFocusMeteringAndAwait(
+            FocusMeteringAction.Builder(point1).build()
+        )
+
+        testDispatcher.scheduler.advanceUntilIdle()
+
+        with(fakeRequestControl.focusMeteringCalls.last()) {
+            assertWithMessage("Wrong number of AF regions").that(afRegions.size).isEqualTo(1)
+            assertWithMessage("Wrong AF region")
+                .that(afRegions[0].rect).isEqualTo(M_RECT_PVIEW_RATIO_16x9_SENSOR_640x480)
         }
     }
 
@@ -789,6 +887,7 @@
         assertThat(focusMeteringControl.isFocusMeteringSupported(action)).isTrue()
     }
 
+    @Ignore("b/266123157")
     @Test
     fun isFocusMeteringSupported_noSupport3ARegion_shouldReturnFalse() {
         val action = FocusMeteringAction.Builder(point1).build()
@@ -1035,8 +1134,9 @@
     private fun initFocusMeteringControl(
         cameraId: String,
         useCases: Set<UseCase> = emptySet(),
+        useCaseThreads: UseCaseThreads = fakeUseCaseThreads,
     ) = FocusMeteringControl(
-            cameraPropertiesMap[cameraId]!!, fakeUseCaseThreads
+            cameraPropertiesMap[cameraId]!!, useCaseThreads
         ).apply {
             fakeUseCaseCamera.runningUseCasesLiveData.value = useCases
             useCaseCamera = fakeUseCaseCamera
@@ -1145,7 +1245,7 @@
         )
     }
 
-    private fun createPreview(suggestedResolution: Size) =
+    private fun createPreview(suggestedStreamSpecResolution: Size) =
         Preview.Builder()
             .setCaptureOptionUnpacker { _, _ -> }
             .setSessionOptionUnpacker() { _, _ -> }
@@ -1156,6 +1256,8 @@
                 )
             }.also {
                 it.bindToCamera(FakeCamera("0"), null, null)
-                it.updateSuggestedResolution(suggestedResolution)
+                it.updateSuggestedStreamSpec(
+                    StreamSpec.builder(suggestedStreamSpecResolution).build()
+                )
             }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
index 9b13a31..59295e2 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
@@ -30,7 +30,6 @@
 import android.view.Surface
 import android.view.WindowManager
 import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CameraMetadata
 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.camera2.pipe.integration.adapter.GuaranteedConfigurationsUtil.getFullSupportedCombinationList
 import androidx.camera.camera2.pipe.integration.adapter.GuaranteedConfigurationsUtil.getLegacySupportedCombinationList
@@ -38,7 +37,9 @@
 import androidx.camera.camera2.pipe.integration.adapter.GuaranteedConfigurationsUtil.getLimitedSupportedCombinationList
 import androidx.camera.camera2.pipe.integration.adapter.GuaranteedConfigurationsUtil.getRAWSupportedCombinationList
 import androidx.camera.camera2.pipe.integration.config.CameraAppComponent
-import androidx.camera.camera2.pipe.integration.testing.FakeCameraDevicesWithCameraMetaData
+import androidx.camera.camera2.pipe.testing.FakeCameraBackend
+import androidx.camera.camera2.pipe.testing.FakeCameraDevices
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
 import androidx.camera.core.AspectRatio
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.CameraSelector.LensFacing
@@ -53,6 +54,7 @@
 import androidx.camera.core.impl.CameraThreadConfig
 import androidx.camera.core.impl.MutableStateObservable
 import androidx.camera.core.impl.Observable
+import androidx.camera.core.impl.StreamSpec
 import androidx.camera.core.impl.SurfaceCombination
 import androidx.camera.core.impl.SurfaceConfig
 import androidx.camera.core.impl.UseCaseConfig
@@ -81,7 +83,7 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.testutils.assertThrows
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
 import java.util.Arrays
 import java.util.concurrent.ExecutionException
 import java.util.concurrent.TimeUnit
@@ -90,7 +92,8 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers
-import org.mockito.Mockito
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 import org.robolectric.RobolectricTestRunner
 import org.robolectric.Shadows
 import org.robolectric.annotation.Config
@@ -104,8 +107,6 @@
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 class SupportedSurfaceCombinationTest {
-    private val cameraId = "0"
-    private val cameraIdExternal = "0-external"
     private val sensorOrientation0 = 0
     private val sensorOrientation90 = 90
     private val aspectRatio43 = Rational(4, 3)
@@ -114,11 +115,16 @@
     private val portraitPixelArraySize = Size(3024, 4032)
     private val displaySize = Size(720, 1280)
     private val vgaSize = Size(640, 480)
+    private val vgaSizeStreamSpec = StreamSpec.builder(vgaSize).build()
     private val previewSize = Size(1280, 720)
+    private val previewSizeStreamSpec = StreamSpec.builder(previewSize).build()
     private val recordSize = Size(3840, 2160)
+    private val recordSizeStreamSpec = StreamSpec.builder(recordSize).build()
     private val maximumSize = Size(4032, 3024)
+    private val maximumSizeStreamSpec = StreamSpec.builder(maximumSize).build()
     private val legacyVideoMaximumVideoSize = Size(1920, 1080)
     private val mod16Size = Size(960, 544)
+    private val mod16SizeStreamSpec = StreamSpec.builder(mod16Size).build()
     private val profileUhd = CamcorderProfileUtil.createCamcorderProfileProxy(
         CamcorderProfile.QUALITY_2160P, recordSize.width, recordSize
             .height
@@ -151,21 +157,12 @@
     )
     private val context = InstrumentationRegistry.getInstrumentation().context
     private var cameraFactory: FakeCameraFactory? = null
-    private var useCaseConfigFactory = Mockito.mock(
-        UseCaseConfigFactory::class.java
-    )
-    private val mockCameraMetadata = Mockito.mock(
-        CameraMetadata::class.java
-    )
-    private val mockCameraAppComponent = Mockito.mock(
-        CameraAppComponent::class.java
-    )
-    private val mockCamcorderProfileAdapter = Mockito.mock(
-        CamcorderProfileProviderAdapter::class.java
-    )
-    private val mockCamcorderProxy = Mockito.mock(
-        CamcorderProfileProxy::class.java
-    )
+    private var useCaseConfigFactory: UseCaseConfigFactory = mock()
+    private lateinit var fakeCameraMetadata: FakeCameraMetadata
+
+    private val mockCameraAppComponent: CameraAppComponent = mock()
+    private val mockCamcorderProfileAdapter: CamcorderProfileProviderAdapter = mock()
+    private val mockCamcorderProxy: CamcorderProfileProxy = mock()
 
     @Before
     fun setUp() {
@@ -175,11 +172,11 @@
             displaySize
                 .height
         )
-        Mockito.`when`(mockCamcorderProfileAdapter.hasProfile(ArgumentMatchers.anyInt()))
+        whenever(mockCamcorderProfileAdapter.hasProfile(ArgumentMatchers.anyInt()))
             .thenReturn(true)
-        Mockito.`when`(mockCamcorderProxy.videoFrameWidth).thenReturn(3840)
-        Mockito.`when`(mockCamcorderProxy.videoFrameHeight).thenReturn(2160)
-        Mockito.`when`(mockCamcorderProfileAdapter[ArgumentMatchers.anyInt()])
+        whenever(mockCamcorderProxy.videoFrameWidth).thenReturn(3840)
+        whenever(mockCamcorderProxy.videoFrameHeight).thenReturn(2160)
+        whenever(mockCamcorderProfileAdapter[ArgumentMatchers.anyInt()])
             .thenReturn(mockCamcorderProxy)
     }
 
@@ -192,14 +189,14 @@
     fun checkLegacySurfaceCombinationSupportedInLegacyDevice() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val combinationList = getLegacySupportedCombinationList()
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
-            Truth.assertThat(isSupported).isTrue()
+            assertThat(isSupported).isTrue()
         }
     }
 
@@ -207,26 +204,26 @@
     fun checkLegacySurfaceCombinationSubListSupportedInLegacyDevice() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val combinationList = getLegacySupportedCombinationList()
         val isSupported = isAllSubConfigListSupported(supportedSurfaceCombination, combinationList)
-        Truth.assertThat(isSupported).isTrue()
+        assertThat(isSupported).isTrue()
     }
 
     @Test
     fun checkLimitedSurfaceCombinationNotSupportedInLegacyDevice() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val combinationList = getLimitedSupportedCombinationList()
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
-            Truth.assertThat(isSupported).isFalse()
+            assertThat(isSupported).isFalse()
         }
     }
 
@@ -234,14 +231,14 @@
     fun checkFullSurfaceCombinationNotSupportedInLegacyDevice() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val combinationList = getFullSupportedCombinationList()
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
-            Truth.assertThat(isSupported).isFalse()
+            assertThat(isSupported).isFalse()
         }
     }
 
@@ -249,14 +246,14 @@
     fun checkLevel3SurfaceCombinationNotSupportedInLegacyDevice() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val combinationList = getLevel3SupportedCombinationList()
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
-            Truth.assertThat(isSupported).isFalse()
+            assertThat(isSupported).isFalse()
         }
     }
 
@@ -264,14 +261,14 @@
     fun checkLimitedSurfaceCombinationSupportedInLimitedDevice() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val combinationList = getLimitedSupportedCombinationList()
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
-            Truth.assertThat(isSupported).isTrue()
+            assertThat(isSupported).isTrue()
         }
     }
 
@@ -279,26 +276,26 @@
     fun checkLimitedSurfaceCombinationSubListSupportedInLimited3Device() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val combinationList = getLimitedSupportedCombinationList()
         val isSupported = isAllSubConfigListSupported(supportedSurfaceCombination, combinationList)
-        Truth.assertThat(isSupported).isTrue()
+        assertThat(isSupported).isTrue()
     }
 
     @Test
     fun checkFullSurfaceCombinationNotSupportedInLimitedDevice() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val combinationList = getFullSupportedCombinationList()
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
-            Truth.assertThat(isSupported).isFalse()
+            assertThat(isSupported).isFalse()
         }
     }
 
@@ -306,14 +303,14 @@
     fun checkLevel3SurfaceCombinationNotSupportedInLimitedDevice() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val combinationList = getLevel3SupportedCombinationList()
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
-            Truth.assertThat(isSupported).isFalse()
+            assertThat(isSupported).isFalse()
         }
     }
 
@@ -321,14 +318,14 @@
     fun checkFullSurfaceCombinationSupportedInFullDevice() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val combinationList = getFullSupportedCombinationList()
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
-            Truth.assertThat(isSupported).isTrue()
+            assertThat(isSupported).isTrue()
         }
     }
 
@@ -336,26 +333,26 @@
     fun checkFullSurfaceCombinationSubListSupportedInFullDevice() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val combinationList = getFullSupportedCombinationList()
         val isSupported = isAllSubConfigListSupported(supportedSurfaceCombination, combinationList)
-        Truth.assertThat(isSupported).isTrue()
+        assertThat(isSupported).isTrue()
     }
 
     @Test
     fun checkLevel3SurfaceCombinationNotSupportedInFullDevice() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val combinationList = getLevel3SupportedCombinationList()
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
-            Truth.assertThat(isSupported).isFalse()
+            assertThat(isSupported).isFalse()
         }
     }
 
@@ -367,14 +364,14 @@
             )
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val combinationList = getLimitedSupportedCombinationList()
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
-            Truth.assertThat(isSupported).isTrue()
+            assertThat(isSupported).isTrue()
         }
     }
 
@@ -386,14 +383,14 @@
             )
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val combinationList = getLegacySupportedCombinationList()
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
-            Truth.assertThat(isSupported).isTrue()
+            assertThat(isSupported).isTrue()
         }
     }
 
@@ -405,14 +402,14 @@
             )
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val combinationList = getFullSupportedCombinationList()
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
-            Truth.assertThat(isSupported).isTrue()
+            assertThat(isSupported).isTrue()
         }
     }
 
@@ -424,14 +421,14 @@
             )
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val combinationList = getRAWSupportedCombinationList()
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
-            Truth.assertThat(isSupported).isTrue()
+            assertThat(isSupported).isTrue()
         }
     }
 
@@ -439,14 +436,14 @@
     fun checkLevel3SurfaceCombinationSupportedInLevel3Device() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val combinationList = getLevel3SupportedCombinationList()
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(combination.surfaceConfigList)
-            Truth.assertThat(isSupported).isTrue()
+            assertThat(isSupported).isTrue()
         }
     }
 
@@ -454,19 +451,19 @@
     fun checkLevel3SurfaceCombinationSubListSupportedInLevel3Device() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val combinationList = getLevel3SupportedCombinationList()
         val isSupported = isAllSubConfigListSupported(supportedSurfaceCombination, combinationList)
-        Truth.assertThat(isSupported).isTrue()
+        assertThat(isSupported).isTrue()
     }
 
     @Test
     fun checkTargetAspectRatio() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val fakeUseCase = FakeUseCaseConfig.Builder()
@@ -475,20 +472,20 @@
         val useCases: MutableList<UseCase> = ArrayList()
         useCases.add(fakeUseCase)
         val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
             useCases,
             useCaseConfigFactory
         )
-        val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+        val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
             emptyList(),
             ArrayList(useCaseToConfigMap.values)
         )
-        val selectedSize = suggestedResolutionMap[useCaseToConfigMap[fakeUseCase]]!!
+        val selectedStreamSpec = suggestedStreamSpecMap[useCaseToConfigMap[fakeUseCase]]!!
         val resultAspectRatio = Rational(
-            selectedSize.width,
-            selectedSize.height
+            selectedStreamSpec.resolution.width,
+            selectedStreamSpec.resolution.height
         )
-        Truth.assertThat(resultAspectRatio).isEqualTo(aspectRatio169)
+        assertThat(resultAspectRatio).isEqualTo(aspectRatio169)
     }
 
     @Test
@@ -502,11 +499,7 @@
             .build()
         preview.setSurfaceProvider(
             CameraXExecutors.directExecutor(),
-            SurfaceTextureProvider.createSurfaceTextureProvider(
-                Mockito.mock(
-                    SurfaceTextureCallback::class.java
-                )
-            )
+            SurfaceTextureProvider.createSurfaceTextureProvider(mock<SurfaceTextureCallback>())
         )
         val imageCapture = ImageCapture.Builder()
             .setTargetAspectRatio(AspectRatio.RATIO_16_9)
@@ -537,26 +530,22 @@
         )
 
         // Checks no correction is needed.
-        Truth.assertThat(previewRatio).isEqualTo(targetAspectRatio)
-        Truth.assertThat(imageCaptureRatio).isEqualTo(targetAspectRatio)
-        Truth.assertThat(imageAnalysisRatio).isEqualTo(targetAspectRatio)
+        assertThat(previewRatio).isEqualTo(targetAspectRatio)
+        assertThat(imageCaptureRatio).isEqualTo(targetAspectRatio)
+        assertThat(imageAnalysisRatio).isEqualTo(targetAspectRatio)
     }
 
     @Test
     fun checkDefaultAspectRatioAndResolutionForMixedUseCase() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val preview = Preview.Builder().build()
         preview.setSurfaceProvider(
             CameraXExecutors.directExecutor(),
-            SurfaceTextureProvider.createSurfaceTextureProvider(
-                Mockito.mock(
-                    SurfaceTextureCallback::class.java
-                )
-            )
+            SurfaceTextureProvider.createSurfaceTextureProvider(mock<SurfaceTextureCallback>())
         )
         val imageCapture = ImageCapture.Builder().build()
         val imageAnalysis = ImageAnalysis.Builder().build()
@@ -579,47 +568,48 @@
         useCases.add(imageCapture)
         useCases.add(imageAnalysis)
         val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
             useCases,
             useCaseConfigFactory
         )
-        val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+        val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
             emptyList(),
             ArrayList(useCaseToConfigMap.values)
         )
-        val previewSize = suggestedResolutionMap[useCaseToConfigMap[preview]]
-        val imageCaptureSize = suggestedResolutionMap[useCaseToConfigMap[imageCapture]]
-        val imageAnalysisSize = suggestedResolutionMap[useCaseToConfigMap[imageAnalysis]]
-        assert(previewSize != null)
+        val previewSize = suggestedStreamSpecMap[useCaseToConfigMap[preview]]!!.resolution
+        val imageCaptureSize = suggestedStreamSpecMap[useCaseToConfigMap[imageCapture]]!!.resolution
+        val imageAnalysisSize =
+            suggestedStreamSpecMap[useCaseToConfigMap[imageAnalysis]]!!.resolution
+
         val previewAspectRatio = Rational(
-            previewSize!!.width,
+            previewSize.width,
             previewSize.height
         )
-        assert(imageCaptureSize != null)
+
         val imageCaptureAspectRatio = Rational(
-            imageCaptureSize!!.width,
+            imageCaptureSize.width,
             imageCaptureSize.height
         )
-        assert(imageAnalysisSize != null)
+
         val imageAnalysisAspectRatio = Rational(
-            imageAnalysisSize!!.width,
+            imageAnalysisSize.width,
             imageAnalysisSize.height
         )
 
         // Checks the default aspect ratio.
-        Truth.assertThat(previewAspectRatio).isEqualTo(aspectRatio43)
-        Truth.assertThat(imageCaptureAspectRatio).isEqualTo(aspectRatio43)
-        Truth.assertThat(imageAnalysisAspectRatio).isEqualTo(aspectRatio43)
+        assertThat(previewAspectRatio).isEqualTo(aspectRatio43)
+        assertThat(imageCaptureAspectRatio).isEqualTo(aspectRatio43)
+        assertThat(imageAnalysisAspectRatio).isEqualTo(aspectRatio43)
 
         // Checks the default resolution.
-        Truth.assertThat(imageAnalysisSize).isEqualTo(vgaSize)
+        assertThat(imageAnalysisSize).isEqualTo(vgaSize)
     }
 
     @Test
     fun checkSmallSizesAreFilteredOutByDefaultSize480p() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
 
@@ -638,11 +628,11 @@
         val useCases: MutableList<UseCase> = ArrayList()
         useCases.add(preview)
         val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
             useCases,
             useCaseConfigFactory
         )
-        val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+        val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
             emptyList(),
             ArrayList(useCaseToConfigMap.values)
         )
@@ -651,23 +641,23 @@
         val preconditionSize = Size(256, 144)
         val targetRatio = Rational(displayHeight, displayWidth)
         val sizeList = ArrayList(listOf(*supportedSizes))
-        Truth.assertThat(sizeList).contains(preconditionSize)
+        assertThat(sizeList).contains(preconditionSize)
         for (s in supportedSizes) {
             val supportedRational = Rational(s.width, s.height)
-            Truth.assertThat(supportedRational).isNotEqualTo(targetRatio)
+            assertThat(supportedRational).isNotEqualTo(targetRatio)
         }
 
         // Checks the mechanism has filtered out the sizes which are smaller than default size
         // 480p.
-        val previewSize = suggestedResolutionMap[useCaseToConfigMap[preview]]
-        Truth.assertThat(previewSize).isNotEqualTo(preconditionSize)
+        val previewSize = suggestedStreamSpecMap[useCaseToConfigMap[preview]]
+        assertThat(previewSize).isNotEqualTo(preconditionSize)
     }
 
     @Test
     fun checkAspectRatioMatchedSizeCanBeSelected() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
 
@@ -679,12 +669,13 @@
             val imageCapture = ImageCapture.Builder().setTargetResolution(
                 targetResolution
             ).setTargetRotation(Surface.ROTATION_90).build()
-            val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
-                emptyList(),
-                listOf(imageCapture.currentConfig)
-            )
-            Truth.assertThat(targetResolution).isEqualTo(
-                suggestedResolutionMap[imageCapture.currentConfig]
+            val suggestedStreamSpecMap =
+                supportedSurfaceCombination.getSuggestedStreamSpecifications(
+                    emptyList(),
+                    listOf(imageCapture.currentConfig)
+                )
+            assertThat(targetResolution).isEqualTo(
+                suggestedStreamSpecMap[imageCapture.currentConfig]?.resolution
             )
         }
     }
@@ -693,7 +684,7 @@
     fun checkCorrectAspectRatioNotMatchedSizeCanBeSelected() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
 
@@ -704,20 +695,20 @@
         val imageCapture = ImageCapture.Builder().setTargetResolution(
             targetResolution
         ).setTargetRotation(Surface.ROTATION_90).build()
-        val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+        val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
             emptyList(),
             listOf(imageCapture.currentConfig)
         )
-        Truth.assertThat(Size(1280, 720)).isEqualTo(
-            suggestedResolutionMap[imageCapture.currentConfig]
+        assertThat(Size(1280, 720)).isEqualTo(
+            suggestedStreamSpecMap[imageCapture.currentConfig]?.resolution
         )
     }
 
     @Test
-    fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
+    fun suggestedStreamSpecsForMixedUseCaseNotSupportedInLegacyDevice() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val imageCapture = ImageCapture.Builder()
@@ -732,12 +723,12 @@
         useCases.add(videoCapture)
         useCases.add(preview)
         val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
             useCases,
             useCaseConfigFactory
         )
         assertThrows(IllegalArgumentException::class.java) {
-            supportedSurfaceCombination.getSuggestedResolutions(
+            supportedSurfaceCombination.getSuggestedStreamSpecifications(
                 emptyList(),
                 ArrayList(useCaseToConfigMap.values)
             )
@@ -745,10 +736,10 @@
     }
 
     @Test
-    fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice() {
+    fun suggestedStreamSpecsForCustomizeResolutionsNotSupportedInLegacyDevice() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
 
@@ -765,12 +756,12 @@
         useCases.add(videoCapture)
         useCases.add(preview)
         val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
             useCases,
             useCaseConfigFactory
         )
         assertThrows(IllegalArgumentException::class.java) {
-            supportedSurfaceCombination.getSuggestedResolutions(
+            supportedSurfaceCombination.getSuggestedStreamSpecifications(
                 emptyList(),
                 ArrayList(useCaseToConfigMap.values)
             )
@@ -779,10 +770,10 @@
 
     // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
     @Test
-    fun suggestedResolutionsForMixedUseCaseInLimitedDevice() {
+    fun suggestedStreamSpecsForMixedUseCaseInLimitedDevice() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val imageCapture = ImageCapture.Builder()
@@ -797,36 +788,36 @@
         useCases.add(videoCapture)
         useCases.add(preview)
         val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
             useCases,
             useCaseConfigFactory
         )
-        val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
-            supportedSurfaceCombination.getSuggestedResolutions(
+        val suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec> =
+            supportedSurfaceCombination.getSuggestedStreamSpecifications(
                 emptyList(),
                 ArrayList(useCaseToConfigMap.values)
             )
 
         // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
+        assertThat(suggestedStreamSpecMap).containsEntry(
             useCaseToConfigMap[imageCapture],
-            recordSize
+            recordSizeStreamSpec
         )
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
+        assertThat(suggestedStreamSpecMap).containsEntry(
             useCaseToConfigMap[videoCapture],
-            recordSize
+            recordSizeStreamSpec
         )
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
+        assertThat(suggestedStreamSpecMap).containsEntry(
             useCaseToConfigMap[preview],
-            previewSize
+            previewSizeStreamSpec
         )
     }
 
     @Test
-    fun suggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage() {
+    fun suggestedStreamSpecsInFullDevice_videoHasHigherPriorityThanImage() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val imageCapture = ImageCapture.Builder()
@@ -846,12 +837,12 @@
         useCases.add(videoCapture)
         useCases.add(preview)
         val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
             useCases,
             useCaseConfigFactory
         )
-        val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
-            supportedSurfaceCombination.getSuggestedResolutions(
+        val suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec> =
+            supportedSurfaceCombination.getSuggestedStreamSpecifications(
                 emptyList(),
                 ArrayList(useCaseToConfigMap.values)
             )
@@ -859,17 +850,17 @@
         // There are two possible combinations in Full level device
         // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD) => should be applied
         // (PRIV, PREVIEW) + (PRIV, PREVIEW) + (JPEG, MAXIMUM)
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
+        assertThat(suggestedStreamSpecMap).containsEntry(
             useCaseToConfigMap[imageCapture],
-            recordSize
+            recordSizeStreamSpec
         )
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
+        assertThat(suggestedStreamSpecMap).containsEntry(
             useCaseToConfigMap[videoCapture],
-            recordSize
+            recordSizeStreamSpec
         )
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
+        assertThat(suggestedStreamSpecMap).containsEntry(
             useCaseToConfigMap[preview],
-            previewSize
+            previewSizeStreamSpec
         )
     }
 
@@ -877,7 +868,7 @@
     fun suggestedResInFullDevice_videoRecordSizeLowPriority_imageCanGetMaxSize() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val imageCapture = ImageCapture.Builder()
@@ -896,12 +887,12 @@
         useCases.add(videoCapture)
         useCases.add(preview)
         val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
             useCases,
             useCaseConfigFactory
         )
-        val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
-            supportedSurfaceCombination.getSuggestedResolutions(
+        val suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec> =
+            supportedSurfaceCombination.getSuggestedStreamSpecifications(
                 emptyList(),
                 ArrayList(useCaseToConfigMap.values)
             )
@@ -909,25 +900,25 @@
         // There are two possible combinations in Full level device
         // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
         // (PRIV, PREVIEW) + (PRIV, PREVIEW) + (JPEG, MAXIMUM) => should be applied
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
+        assertThat(suggestedStreamSpecMap).containsEntry(
             useCaseToConfigMap[imageCapture],
-            maximumSize
+            maximumSizeStreamSpec
         )
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
+        assertThat(suggestedStreamSpecMap).containsEntry(
             useCaseToConfigMap[videoCapture],
-            previewSize
+            previewSizeStreamSpec
         ) // Quality.HD
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
+        assertThat(suggestedStreamSpecMap).containsEntry(
             useCaseToConfigMap[preview],
-            previewSize
+            previewSizeStreamSpec
         )
     }
 
     @Test
-    fun suggestedResolutionsWithSameSupportedListForDifferentUseCases() {
+    fun suggestedStreamSpecsWithSameSupportedListForDifferentUseCases() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
 
@@ -951,26 +942,26 @@
         useCases.add(preview)
         useCases.add(imageAnalysis)
         val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
             useCases,
             useCaseConfigFactory
         )
-        val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
-            supportedSurfaceCombination.getSuggestedResolutions(
+        val suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec> =
+            supportedSurfaceCombination.getSuggestedStreamSpecifications(
                 emptyList(),
                 ArrayList(useCaseToConfigMap.values)
             )
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
+        assertThat(suggestedStreamSpecMap).containsEntry(
             useCaseToConfigMap[imageCapture],
-            previewSize
+            previewSizeStreamSpec
         )
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
+        assertThat(suggestedStreamSpecMap).containsEntry(
             useCaseToConfigMap[preview],
-            previewSize
+            previewSizeStreamSpec
         )
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
+        assertThat(suggestedStreamSpecMap).containsEntry(
             useCaseToConfigMap[imageAnalysis],
-            previewSize
+            previewSizeStreamSpec
         )
     }
 
@@ -986,7 +977,7 @@
         } catch (e: IllegalArgumentException) {
             previewExceptionHappened = true
         }
-        Truth.assertThat(previewExceptionHappened).isTrue()
+        assertThat(previewExceptionHappened).isTrue()
         var imageCaptureExceptionHappened = false
         val imageCaptureConfigBuilder = ImageCapture.Builder()
             .setTargetResolution(displaySize)
@@ -996,7 +987,7 @@
         } catch (e: IllegalArgumentException) {
             imageCaptureExceptionHappened = true
         }
-        Truth.assertThat(imageCaptureExceptionHappened).isTrue()
+        assertThat(imageCaptureExceptionHappened).isTrue()
         var imageAnalysisExceptionHappened = false
         val imageAnalysisConfigBuilder = ImageAnalysis.Builder()
             .setTargetResolution(displaySize)
@@ -1006,16 +997,16 @@
         } catch (e: IllegalArgumentException) {
             imageAnalysisExceptionHappened = true
         }
-        Truth.assertThat(imageAnalysisExceptionHappened).isTrue()
+        assertThat(imageAnalysisExceptionHappened).isTrue()
     }
 
     @Test
-    fun suggestedResolutionsForCustomizedSupportedResolutions() {
+    fun suggestedStreamSpecsForCustomizedSupportedResolutions() {
 
-        // Checks all suggested resolutions will become 640x480.
+        // Checks all suggested stream specs will have their resolutions become 640x480.
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val formatResolutionsPairList: MutableList<Pair<Int, Array<Size>>> = ArrayList()
@@ -1038,28 +1029,28 @@
         useCases.add(videoCapture)
         useCases.add(preview)
         val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
             useCases,
             useCaseConfigFactory
         )
-        val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
-            supportedSurfaceCombination.getSuggestedResolutions(
+        val suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec> =
+            supportedSurfaceCombination.getSuggestedStreamSpecifications(
                 emptyList(),
                 ArrayList(useCaseToConfigMap.values)
             )
 
-        // Checks all suggested resolutions will become 640x480.
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
+        // Checks all suggested stream specs will have their resolutions become 640x480.
+        assertThat(suggestedStreamSpecMap).containsEntry(
             useCaseToConfigMap[imageCapture],
-            vgaSize
+            vgaSizeStreamSpec
         )
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
+        assertThat(suggestedStreamSpecMap).containsEntry(
             useCaseToConfigMap[videoCapture],
-            vgaSize
+            vgaSizeStreamSpec
         )
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
+        assertThat(suggestedStreamSpecMap).containsEntry(
             useCaseToConfigMap[preview],
-            vgaSize
+            vgaSizeStreamSpec
         )
     }
 
@@ -1067,7 +1058,7 @@
     fun transformSurfaceConfigWithYUVAnalysisSize() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val surfaceConfig = supportedSurfaceCombination.transformSurfaceConfig(
@@ -1075,14 +1066,14 @@
         )
         val expectedSurfaceConfig =
             SurfaceConfig.create(SurfaceConfig.ConfigType.YUV, SurfaceConfig.ConfigSize.VGA)
-        Truth.assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+        assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
     }
 
     @Test
     fun transformSurfaceConfigWithYUVPreviewSize() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val surfaceConfig = supportedSurfaceCombination.transformSurfaceConfig(
@@ -1090,14 +1081,14 @@
         )
         val expectedSurfaceConfig =
             SurfaceConfig.create(SurfaceConfig.ConfigType.YUV, SurfaceConfig.ConfigSize.PREVIEW)
-        Truth.assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+        assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
     }
 
     @Test
     fun transformSurfaceConfigWithYUVRecordSize() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val surfaceConfig = supportedSurfaceCombination.transformSurfaceConfig(
@@ -1105,14 +1096,14 @@
         )
         val expectedSurfaceConfig =
             SurfaceConfig.create(SurfaceConfig.ConfigType.YUV, SurfaceConfig.ConfigSize.RECORD)
-        Truth.assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+        assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
     }
 
     @Test
     fun transformSurfaceConfigWithYUVMaximumSize() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val surfaceConfig = supportedSurfaceCombination.transformSurfaceConfig(
@@ -1120,14 +1111,14 @@
         )
         val expectedSurfaceConfig =
             SurfaceConfig.create(SurfaceConfig.ConfigType.YUV, SurfaceConfig.ConfigSize.MAXIMUM)
-        Truth.assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+        assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
     }
 
     @Test
     fun transformSurfaceConfigWithJPEGAnalysisSize() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val surfaceConfig = supportedSurfaceCombination.transformSurfaceConfig(
@@ -1135,14 +1126,14 @@
         )
         val expectedSurfaceConfig =
             SurfaceConfig.create(SurfaceConfig.ConfigType.JPEG, SurfaceConfig.ConfigSize.VGA)
-        Truth.assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+        assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
     }
 
     @Test
     fun transformSurfaceConfigWithJPEGPreviewSize() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val surfaceConfig = supportedSurfaceCombination.transformSurfaceConfig(
@@ -1150,14 +1141,14 @@
         )
         val expectedSurfaceConfig =
             SurfaceConfig.create(SurfaceConfig.ConfigType.JPEG, SurfaceConfig.ConfigSize.PREVIEW)
-        Truth.assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+        assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
     }
 
     @Test
     fun transformSurfaceConfigWithJPEGRecordSize() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val surfaceConfig = supportedSurfaceCombination.transformSurfaceConfig(
@@ -1165,14 +1156,14 @@
         )
         val expectedSurfaceConfig =
             SurfaceConfig.create(SurfaceConfig.ConfigType.JPEG, SurfaceConfig.ConfigSize.RECORD)
-        Truth.assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+        assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
     }
 
     @Test
     fun transformSurfaceConfigWithJPEGMaximumSize() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val surfaceConfig = supportedSurfaceCombination.transformSurfaceConfig(
@@ -1180,29 +1171,29 @@
         )
         val expectedSurfaceConfig =
             SurfaceConfig.create(SurfaceConfig.ConfigType.JPEG, SurfaceConfig.ConfigSize.MAXIMUM)
-        Truth.assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
+        assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
     }
 
     @Test
     fun maximumSizeForImageFormat() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val maximumYUVSize =
             supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.YUV_420_888)
-        Truth.assertThat(maximumYUVSize).isEqualTo(maximumSize)
+        assertThat(maximumYUVSize).isEqualTo(maximumSize)
         val maximumJPEGSize =
             supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.JPEG)
-        Truth.assertThat(maximumJPEGSize).isEqualTo(maximumSize)
+        assertThat(maximumJPEGSize).isEqualTo(maximumSize)
     }
 
     @Test
     fun isAspectRatioMatchWithSupportedMod16Resolution() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val preview = Preview.Builder()
@@ -1217,22 +1208,22 @@
         useCases.add(preview)
         useCases.add(imageCapture)
         val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
             useCases,
             useCaseConfigFactory
         )
-        val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
-            supportedSurfaceCombination.getSuggestedResolutions(
+        val suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec> =
+            supportedSurfaceCombination.getSuggestedStreamSpecifications(
                 emptyList(),
                 ArrayList(useCaseToConfigMap.values)
             )
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
+        assertThat(suggestedStreamSpecMap).containsEntry(
             useCaseToConfigMap[preview],
-            mod16Size
+            mod16SizeStreamSpec
         )
-        Truth.assertThat(suggestedResolutionMap).containsEntry(
+        assertThat(suggestedStreamSpecMap).containsEntry(
             useCaseToConfigMap[imageCapture],
-            mod16Size
+            mod16SizeStreamSpec
         )
     }
 
@@ -1257,14 +1248,14 @@
 
         // The testing sizes array will be equal to mSupportedSizes after sorting.
         Arrays.sort(sizes, CompareSizesByArea(true))
-        Truth.assertThat(listOf(*sizes)).isEqualTo(listOf(*supportedSizes))
+        assertThat(listOf(*sizes)).isEqualTo(listOf(*supportedSizes))
     }
 
     @Test
     fun supportedOutputSizes_noConfigSettings() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().build()
@@ -1286,14 +1277,14 @@
             Size(800, 450),
             Size(640, 480)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
     fun supportedOutputSizes_aspectRatio4x3() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setTargetAspectRatio(AspectRatio.RATIO_4_3)
@@ -1320,14 +1311,14 @@
             Size(960, 544),
             Size(800, 450)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
     fun supportedOutputSizes_aspectRatio16x9() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setTargetAspectRatio(
@@ -1355,14 +1346,14 @@
             Size(1280, 960),
             Size(640, 480)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
     fun supportedOutputSizes_targetResolution1080x1920InRotation0() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
@@ -1392,14 +1383,14 @@
             Size(1280, 960),
             Size(640, 480)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
     fun supportedOutputSizes_targetResolutionLargerThan640x480() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setTargetRotation(
@@ -1428,14 +1419,14 @@
             Size(960, 544),
             Size(800, 450)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
     fun supportedOutputSizes_targetResolutionSmallerThan640x480() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setTargetRotation(
@@ -1459,14 +1450,14 @@
             ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
             Size(800, 450)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
     fun supportedOutputSizes_targetResolution1800x1440NearTo4x3() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setTargetRotation(
@@ -1492,14 +1483,14 @@
             Size(960, 544),
             Size(800, 450)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
     fun supportedOutputSizes_targetResolution1280x600NearTo16x9() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
@@ -1522,14 +1513,14 @@
             Size(1280, 960),
             Size(640, 480)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
     fun supportedOutputSizes_maxResolution1280x720() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setMaxResolution(Size(1280, 720)).build()
@@ -1546,14 +1537,14 @@
             Size(800, 450),
             Size(640, 480)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
     fun supportedOutputSizes_setCustomOrderedResolutions() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val customOrderedResolutions = listOf(
@@ -1584,14 +1575,14 @@
         val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
             useCase.currentConfig
         )
-        Truth.assertThat(resultList).containsExactlyElementsIn(customOrderedResolutions).inOrder()
+        assertThat(resultList).containsExactlyElementsIn(customOrderedResolutions).inOrder()
     }
 
     @Test
     fun supportedOutputSizes_defaultResolution1280x720_noTargetResolution() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setDefaultResolution(
@@ -1614,14 +1605,14 @@
             Size(800, 450),
             Size(640, 480)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
     fun supportedOutputSizes_defaultResolution1280x720_targetResolution1920x1080() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setDefaultResolution(
@@ -1651,7 +1642,7 @@
             Size(1280, 960),
             Size(640, 480)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
@@ -1665,7 +1656,7 @@
             )
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
@@ -1679,7 +1670,7 @@
             useCase.currentConfig
         )
         val expectedList = listOf(Size(640, 480))
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
@@ -1693,7 +1684,7 @@
             )
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setMaxResolution(
@@ -1712,7 +1703,7 @@
             Size(320, 180),
             Size(256, 144)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
@@ -1726,7 +1717,7 @@
             )
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setMaxResolution(
@@ -1747,7 +1738,7 @@
             Size(320, 180),
             Size(256, 144)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
@@ -1761,7 +1752,7 @@
             )
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setMaxResolution(
@@ -1783,14 +1774,14 @@
             Size(256, 144),
             Size(320, 240)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
     fun supportedOutputSizes_whenMaxSizeSmallerThanBigTargetResolution() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setMaxResolution(
@@ -1820,7 +1811,7 @@
             Size(1280, 960),
             Size(640, 480)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
@@ -1834,7 +1825,7 @@
             )
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setMaxResolution(
@@ -1855,7 +1846,7 @@
             Size(320, 180),
             Size(256, 144)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
@@ -1869,7 +1860,7 @@
             )
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
@@ -1887,7 +1878,7 @@
             Size(320, 240),
             Size(256, 144)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
@@ -1901,7 +1892,7 @@
             )
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setMaxResolution(
@@ -1927,7 +1918,7 @@
             )
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
@@ -1947,7 +1938,7 @@
             Size(256, 144),
             Size(320, 240)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
@@ -1967,7 +1958,7 @@
             )
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
@@ -1985,7 +1976,7 @@
             Size(1280, 960),
             Size(640, 480)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
@@ -2008,7 +1999,7 @@
             sensorOrientation0, portraitPixelArraySize, supportedSizes, null
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setTargetAspectRatio(
@@ -2035,7 +2026,7 @@
             Size(640, 480),
             Size(1280, 720)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
@@ -2058,7 +2049,7 @@
             sensorOrientation90, portraitPixelArraySize, supportedSizes, null
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setTargetAspectRatio(
@@ -2085,7 +2076,7 @@
             Size(640, 480),
             Size(1280, 720)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
@@ -2095,7 +2086,7 @@
             sensorOrientation0, landscapePixelArraySize, supportedSizes, null
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setTargetAspectRatio(
@@ -2123,7 +2114,7 @@
             Size(1280, 960),
             Size(640, 480)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
@@ -2143,7 +2134,7 @@
             sensorOrientation0, landscapePixelArraySize, supportedSizes, null
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val useCase = FakeUseCaseConfig.Builder().setTargetAspectRatio(
@@ -2169,23 +2160,23 @@
             Size(480, 640),
             Size(720, 1280)
         )
-        Truth.assertThat(resultList).isEqualTo(expectedList)
+        assertThat(resultList).isEqualTo(expectedList)
     }
 
     @Test
     fun determineRecordSizeFromStreamConfigurationMap() {
         // Setup camera with non-integer camera Id
         setupCamera(
-            cameraIdExternal, CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
-            sensorOrientation90, landscapePixelArraySize, supportedSizes, null
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+            cameraId = CameraId("externalCameraId")
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraIdExternal,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
 
         // Checks the determined RECORD size
-        Truth.assertThat(
+        assertThat(
             supportedSurfaceCombination.surfaceSizeDefinition.recordSize
         ).isEqualTo(
             legacyVideoMaximumVideoSize
@@ -2209,7 +2200,7 @@
             sensorOrientation90, landscapePixelArraySize, supportedSizes, null
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
 
@@ -2218,13 +2209,13 @@
         val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
             vgaSize
         ).setTargetRotation(Surface.ROTATION_90).build()
-        val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+        val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
             emptyList(),
             listOf(useCase.currentConfig)
         )
 
         // Checks 640x480 is final selected for the use case.
-        Truth.assertThat(suggestedResolutionMap[useCase.currentConfig]).isEqualTo(vgaSize)
+        assertThat(suggestedStreamSpecMap[useCase.currentConfig]?.resolution).isEqualTo(vgaSize)
     }
 
     @Test
@@ -2237,19 +2228,19 @@
             sensorOrientation90, landscapePixelArraySize, supportedSizes, null
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
 
         // Sets the max resolution as 720x1280
         val useCase = FakeUseCaseConfig.Builder().setMaxResolution(displaySize).build()
-        val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+        val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
             emptyList(),
             listOf(useCase.currentConfig)
         )
 
         // Checks 480x480 is final selected for the use case.
-        Truth.assertThat(suggestedResolutionMap[useCase.currentConfig]).isEqualTo(
+        assertThat(suggestedStreamSpecMap[useCase.currentConfig]?.resolution).isEqualTo(
             Size(480, 480)
         )
     }
@@ -2258,16 +2249,14 @@
     fun previewSizeIsSelectedForImageAnalysis_imageCaptureHasNoSetSizeInLimitedDevice() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val preview = Preview.Builder().build()
         preview.setSurfaceProvider(
             CameraXExecutors.directExecutor(),
             SurfaceTextureProvider.createSurfaceTextureProvider(
-                Mockito.mock(
-                    SurfaceTextureCallback::class.java
-                )
+                mock<SurfaceTextureCallback>()
             )
         )
 
@@ -2292,15 +2281,15 @@
         useCases.add(imageCapture)
         useCases.add(imageAnalysis)
         val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
             useCases,
             useCaseConfigFactory
         )
-        val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+        val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
             emptyList(),
             ArrayList(useCaseToConfigMap.values)
         )
-        Truth.assertThat(suggestedResolutionMap[useCaseToConfigMap[imageAnalysis]]).isEqualTo(
+        assertThat(suggestedStreamSpecMap[useCaseToConfigMap[imageAnalysis]]?.resolution).isEqualTo(
             previewSize
         )
     }
@@ -2309,16 +2298,14 @@
     fun recordSizeIsSelectedForImageAnalysis_imageCaptureHasExplicitSizeInLimitedDevice() {
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, mockCameraMetadata, cameraId,
+            context, fakeCameraMetadata,
             mockCamcorderProfileAdapter
         )
         val preview = Preview.Builder().build()
         preview.setSurfaceProvider(
             CameraXExecutors.directExecutor(),
             SurfaceTextureProvider.createSurfaceTextureProvider(
-                Mockito.mock(
-                    SurfaceTextureCallback::class.java
-                )
+                mock<SurfaceTextureCallback>()
             )
         )
 
@@ -2346,15 +2333,15 @@
         useCases.add(imageCapture)
         useCases.add(imageAnalysis)
         val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
             useCases,
             useCaseConfigFactory
         )
-        val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+        val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
             emptyList(),
             ArrayList(useCaseToConfigMap.values)
         )
-        Truth.assertThat(suggestedResolutionMap[useCaseToConfigMap[imageAnalysis]]).isEqualTo(
+        assertThat(suggestedStreamSpecMap[useCaseToConfigMap[imageAnalysis]]?.resolution).isEqualTo(
             recordSize
         )
     }
@@ -2379,25 +2366,8 @@
         pixelArraySize: Size = landscapePixelArraySize,
         supportedSizes: Array<Size> =
             this.supportedSizes,
-        capabilities: IntArray? = null
-    ) {
-        setupCamera(
-            cameraId,
-            hardwareLevel,
-            sensorOrientation,
-            pixelArraySize,
-            supportedSizes,
-            capabilities
-        )
-    }
-
-    private fun setupCamera(
-        cameraId: String,
-        hardwareLevel: Int,
-        sensorOrientation: Int,
-        pixelArraySize: Size,
-        supportedSizes: Array<Size>,
-        capabilities: IntArray?
+        capabilities: IntArray? = null,
+        cameraId: CameraId = CameraId.fromCamera1Id(0)
     ) {
         cameraFactory = FakeCameraFactory()
         val characteristics = ShadowCameraCharacteristics.newCameraCharacteristics()
@@ -2418,23 +2388,37 @@
                 CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES, capabilities
             )
         }
+
+        val mockMap: StreamConfigurationMap = mock()
+
+        // set up FakeCafakeCameraMetadatameraMetadata
+        fakeCameraMetadata = FakeCameraMetadata(
+            cameraId = cameraId,
+            characteristics = mapOf(
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL to hardwareLevel,
+                CameraCharacteristics.SENSOR_ORIENTATION to sensorOrientation,
+                CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE to pixelArraySize,
+                CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK,
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES to capabilities,
+                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP to mockMap
+            )
+        )
+
         val cameraManager = ApplicationProvider.getApplicationContext<Context>()
             .getSystemService(Context.CAMERA_SERVICE) as CameraManager
         (Shadow.extract<Any>(cameraManager) as ShadowCameraManager)
-            .addCamera(cameraId, characteristics)
-        val mockMap = Mockito.mock(
-            StreamConfigurationMap::class.java
-        )
-        Mockito.`when`(mockMap.getOutputSizes(ArgumentMatchers.anyInt())).thenReturn(supportedSizes)
+            .addCamera(fakeCameraMetadata.camera.value, characteristics)
+
+        whenever(mockMap.getOutputSizes(ArgumentMatchers.anyInt())).thenReturn(supportedSizes)
         // ImageFormat.PRIVATE was supported since API level 23. Before that, the supported
         // output sizes need to be retrieved via SurfaceTexture.class.
-        Mockito.`when`(
+        whenever(
             mockMap.getOutputSizes(
                 SurfaceTexture::class.java
             )
         ).thenReturn(supportedSizes)
         // This is setup for the test to determine RECORD size from StreamConfigurationMap
-        Mockito.`when`(
+        whenever(
             mockMap.getOutputSizes(
                 MediaRecorder::class.java
             )
@@ -2443,7 +2427,7 @@
         @LensFacing val lensFacingEnum = CameraUtil.getLensFacingEnumFromInt(
             CameraCharacteristics.LENS_FACING_BACK
         )
-        val cameraInfo = FakeCameraInfoInternal(cameraId)
+        val cameraInfo = FakeCameraInfoInternal(fakeCameraMetadata.camera.value)
         cameraInfo.camcorderProfileProvider = FakeCamcorderProfileProvider.Builder()
             .addProfile(
                 CamcorderProfileUtil.asHighQuality(profileUhd),
@@ -2454,39 +2438,21 @@
                 CamcorderProfileUtil.asLowQuality(profileSd)
             ).build()
         cameraFactory!!.insertCamera(
-            lensFacingEnum, cameraId
-        ) { FakeCamera(cameraId, null, cameraInfo) }
+            lensFacingEnum, fakeCameraMetadata.camera.value
+        ) { FakeCamera(fakeCameraMetadata.camera.value, null, cameraInfo) }
 
-        // set up CameraMetaData
-        Mockito.`when`(
-            mockCameraMetadata[CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL]
-        ).thenReturn(hardwareLevel)
-        Mockito.`when`(mockCameraMetadata[CameraCharacteristics.SENSOR_ORIENTATION])
-            .thenReturn(
-                sensorOrientation
-            )
-        Mockito.`when`(
-            mockCameraMetadata[CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE]
-        ).thenReturn(pixelArraySize)
-        Mockito.`when`(mockCameraMetadata[CameraCharacteristics.LENS_FACING]).thenReturn(
-            CameraCharacteristics.LENS_FACING_BACK
-        )
-        Mockito.`when`(
-            mockCameraMetadata[CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES]
-        ).thenReturn(capabilities)
-        Mockito.`when`(
-            mockCameraMetadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]
-        ).thenReturn(mockMap)
-        initCameraX(cameraId)
+        initCameraX(fakeCameraMetadata.camera.value)
     }
 
     private fun initCameraX(cameraId: String) {
-        val cameraMetaDataMap = mutableMapOf<CameraId, CameraMetadata>()
-        cameraMetaDataMap[CameraId(cameraId)] = mockCameraMetadata
-        val cameraDevicesWithCameraMetaData =
-            FakeCameraDevicesWithCameraMetaData(cameraMetaDataMap, mockCameraMetadata)
-        Mockito.`when`(mockCameraAppComponent.getCameraDevices())
-            .thenReturn(cameraDevicesWithCameraMetaData)
+        val fakeCameraDevices =
+            FakeCameraDevices(
+                defaultCameraBackendId = FakeCameraBackend.FAKE_CAMERA_BACKEND_ID,
+                cameraMetadataMap =
+                mapOf(FakeCameraBackend.FAKE_CAMERA_BACKEND_ID to listOf(fakeCameraMetadata))
+            )
+        whenever(mockCameraAppComponent.getCameraDevices())
+            .thenReturn(fakeCameraDevices)
         cameraFactory!!.cameraManager = mockCameraAppComponent
         val cameraXConfig = CameraXConfig.Builder.fromConfig(
             CameraPipeConfig.defaultConfig()
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
index cc035a9..8687489 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
@@ -24,6 +24,7 @@
 import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
 import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.core.impl.StreamSpec
 import androidx.test.core.app.ApplicationProvider
 import org.junit.After
 import org.junit.Assert.assertEquals
@@ -41,7 +42,7 @@
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 class MeteringRepeatingTest {
     companion object {
-        val dummyZeroSize = Size(0, 0)
+        val dummyZeroSizeStreamSpec = StreamSpec.builder(Size(0, 0)).build()
 
         val dummySizeListWithout640x480 = listOf(
             Size(4160, 3120),
@@ -130,7 +131,7 @@
     fun attachedSurfaceResolutionIsLargestLessThan640x480_when640x480NotPresentInOutputSizes() {
         meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListWithout640x480)
 
-        meteringRepeating.updateSuggestedResolution(dummyZeroSize)
+        meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec)
 
         assertEquals(Size(320, 240), meteringRepeating.attachedSurfaceResolution)
     }
@@ -139,7 +140,7 @@
     fun attachedSurfaceResolutionIs640x480_when640x480PresentInOutputSizes() {
         meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListWith640x480)
 
-        meteringRepeating.updateSuggestedResolution(dummyZeroSize)
+        meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec)
 
         assertEquals(Size(640, 480), meteringRepeating.attachedSurfaceResolution)
     }
@@ -148,7 +149,7 @@
     fun attachedSurfaceResolutionFallsBackToMinimum_whenAllOutputSizesLargerThan640x480() {
         meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListWithoutSmaller)
 
-        meteringRepeating.updateSuggestedResolution(dummyZeroSize)
+        meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec)
 
         assertEquals(Size(1280, 720), meteringRepeating.attachedSurfaceResolution)
     }
@@ -157,7 +158,7 @@
     fun attachedSurfaceResolutionIsLargestWithinPreviewSize_whenAllOutputSizesLessThan640x480() {
         meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListSmallerThan640x480)
 
-        meteringRepeating.updateSuggestedResolution(dummyZeroSize)
+        meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec)
 
         assertEquals(Size(320, 480), meteringRepeating.attachedSurfaceResolution)
     }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
index 8641662..1617485 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
@@ -30,6 +30,7 @@
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.Preview
 import androidx.camera.core.UseCase
+import androidx.camera.core.impl.StreamSpec
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.testing.SurfaceTextureProvider
 import androidx.camera.testing.fakes.FakeCamera
@@ -269,6 +270,6 @@
 
     private fun UseCase.simulateActivation() {
         bindToCamera(FakeCamera("0"), null, null)
-        updateSuggestedResolution(Size(640, 480))
+        updateSuggestedStreamSpec(StreamSpec.builder(Size(640, 480)).build())
     }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/ZoomControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/ZoomControlTest.kt
new file mode 100644
index 0000000..600f8ab
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/ZoomControlTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.impl
+
+import android.os.Build
+import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
+import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
+import androidx.camera.camera2.pipe.integration.testing.FakeZoomCompat
+import androidx.testutils.MainDispatcherRule
+import com.google.common.truth.Truth
+import com.google.common.util.concurrent.MoreExecutors
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.asCoroutineDispatcher
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(RobolectricCameraPipeTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+@DoNotInstrument
+class ZoomControlTest {
+    @get:Rule
+    val dispatcherRule = MainDispatcherRule(MoreExecutors.directExecutor().asCoroutineDispatcher())
+
+    private val fakeUseCaseThreads by lazy {
+        val executor = Executors.newSingleThreadExecutor()
+        val dispatcher = executor.asCoroutineDispatcher()
+        val cameraScope = CoroutineScope(Job() + dispatcher)
+
+        UseCaseThreads(
+            cameraScope,
+            executor,
+            dispatcher,
+        )
+    }
+
+    private val zoomCompat = FakeZoomCompat()
+    private lateinit var zoomControl: ZoomControl
+
+    @Before
+    fun setUp() {
+        zoomControl = ZoomControl(fakeUseCaseThreads, zoomCompat).apply {
+            useCaseCamera = FakeUseCaseCamera()
+        }
+    }
+
+    @Test
+    fun canUpdateZoomRatioInCompat() {
+        zoomControl.setZoomRatioAsync(3.0f)[3, TimeUnit.SECONDS]
+
+        Truth.assertWithMessage("zoomState did not return default zoom state successfully")
+            .that(zoomCompat.zoomRatio)
+            .isEqualTo(3.0f)
+    }
+
+    // TODO: port tests from camera-camera2
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraDevicesWithCameraMetaData.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraDevicesWithCameraMetaData.kt
deleted file mode 100644
index 7e64564..0000000
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraDevicesWithCameraMetaData.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.pipe.integration.testing
-
-import androidx.camera.camera2.pipe.CameraDevices
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CameraMetadata
-import kotlinx.coroutines.runBlocking
-
-class FakeCameraDevicesWithCameraMetaData(
-    private val cameraMetadataMap: Map<CameraId, CameraMetadata>,
-    private val defaultCameraMetadata: CameraMetadata
-) : CameraDevices {
-    @Deprecated(
-        message = "findAll may block the calling thread and is deprecated.",
-        replaceWith = ReplaceWith("ids"),
-        level = DeprecationLevel.WARNING
-    )
-    override fun findAll(): List<CameraId> = runBlocking { ids() }
-    override suspend fun ids(): List<CameraId> = cameraMetadataMap.keys.toList()
-    override suspend fun getMetadata(camera: CameraId): CameraMetadata = awaitMetadata(camera)
-    override fun awaitMetadata(camera: CameraId) =
-        cameraMetadataMap[camera] ?: defaultCameraMetadata
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
index 34ce448..5c6e224 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
@@ -32,7 +32,6 @@
 import androidx.camera.camera2.pipe.integration.impl.TorchControl
 import androidx.camera.camera2.pipe.integration.impl.UseCaseThreads
 import androidx.camera.camera2.pipe.integration.impl.ZoomControl
-import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
 import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
 import androidx.camera.core.impl.ImageFormatConstants
 import com.google.common.util.concurrent.MoreExecutors
@@ -45,7 +44,7 @@
 object FakeCameraInfoAdapterCreator {
     private val CAMERA_ID_0 = CameraId("0")
 
-    private val useCaseThreads by lazy {
+    val useCaseThreads by lazy {
         val executor = MoreExecutors.directExecutor()
         val dispatcher = executor.asCoroutineDispatcher()
         val cameraScope = CoroutineScope(
@@ -68,7 +67,8 @@
         CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE to Rect(0, 0, 640, 480)
     )
 
-    @OptIn(ExperimentalCamera2Interop::class)
+    private val zoomControl = ZoomControl(useCaseThreads, FakeZoomCompat())
+
     fun createCameraInfoAdapter(
         cameraId: CameraId = CAMERA_ID_0,
         cameraProperties: CameraProperties = FakeCameraProperties(
@@ -77,13 +77,14 @@
                 characteristics = cameraCharacteristics
             ),
             cameraId
-        )
+        ),
+        zoomControl: ZoomControl = this.zoomControl,
     ) = CameraInfoAdapter(
         cameraProperties,
         CameraConfig(cameraId),
         CameraStateAdapter(),
         CameraControlStateAdapter(
-            ZoomControl(FakeZoomCompat()),
+            zoomControl,
             EvCompControl(FakeEvCompCompat()),
             TorchControl(cameraProperties, useCaseThreads),
         ),
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeZoomCompat.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeZoomCompat.kt
index d2f88f5..cabac1a 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeZoomCompat.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeZoomCompat.kt
@@ -23,8 +23,9 @@
     override val minZoom: Float = 0f,
     override val maxZoom: Float = 0f,
 ) : ZoomCompat {
+    var zoomRatio = 0f
 
     override fun apply(zoomRatio: Float, camera: UseCaseCamera) {
-        TODO("Not yet implemented")
+        this.zoomRatio = zoomRatio
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraBackend.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraBackend.kt
index aa1ab5f..eaaa2eb 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraBackend.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraBackend.kt
@@ -42,13 +42,11 @@
         get() = synchronized(lock) { _cameraControllers.toList() }
 
     override val id: CameraBackendId
-        get() = FAKE_CAMERA_BACKEND
+        get() = FAKE_CAMERA_BACKEND_ID
 
-    override fun readCameraIdList(): List<CameraId> = fakeCameraIds
-    override fun readCameraMetadata(cameraId: CameraId): CameraMetadata =
-        checkNotNull(fakeCameras[cameraId]) {
-            "fakeCameras does not contain $cameraId. Available cameras are: $fakeCameras"
-        }
+    override fun awaitCameraIds(): List<CameraId> = fakeCameraIds
+
+    override fun awaitCameraMetadata(cameraId: CameraId): CameraMetadata? = fakeCameras[cameraId]
 
     override fun disconnectAllAsync(): Deferred<Unit> {
         _cameraControllers.forEach {
@@ -83,6 +81,7 @@
     }
 
     companion object {
-        private val FAKE_CAMERA_BACKEND = CameraBackendId("camerapipe.testing.fake_backend")
+        val FAKE_CAMERA_BACKEND_ID =
+            CameraBackendId("androidx.camera.camera2.pipe.testing.FakeCameraBackend")
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraDevices.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraDevices.kt
index c775914..8352e33 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraDevices.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraDevices.kt
@@ -17,28 +17,73 @@
 package androidx.camera.camera2.pipe.testing
 
 import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraBackendId
 import androidx.camera.camera2.pipe.CameraDevices
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.CameraMetadata
-import kotlinx.coroutines.runBlocking
 
 /**
  * This provides a fake implementation of [CameraDevices] for tests with a fixed list of Cameras.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 class FakeCameraDevices(
-    private val cameras: List<CameraMetadata>
+    private val defaultCameraBackendId: CameraBackendId,
+    private val cameraMetadataMap: Map<CameraBackendId, List<CameraMetadata>>
 ) : CameraDevices {
+    init {
+        check(cameraMetadataMap.containsKey(defaultCameraBackendId)) {
+            "FakeCameraDevices must include $defaultCameraBackendId"
+        }
+    }
+
+    override suspend fun getCameraIds(cameraBackendId: CameraBackendId?): List<CameraId>? =
+        awaitCameraIds(cameraBackendId)
+
+    override fun awaitCameraIds(cameraBackendId: CameraBackendId?): List<CameraId>? {
+        val backendId = cameraBackendId ?: defaultCameraBackendId
+        return cameraMetadataMap[backendId]?.map { it.camera }
+    }
+
+    override suspend fun getCameraMetadata(
+        cameraId: CameraId,
+        cameraBackendId: CameraBackendId?
+    ): CameraMetadata? = awaitCameraMetadata(cameraId, cameraBackendId)
+
+    override fun awaitCameraMetadata(
+        cameraId: CameraId,
+        cameraBackendId: CameraBackendId?
+    ): CameraMetadata? {
+        val backendId = cameraBackendId ?: defaultCameraBackendId
+        return cameraMetadataMap[backendId]?.firstOrNull { it.camera == cameraId }
+    }
+
     @Deprecated(
-        message = "findAll may block the calling thread and is deprecated.",
-        replaceWith = ReplaceWith("ids"),
+        "findAll() is not able to specify a specific CameraBackendId to query.",
+        replaceWith = ReplaceWith("awaitCameraIds"),
         level = DeprecationLevel.WARNING
     )
-    override fun findAll(): List<CameraId> = runBlocking { ids() }
-    override suspend fun ids(): List<CameraId> = cameras.map { it.camera }
+    override fun findAll(): List<CameraId> = checkNotNull(awaitCameraIds())
 
-    override suspend fun getMetadata(camera: CameraId): CameraMetadata = awaitMetadata(camera)
-    override fun awaitMetadata(camera: CameraId): CameraMetadata = cameras.first {
-        it.camera == camera
-    }
+    @Deprecated(
+        "ids() is not able to specify a specific CameraBackendId to query.",
+        replaceWith = ReplaceWith("getCameraIds"),
+        level = DeprecationLevel.WARNING
+    )
+    override suspend fun ids(): List<CameraId> = checkNotNull(getCameraIds())
+
+    @Deprecated(
+        "getMetadata() is not able to specify a specific CameraBackendId to query.",
+        replaceWith = ReplaceWith("getCameraMetadata"),
+        level = DeprecationLevel.WARNING
+    )
+    override suspend fun getMetadata(camera: CameraId): CameraMetadata =
+        checkNotNull(getCameraMetadata(camera))
+
+    @Deprecated(
+        "awaitMetadata() is not able to specify a specific CameraBackendId to query.",
+        replaceWith = ReplaceWith("awaitCameraMetadata"),
+        level = DeprecationLevel.WARNING
+    )
+    override fun awaitMetadata(camera: CameraId): CameraMetadata =
+        checkNotNull(awaitCameraMetadata(camera))
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt
index fb5c682..f709049 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt
@@ -18,26 +18,42 @@
 
 import android.hardware.camera2.CameraCharacteristics
 import android.os.Build
+import androidx.camera.camera2.pipe.CameraBackendId
+import androidx.camera.camera2.pipe.testing.FakeCameraBackend.Companion.FAKE_CAMERA_BACKEND_ID
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 public class FakeCameraDevicesTest {
-    @Test
-    fun cameraMetadataIsNotEqual() {
-        val metadata1 = FakeCameraMetadata(
-            mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
-        )
-        val metadata2 = FakeCameraMetadata(
-            mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
-        )
+    private val EXTERNAL_BACKEND_ID =
+        CameraBackendId("androidx.camera.camera2.pipe.testing.FakeCameraDevicesTest")
+    private val metadata1 = FakeCameraMetadata(
+        mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
+    )
+    private val metadata2 = FakeCameraMetadata(
+        mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
+    )
+    private val metadata3 = FakeCameraMetadata(
+        mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_EXTERNAL)
+    )
+    private val cameraMetadataMap = mapOf(
+        FAKE_CAMERA_BACKEND_ID to listOf(metadata1, metadata2),
+        EXTERNAL_BACKEND_ID to listOf(metadata3)
+    )
 
-        val cameraDevices = FakeCameraDevices(listOf(metadata1, metadata2))
-        val devices = runBlocking { cameraDevices.ids() }
+    @Test
+    fun getCameraIdsReturnsDefaultCameraIdList() = runTest {
+        val cameraDevices = FakeCameraDevices(
+            defaultCameraBackendId = FAKE_CAMERA_BACKEND_ID,
+            cameraMetadataMap = cameraMetadataMap
+        )
+        val devices = cameraDevices.getCameraIds()
         assertThat(devices).containsExactlyElementsIn(
             listOf(
                 metadata1.camera,
@@ -45,7 +61,29 @@
             )
         ).inOrder()
 
-        assertThat(cameraDevices.awaitMetadata(metadata1.camera)).isSameInstanceAs(metadata1)
-        assertThat(cameraDevices.awaitMetadata(metadata2.camera)).isSameInstanceAs(metadata2)
+        assertThat(cameraDevices.getCameraMetadata(metadata1.camera)).isSameInstanceAs(metadata1)
+        assertThat(cameraDevices.getCameraMetadata(metadata2.camera)).isSameInstanceAs(metadata2)
+    }
+
+    @Test
+    fun getCameraIdsWithBackendReturnsCustomCameraIdList() = runTest {
+        val cameraDevices = FakeCameraDevices(
+            defaultCameraBackendId = FAKE_CAMERA_BACKEND_ID,
+            cameraMetadataMap = cameraMetadataMap
+        )
+        val devices = cameraDevices.getCameraIds(EXTERNAL_BACKEND_ID)
+        assertThat(devices).containsExactlyElementsIn(
+            listOf(
+                metadata3.camera,
+            )
+        ).inOrder()
+
+        assertThat(cameraDevices.getCameraMetadata(metadata3.camera)).isNull()
+        assertThat(
+            cameraDevices.getCameraMetadata(
+                metadata3.camera,
+                EXTERNAL_BACKEND_ID
+            )
+        ).isSameInstanceAs(metadata3)
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/androidTest/java/androidx/camera/camera2/pipe/CameraPipeInstrumentationTest.kt b/camera/camera-camera2-pipe/src/androidTest/java/androidx/camera/camera2/pipe/CameraPipeInstrumentationTest.kt
index 8a2e4bc..6d67fe0 100644
--- a/camera/camera-camera2-pipe/src/androidTest/java/androidx/camera/camera2/pipe/CameraPipeInstrumentationTest.kt
+++ b/camera/camera-camera2-pipe/src/androidTest/java/androidx/camera/camera2/pipe/CameraPipeInstrumentationTest.kt
@@ -26,7 +26,5 @@
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = 21)
 class CameraPipeInstrumentationTest {
-    @Test
-    fun test() {
-    }
-}
\ No newline at end of file
+    @Test fun test() {}
+}
diff --git a/camera/camera-camera2-pipe/src/androidTest/java/androidx/camera/camera2/pipe/impl/TokenLockInstrumentationTest.kt b/camera/camera-camera2-pipe/src/androidTest/java/androidx/camera/camera2/pipe/impl/TokenLockInstrumentationTest.kt
index 54157cf..ab4323f 100644
--- a/camera/camera-camera2-pipe/src/androidTest/java/androidx/camera/camera2/pipe/impl/TokenLockInstrumentationTest.kt
+++ b/camera/camera-camera2-pipe/src/androidTest/java/androidx/camera/camera2/pipe/impl/TokenLockInstrumentationTest.kt
@@ -57,4 +57,4 @@
 
         assertThat(tokenLock.available).isEqualTo(10)
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt
index 760e4e8..098631d 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt
@@ -18,11 +18,8 @@
 import androidx.camera.camera2.pipe.graph.GraphListener
 import kotlinx.coroutines.Deferred
 
-/**
- * This is used to uniquely identify a specific backend implementation.
- */
-@JvmInline
-value class CameraBackendId(public val value: String)
+/** This is used to uniquely identify a specific backend implementation. */
+@JvmInline value class CameraBackendId(public val value: String)
 
 /**
  * A CameraBackend is used by [CameraPipe] to abstract out the lifecycle, state, and interactions
@@ -40,21 +37,27 @@
     val id: CameraBackendId
 
     /**
-     * Read out a list of openable [CameraId]s for this backend. This call may block the calling
-     * thread and should not cache the list of [CameraId]s if it's possible for them to change at
-     * runtime.
+     * Read out a list of _openable_ [CameraId]s for this backend. The backend may be able to report
+     * Metadata for non-openable cameras. However, these cameras should not appear the list of
+     * cameras returned by [getCameraIds].
      */
-    fun readCameraIdList(): List<CameraId>
+    suspend fun getCameraIds(): List<CameraId>? = awaitCameraIds()
 
-    /** Retrieve [CameraMetadata] for this backend. This call may block the calling thread and
-     * should not internally cache the [CameraMetadata] instance if it's possible for it to change
-     * at runtime.
+    /** Thread-blocking version of [getCameraIds] for compatibility. */
+    fun awaitCameraIds(): List<CameraId>?
+
+    /**
+     * Retrieve [CameraMetadata] for this backend. Backends may cache the results of these calls.
      *
-     * This call should should succeed if the [CameraId] is in the list of ids returned by
-     * [readCameraIdList]. For some backends, it may be possible to retrieve metadata for cameras
-     * that cannot be opened directly.
+     * This call should should always succeed if the [CameraId] is in the list of ids returned by
+     * [getCameraIds]. For some backends, it may be possible to retrieve metadata for cameras that
+     * cannot be opened directly.
      */
-    fun readCameraMetadata(cameraId: CameraId): CameraMetadata
+    suspend fun getCameraMetadata(cameraId: CameraId): CameraMetadata? =
+        awaitCameraMetadata(cameraId)
+
+    /** Thread-blocking version of [getCameraMetadata] for compatibility. */
+    fun awaitCameraMetadata(cameraId: CameraId): CameraMetadata?
 
     /**
      * Stops all active [CameraController]s, which may disconnect any cached camera connection(s).
@@ -78,8 +81,8 @@
 
     /**
      * Creates a new [CameraController] instance that can be used to initialize and interact with a
-     * specific camera device defined by this CameraBackend. Creating a [CameraController] should
-     * _not_ begin opening or interacting with the camera device until [CameraController.start] is
+     * specific Camera that is available from this CameraBackend. Creating a [CameraController]
+     * should _not_ begin opening or interacting with the Camera until [CameraController.start] is
      * called.
      */
     fun createCameraController(
@@ -98,9 +101,7 @@
  * and release previously created [CameraBackend]s.
  */
 fun interface CameraBackendFactory {
-    /**
-     * Create a new [CameraBackend] instance based on the provided [CameraContext].
-     */
+    /** Create a new [CameraBackend] instance based on the provided [CameraContext]. */
     fun create(cameraContext: CameraContext): CameraBackend
 }
 
@@ -129,8 +130,8 @@
     val activeIds: Set<CameraBackendId>
 
     /**
-     * Get a previously created [CameraBackend] instance, or create a new one. If the backend
-     * fails to load or is not available, this method will return null.
+     * Get a previously created [CameraBackend] instance, or create a new one. If the backend fails
+     * to load or is not available, this method will return null.
      */
     operator fun get(backendId: CameraBackendId): CameraBackend?
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraContext.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraContext.kt
index 9ca6c1a..a4908d0 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraContext.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraContext.kt
@@ -30,4 +30,4 @@
     val appContext: Context
     val threads: Threads
     val cameraBackends: CameraBackends
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraController.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraController.kt
index 086e22b..0eacef9 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraController.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraController.kt
@@ -22,9 +22,9 @@
 /**
  * A single [CameraController] handles the state and connection status for a [CameraGraph] instance.
  *
- * Calling [start] should eventually invoke [GraphListener.onGraphStarted] on the listener that
- * was used to create this [CameraController] instance. Creating a [CameraController]
- * should not initiate or start opening the underlying camera as part of the creation process.
+ * Calling [start] should eventually invoke [GraphListener.onGraphStarted] on the listener that was
+ * used to create this [CameraController] instance. Creating a [CameraController] should not
+ * initiate or start opening the underlying camera as part of the creation process.
  *
  * If the connection fails or the underlying camera encounters a failure that may be recoverable,
  * [GraphListener.onGraphStopped] should be invoked. If the state of the camera changes in any way
@@ -36,8 +36,8 @@
  */
 interface CameraController {
     /**
-     * Connect and start the underlying camera.This may be called on the main thread and should
-     * not make long blocking calls. This may be called opportunistically (eg, whenever a lifecycle
+     * Connect and start the underlying camera.This may be called on the main thread and should not
+     * make long blocking calls. This may be called opportunistically (eg, whenever a lifecycle
      * indicates the camera should be in a running state)
      */
     fun start()
@@ -49,15 +49,15 @@
     fun stop()
 
     /**
-     * Close this instance. [start] and [stop] should not be invoked, and any additional
-     * calls will be ignored once this method returns. Depending on implementation the underlying
-     * camera connection may not be terminated immediately, depending on the [CameraBackend]
+     * Close this instance. [start] and [stop] should not be invoked, and any additional calls will
+     * be ignored once this method returns. Depending on implementation the underlying camera
+     * connection may not be terminated immediately, depending on the [CameraBackend]
      */
     fun close()
 
     /**
-     * Tell the [CameraController] the current mapping between [StreamId] and [Surface]s. This
-     * map should always contain at least one entry, and should never contain [StreamId]s that were
+     * Tell the [CameraController] the current mapping between [StreamId] and [Surface]s. This map
+     * should always contain at least one entry, and should never contain [StreamId]s that were
      * missing from the [StreamGraph] that was used to create this [CameraController].
      */
     fun updateSurfaceMap(surfaceMap: Map<StreamId, Surface>)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt
index bc8e182..7c79688 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt
@@ -26,9 +26,7 @@
 
 // Public controls and enums used to interact with a CameraGraph.
 
-/**
- * An enum to match the CameraMetadata.CONTROL_AF_MODE_* constants.
- */
+/** An enum to match the CameraMetadata.CONTROL_AF_MODE_* constants. */
 public enum class AfMode(public val value: Int) {
     OFF(CameraMetadata.CONTROL_AF_MODE_OFF),
     AUTO(CameraMetadata.CONTROL_AF_MODE_AUTO),
@@ -43,9 +41,7 @@
     }
 }
 
-/**
- * An enum to match the CameraMetadata.CONTROL_AE_MODE_* constants.
- */
+/** An enum to match the CameraMetadata.CONTROL_AE_MODE_* constants. */
 public enum class AeMode(public val value: Int) {
     OFF(CameraMetadata.CONTROL_AE_MODE_OFF),
     ON(CameraMetadata.CONTROL_AE_MODE_ON),
@@ -59,9 +55,7 @@
     }
 }
 
-/**
- * An enum to match the CameraMetadata.CONTROL_AWB_MODE_* constants.
- */
+/** An enum to match the CameraMetadata.CONTROL_AWB_MODE_* constants. */
 public enum class AwbMode(public val value: Int) {
     AUTO(CameraMetadata.CONTROL_AWB_MODE_AUTO),
     CLOUDY_DAYLIGHT(CameraMetadata.CONTROL_AWB_MODE_CLOUDY_DAYLIGHT),
@@ -75,9 +69,7 @@
     }
 }
 
-/**
- * An enum to match the CameraMetadata.FLASH_MODE_* constants.
- */
+/** An enum to match the CameraMetadata.FLASH_MODE_* constants. */
 public enum class FlashMode(public val value: Int) {
     OFF(CameraMetadata.FLASH_MODE_OFF),
     SINGLE(CameraMetadata.FLASH_MODE_SINGLE),
@@ -85,9 +77,8 @@
 
     public companion object {
         @JvmStatic
-        public fun fromIntOrNull(value: Int): FlashMode? = values().firstOrNull {
-            it.value == value
-        }
+        public fun fromIntOrNull(value: Int): FlashMode? =
+            values().firstOrNull { it.value == value }
     }
 }
 
@@ -95,8 +86,10 @@
  * Enum to turn the torch on/off.
  *
  * <https://developer.android.com/reference/android/hardware/camera2/CameraMetadata
+ *
  * #CONTROL_AE_MODE_OFF
  * https://developer.android.com/reference/android/hardware/camera2/CameraMetadata
+ *
  * #CONTROL_AE_MODE_ON
  */
 public enum class TorchState {
@@ -104,15 +97,13 @@
     OFF
 }
 
-/**
- * Requirement to consider prior to locking auto-exposure, auto-focus and auto-whitebalance.
- */
+/** Requirement to consider prior to locking auto-exposure, auto-focus and auto-whitebalance. */
 public enum class Lock3ABehavior {
     /**
      * This requirement means that we want to lock the values for 3A immediately.
      *
-     * For AE/AWB this is achieved by asking the camera device to lock them immediately by
-     * setting [android.hardware.camera2.CaptureRequest.CONTROL_AE_LOCK],
+     * For AE/AWB this is achieved by asking the camera device to lock them immediately by setting
+     * [android.hardware.camera2.CaptureRequest.CONTROL_AE_LOCK],
      * [android.hardware.camera2.CaptureRequest.CONTROL_AWB_LOCK] to true right away.
      *
      * For AF we immediately ask the camera device to trigger AF by setting the
@@ -127,9 +118,7 @@
      */
     AFTER_CURRENT_SCAN,
 
-    /**
-     * Initiate a new scan, and then lock the values once the scan is done.
-     */
+    /** Initiate a new scan, and then lock the values once the scan is done. */
     AFTER_NEW_SCAN,
 }
 
@@ -138,16 +127,17 @@
  *
  * @param status [Status] of the 3A operation at the time of return.
  * @param frameMetadata [FrameMetadata] of the latest frame at which the method succeeded or was
- * aborted. The metadata reflects CaptureResult or TotalCaptureResult for that frame. It can so
- * happen that the [CaptureResult] itself has all the key-value pairs needed to determine the
- * completion of the method, in that case this frameMetadata may not contain all the kay value pairs
- * associated with the final result i.e [TotalCaptureResult] of this frame.
+ *   aborted. The metadata reflects CaptureResult or TotalCaptureResult for that frame. It can so
+ *   happen that the [CaptureResult] itself has all the key-value pairs needed to determine the
+ *   completion of the method, in that case this frameMetadata may not contain all the kay value
+ *   pairs associated with the final result i.e. [TotalCaptureResult] of this frame.
  */
 public data class Result3A(val status: Status, val frameMetadata: FrameMetadata? = null) {
     /**
      * Enum to know the status of 3A operation in case the method returns before the desired
      * operation is complete. The reason could be that the operation was talking a lot longer and an
-     * enforced frame or time limit was reached, submitting the desired request to camera failed etc.
+     * enforced frame or time limit was reached, submitting the desired request to camera failed
+     * etc.
      */
     public enum class Status {
         OK,
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraDevices.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraDevices.kt
index b686291..a28d882 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraDevices.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraDevices.kt
@@ -23,10 +23,38 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.flow
 
-/**
- * Methods for querying, iterating, and selecting the Cameras that are available on the device.
- */
+/** Methods for querying, iterating, and selecting the Cameras that are available on the device. */
 public interface CameraDevices {
+    /**
+     * Read the list of currently openable CameraIds from the provided CameraBackend, suspending if
+     * needed. By default this will load the list of openable CameraIds from the default backend.
+     */
+    suspend fun getCameraIds(cameraBackendId: CameraBackendId? = null): List<CameraId>?
+
+    /**
+     * Read the list of currently openable CameraIds from the provided CameraBackend, blocking the
+     * thread if needed. By default this will load the list of openable CameraIds from the default
+     * backend.
+     */
+    fun awaitCameraIds(cameraBackendId: CameraBackendId? = null): List<CameraId>?
+
+    /**
+     * Read metadata for a specific camera id, suspending if needed. By default, this method will
+     * query metadata from the default backend if one is not specified.
+     */
+    suspend fun getCameraMetadata(
+        cameraId: CameraId,
+        cameraBackendId: CameraBackendId? = null
+    ): CameraMetadata?
+
+    /**
+     * Read metadata for a specific camera id, blocking if needed. By default, this method will
+     * query metadata from the default backend if one is not specified.
+     */
+    fun awaitCameraMetadata(
+        cameraId: CameraId,
+        cameraBackendId: CameraBackendId? = null
+    ): CameraMetadata?
 
     /**
      * Iterate and return a list of CameraId's on the device that are capable of being opened. Some
@@ -34,29 +62,40 @@
      * group.
      */
     @Deprecated(
-        message = "findAll may block the calling thread and is deprecated.",
-        replaceWith = ReplaceWith("ids"),
-        level = DeprecationLevel.WARNING
-    )
+        message = "findAll() is not able to specify a specific CameraBackendId to query.",
+        replaceWith = ReplaceWith("awaitCameraIds"),
+        level = DeprecationLevel.WARNING)
     public fun findAll(): List<CameraId>
 
     /**
      * Load the list of CameraIds from the Camera2 CameraManager, suspending if the list of
      * CameraIds has not yet been loaded.
      */
+    @Deprecated(
+        message = "ids() is not able to specify a specific CameraBackendId to query.",
+        replaceWith = ReplaceWith("getCameraIds"),
+        level = DeprecationLevel.WARNING)
     public suspend fun ids(): List<CameraId>
 
     /**
-     * Load CameraMetadata for a specific CameraId. Loading CameraMetadata can take a
-     * non-zero amount of time to execute. If CameraMetadata is not already cached this function
-     * will suspend until CameraMetadata can be loaded.
+     * Load CameraMetadata for a specific CameraId. Loading CameraMetadata can take a non-zero
+     * amount of time to execute. If CameraMetadata is not already cached this function will suspend
+     * until CameraMetadata can be loaded.
      */
+    @Deprecated(
+        message = "getMetadata() is not able to specify a specific CameraBackendId to query.",
+        replaceWith = ReplaceWith("getCameraMetadata"),
+        level = DeprecationLevel.WARNING)
     public suspend fun getMetadata(camera: CameraId): CameraMetadata
 
     /**
      * Load CameraMetadata for a specific CameraId and block the calling thread until the result is
      * available.
      */
+    @Deprecated(
+        message = "awaitMetadata() is not able to specify a specific CameraBackendId to query.",
+        replaceWith = ReplaceWith("awaitCameraMetadata"),
+        level = DeprecationLevel.WARNING)
     public fun awaitMetadata(camera: CameraId): CameraMetadata
 }
 
@@ -81,27 +120,36 @@
  * metadata of cameras that are otherwise hidden. Metadata for hidden cameras are always returned
  * last.
  */
-public fun CameraDevices.find(includeHidden: Boolean = false): Flow<CameraMetadata> =
-    flow {
-        val cameras = this@find.ids()
-        val visited = mutableSetOf<CameraId>()
+public fun CameraDevices.find(
+    cameraBackendId: CameraBackendId? = null,
+    includePhysicalCameraMetadata: Boolean = false
+): Flow<CameraMetadata> = flow {
+    val cameraIds = this@find.getCameraIds() ?: return@flow
 
-        for (id in cameras) {
-            if (visited.add(id)) {
-                val metadata = this@find.getMetadata(id)
+    val visited = mutableSetOf<CameraId>()
+    val emitted = mutableSetOf<CameraMetadata>()
+    for (cameraId in cameraIds) {
+        if (visited.add(cameraId)) {
+            val metadata = this@find.getCameraMetadata(cameraId, cameraBackendId)
+            if (metadata != null) {
+                emitted.add(metadata)
                 emit(metadata)
             }
         }
+    }
 
-        if (includeHidden) {
-            for (id in cameras) {
-                val metadata = this@find.getMetadata(id)
-                for (physicalId in metadata.physicalCameraIds) {
-                    if (visited.add(physicalId)) {
-                        val physicalMetadata = this@find.getMetadata(id)
+    if (includePhysicalCameraMetadata) {
+        for (metadata in emitted) {
+            for (physicalId in metadata.physicalCameraIds) {
+                if (!visited.contains(physicalId)) {
+                    val physicalMetadata = this@find.getCameraMetadata(physicalId, cameraBackendId)
+                    if (physicalMetadata != null &&
+                        physicalMetadata.camera == physicalId &&
+                        visited.add(physicalId)) {
                         emit(physicalMetadata)
                     }
                 }
             }
         }
     }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraError.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraError.kt
index 17fb42f..2c0ca3d 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraError.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraError.kt
@@ -81,14 +81,10 @@
          */
         val ERROR_CAMERA_DISCONNECTED = CameraError(6)
 
-        /**
-         * This indicates that we received IllegalArgumentException while opening the camera.
-         */
+        /** This indicates that we received IllegalArgumentException while opening the camera. */
         val ERROR_ILLEGAL_ARGUMENT_EXCEPTION = CameraError(7)
 
-        /**
-         * This indicates that we received SecurityException while opening the camera.
-         */
+        /** This indicates that we received SecurityException while opening the camera. */
         val ERROR_SECURITY_EXCEPTION = CameraError(8)
 
         internal fun from(throwable: Throwable) =
@@ -110,9 +106,7 @@
                 MAX_CAMERAS_IN_USE -> ERROR_CAMERA_LIMIT_EXCEEDED
                 else -> {
                     throw IllegalArgumentException(
-                        "Unexpected CameraAccessException reason:" +
-                            "${exception.reason}"
-                    )
+                        "Unexpected CameraAccessException reason:" + "${exception.reason}")
                 }
             }
 
@@ -125,10 +119,8 @@
                 StateCallback.ERROR_CAMERA_SERVICE -> ERROR_CAMERA_SERVICE
                 else -> {
                     throw IllegalArgumentException(
-                        "Unexpected StateCallback error code:" +
-                            "$stateCallbackError"
-                    )
+                        "Unexpected StateCallback error code:" + "$stateCallbackError")
                 }
             }
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
index 0c7138c..ff0b20a 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
@@ -26,7 +26,6 @@
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_FRAME_LIMIT
 import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_NS
-import java.io.Closeable
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.flow.StateFlow
 
@@ -62,16 +61,14 @@
         GraphState()
 }
 
-/**
- * A [CameraGraph] represents the combined configuration and state of a camera.
- */
+/** A [CameraGraph] represents the combined configuration and state of a camera. */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public interface CameraGraph : Closeable {
+public interface CameraGraph : AutoCloseable {
     public val streams: StreamGraph
 
     /**
-     * Returns the state flow of [GraphState], which emits the current state of the
-     * [CameraGraph], including when a [CameraGraph] is stopped, starting or started.
+     * Returns the state flow of [GraphState], which emits the current state of the [CameraGraph],
+     * including when a [CameraGraph] is stopped, starting or started.
      */
     public val graphState: StateFlow<GraphState>
 
@@ -85,14 +82,12 @@
     /**
      * This will cause the [CameraGraph] to stop executing requests and close the current Camera2
      * [CameraCaptureSession] (if one is active). The most recent repeating request will be
-     * preserved, and any calls to submit a request to a session will be enqueued. To stop
-     * requests from being enqueued, close the [CameraGraph].
+     * preserved, and any calls to submit a request to a session will be enqueued. To stop requests
+     * from being enqueued, close the [CameraGraph].
      */
     public fun stop()
 
-    /**
-     * Acquire and exclusive access to the [CameraGraph] in a suspending fashion.
-     */
+    /** Acquire and exclusive access to the [CameraGraph] in a suspending fashion. */
     public suspend fun acquireSession(): Session
 
     /**
@@ -127,8 +122,8 @@
      * @param defaultTemplate The default template to be used if a [Request] does not specify one.
      * @param defaultParameters The default parameters to be used for a [Request].
      * @param defaultListeners A default set of listeners that will be added to every [Request].
-     * @param requiredParameters Will override any other configured parameter, and can be used
-     *   to enforce that specific keys are always set to specific value for every [CaptureRequest].
+     * @param requiredParameters Will override any other configured parameter, and can be used to
+     *   enforce that specific keys are always set to specific value for every [CaptureRequest].
      * @param cameraBackendId If defined, this tells the [CameraGraph] to use a specific
      *   [CameraBackend] to open and operate the camera. The defined [camera] parameter must be a
      *   camera that can be opened by this [CameraBackend]. If this value is null it will use the
@@ -148,12 +143,11 @@
         val defaultParameters: Map<*, Any?> = emptyMap<Any, Any?>(),
         val defaultListeners: List<Request.Listener> = listOf(),
         val requiredParameters: Map<*, Any?> = emptyMap<Any, Any?>(),
-
         val cameraBackendId: CameraBackendId? = null,
         val customCameraBackend: CameraBackendFactory? = null,
         val metadataTransform: MetadataTransform = MetadataTransform(),
         val flags: Flags = Flags()
-        // TODO: Internal error handling. May be better at the CameraPipe level.
+    // TODO: Internal error handling. May be better at the CameraPipe level.
     ) {
         init {
             check(cameraBackendId == null || customCameraBackend == null) {
@@ -196,9 +190,7 @@
         public val METERING_REGIONS_DEFAULT: Array<MeteringRectangle> =
             arrayOf(MeteringRectangle(0, 0, 0, 0, 0))
 
-        /**
-         * Placeholder frame number for [Result3A] when a 3A method encounters an error.
-         */
+        /** Placeholder frame number for [Result3A] when a 3A method encounters an error. */
         public val FRAME_NUMBER_INVALID: FrameNumber = FrameNumber(-1L)
     }
 
@@ -206,35 +198,33 @@
      * A [Session] is an interactive lock for [CameraGraph] and allows state to be changed.
      *
      * Holding this object prevents other systems from acquiring a [Session] until the currently
-     * held session is released. Because of it's exclusive nature, [Session]s are intended for
-     * fast, short-lived state updates, or for interactive capture sequences that must not be
-     * altered. (Flash photo sequences, for example).
+     * held session is released. Because of its exclusive nature, [Session]s are intended for fast,
+     * short-lived state updates, or for interactive capture sequences that must not be altered.
+     * (Flash photo sequences, for example).
      *
      * While this object is thread-safe, it should not shared or held for long periods of time.
      * Example: A [Session] should *not* be held during video recording.
      */
-    public interface Session : Closeable {
+    public interface Session : AutoCloseable {
         /**
-         * Causes the CameraGraph to start or update the current repeating request with the
-         * provided [Request] object. The [Request] object may be cached, and may be used for
-         * other interactions with the camera (such as updating 3A, or issuing 3A triggers).
+         * Causes the CameraGraph to start or update the current repeating request with the provided
+         * [Request] object. The [Request] object may be cached, and may be used for other
+         * interactions with the camera (such as updating 3A, or issuing 3A triggers).
          */
         public fun startRepeating(request: Request)
 
-        /**
-         * Stop the current repeating request.
-         */
+        /** Stop the current repeating request. */
         public fun stopRepeating()
 
         /**
-         * Add the [Request] into an in-flight request queue. Requests will be issued to the
-         * Camera exactly once.
+         * Add the [Request] into an in-flight request queue. Requests will be issued to the Camera
+         * exactly once.
          */
         public fun submit(request: Request)
 
         /**
-         * Add the [Request] into an in-flight request queue. Requests will be issued to the
-         * Camera exactly once. The list of [Request]s is guaranteed to be submitted together.
+         * Add the [Request] into an in-flight request queue. Requests will be issued to the Camera
+         * exactly once. The list of [Request]s is guaranteed to be submitted together.
          */
         public fun submit(requests: List<Request>)
 
@@ -278,14 +268,14 @@
          *
          * This method has a side effect on the currently set AE mode. Ref:
          * https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#FLASH_MODE
-         * To use the flash control, AE mode must be set to ON or OFF. So if the AE mode is
-         * already not either ON or OFF, we will need to update the AE mode to one of those states,
-         * here we will choose ON. It is the responsibility of the application layer above
-         * CameraPipe to restore the AE mode after the torch control has been used. The
-         * [update3A] method can be used to restore the AE state to a previous value.
+         * To use the flash control, AE mode must be set to ON or OFF. So if the AE mode is already
+         * not either ON or OFF, we will need to update the AE mode to one of those states, here we
+         * will choose ON. It is the responsibility of the application layer above CameraPipe to
+         * restore the AE mode after the torch control has been used. The [update3A] method can be
+         * used to restore the AE state to a previous value.
          *
          * @return the FrameNumber at which the turn was fully turned on if switch was ON, or the
-         * FrameNumber at which it was completely turned off when the switch was OFF.
+         *   FrameNumber at which it was completely turned off when the switch was OFF.
          */
         public fun setTorch(torchState: TorchState): Deferred<Result3A>
 
@@ -295,22 +285,21 @@
          * value is passed for a parameter, that parameter is ignored, and the current value for
          * that parameter continues to be applied.
          *
-         * TODO(sushilnath@): Add support for specifying the AE, AF and AWB modes as well. The
-         * update of modes require special care if the desired lock behavior is immediate. In
-         * that case we have to submit a combination of repeating and single requests so that the
-         * AF skips the initial state of the new mode's state machine and stays locks in the new
-         * mode as well.
-         *
          * @param afTriggerStartAeMode the AeMode value that should override current AeMode for
-         * AF_TRIGGER_START request, this value should not be retained for following requests
-         * @param frameLimit the maximum number of frames to wait before we give up waiting for
-         * this operation to complete.
+         *   AF_TRIGGER_START request, this value should not be retained for following requests
+         * @param frameLimit the maximum number of frames to wait before we give up waiting for this
+         *   operation to complete.
          * @param timeLimitNs the maximum time limit in ms we wait before we give up waiting for
-         * this operation to complete.
-         *
+         *   this operation to complete.
          * @return [Result3A], which will contain the latest frame number at which the locks were
-         * applied or the frame number at which the method returned early because either frame limit
-         * or time limit was reached.
+         *   applied or the frame number at which the method returned early because either frame
+         *   limit or time limit was reached.
+         *
+         * TODO(sushilnath@): Add support for specifying the AE, AF and AWB modes as well. The
+         *   update of modes require special care if the desired lock behavior is immediate. In that
+         *   case we have to submit a combination of repeating and single requests so that the AF
+         *   skips the initial state of the new mode's state machine and stays locks in the new mode
+         *   as well.
          */
         public suspend fun lock3A(
             aeMode: AeMode? = null,
@@ -329,8 +318,8 @@
 
         /**
          * Unlocks auto-exposure, auto-focus, auto-whitebalance. Once they are unlocked they get
-         * back to their initial state or resume their auto scan depending on the current mode
-         * they are operating in.
+         * back to their initial state or resume their auto scan depending on the current mode they
+         * are operating in.
          *
          * Providing 'true' for a parameter in this method will unlock that component and if 'false'
          * is provided or the parameter is not specified then it will have no effect on the lock of
@@ -338,28 +327,29 @@
          * unlocked, it will stay unlocked.
          *
          * @return [Result3A], which will contain the latest frame number at which the auto-focus,
-         * auto-exposure, auto-white balance were unlocked as per the method arguments.
-         *
+         *   auto-exposure, auto-white balance were unlocked as per the method arguments.
          */
-        public suspend fun unlock3A(ae: Boolean? = null, af: Boolean? = null, awb: Boolean? = null):
-            Deferred<Result3A>
+        public suspend fun unlock3A(
+            ae: Boolean? = null,
+            af: Boolean? = null,
+            awb: Boolean? = null
+        ): Deferred<Result3A>
 
         /**
-         * This methods does pre-capture metering sequence and locks auto-focus. Once the
-         * operation completes, we can proceed to take high-quality pictures.
+         * This methods does pre-capture metering sequence and locks auto-focus. Once the operation
+         * completes, we can proceed to take high-quality pictures.
          *
-         * Note: Flash will be used during pre-capture metering and during image capture if the
-         * AE mode was set to [AeMode.ON_AUTO_FLASH] or [AeMode.ON_ALWAYS_FLASH], thus firing it
-         * for low light captures or for every capture, respectively.
+         * Note: Flash will be used during pre-capture metering and during image capture if the AE
+         * mode was set to [AeMode.ON_AUTO_FLASH] or [AeMode.ON_ALWAYS_FLASH], thus firing it for
+         * low light captures or for every capture, respectively.
          *
-         * @param frameLimit the maximum number of frames to wait before we give up waiting for
-         * this operation to complete.
+         * @param frameLimit the maximum number of frames to wait before we give up waiting for this
+         *   operation to complete.
          * @param timeLimitNs the maximum time limit in ms we wait before we give up waiting for
-         * this operation to complete.
-         *
+         *   this operation to complete.
          * @return [Result3A], which will contain the latest frame number at which the locks were
-         * applied or the frame number at which the method returned early because either frame limit
-         * or time limit was reached.
+         *   applied or the frame number at which the method returned early because either frame
+         *   limit or time limit was reached.
          */
         public suspend fun lock3AForCapture(
             frameLimit: Int = DEFAULT_FRAME_LIMIT,
@@ -370,9 +360,8 @@
          * After submitting pre-capture metering sequence needed by [lock3AForCapture] method, the
          * camera system can internally lock the auto-exposure routine for subsequent still image
          * capture, and if not image capture request is submitted the auto-exposure may not resume
-         * it's normal scan.
-         * This method brings focus and exposure back to normal after high quality image captures
-         * using [lock3AForCapture] method.
+         * it's normal scan. This method brings focus and exposure back to normal after high quality
+         * image captures using [lock3AForCapture] method.
          */
         public suspend fun unlock3APostCapture(): Deferred<Result3A>
     }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraMetadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraMetadata.kt
index 78f1d39..01ad490 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraMetadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraMetadata.kt
@@ -26,12 +26,11 @@
 /**
  * [CameraMetadata] is a compatibility wrapper around [CameraCharacteristics].
  *
- * Applications should, in most situations, prefer using this interface to using the
- * unwrapping and using the underlying [CameraCharacteristics] object directly. Implementation(s) of
- * this interface provide compatibility guarantees and performance improvements over using
- * [CameraCharacteristics] directly. This allows code to get reasonable behavior for all properties
- * across all OS levels and makes behavior that depends on [CameraMetadata] easier to test and
- * reason about.
+ * Applications should, in most situations, prefer using this interface to using the unwrapping and
+ * using the underlying [CameraCharacteristics] object directly. Implementation(s) of this interface
+ * provide compatibility guarantees and performance improvements over using [CameraCharacteristics]
+ * directly. This allows code to get reasonable behavior for all properties across all OS levels and
+ * makes behavior that depends on [CameraMetadata] easier to test and reason about.
  */
 public interface CameraMetadata : Metadata, UnsafeWrapper {
     public operator fun <T> get(key: CameraCharacteristics.Key<T>): T?
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
index 3ecf584..b67d791 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
@@ -51,39 +51,37 @@
  */
 public class CameraPipe(config: Config) {
     private val debugId = cameraPipeIds.incrementAndGet()
-    private val component: CameraPipeComponent = DaggerCameraPipeComponent.builder()
-        .cameraPipeConfigModule(CameraPipeConfigModule(config))
-        .threadConfigModule(ThreadConfigModule(config.threadConfig))
-        .build()
+    private val component: CameraPipeComponent =
+        DaggerCameraPipeComponent.builder()
+            .cameraPipeConfigModule(CameraPipeConfigModule(config))
+            .threadConfigModule(ThreadConfigModule(config.threadConfig))
+            .build()
 
     /**
      * This creates a new [CameraGraph] that can be used to interact with a single Camera on the
      * device. Multiple [CameraGraph]s can be created, but only one should be active at a time.
      */
     public fun create(config: CameraGraph.Config): CameraGraph {
-        return component.cameraGraphComponentBuilder()
+        return component
+            .cameraGraphComponentBuilder()
             .cameraGraphConfigModule(CameraGraphConfigModule(config))
             .build()
             .cameraGraph()
     }
 
-    /**
-     * This provides access to information about the available cameras on the device.
-     */
+    /** This provides access to information about the available cameras on the device. */
     public fun cameras(): CameraDevices {
         return component.cameras()
     }
 
-    /**
-     * This returns [CameraSurfaceManager] which tracks the lifetime of Surfaces in CameraPipe.
-     */
+    /** This returns [CameraSurfaceManager] which tracks the lifetime of Surfaces in CameraPipe. */
     public fun cameraSurfaceManager(): CameraSurfaceManager {
         return component.cameraSurfaceManager()
     }
 
     /**
-     * Application level configuration for [CameraPipe]. Nullable values are optional and
-     * reasonable defaults will be provided if values are not specified.
+     * Application level configuration for [CameraPipe]. Nullable values are optional and reasonable
+     * defaults will be provided if values are not specified.
      */
     public data class Config(
         val appContext: Context,
@@ -94,8 +92,8 @@
     )
 
     /**
-     * Application level configuration for Camera2Interop callbacks. If set, these callbacks
-     * will be triggered at the appropriate places in Camera-Pipe.
+     * Application level configuration for Camera2Interop callbacks. If set, these callbacks will be
+     * triggered at the appropriate places in Camera-Pipe.
      */
     public data class CameraInteropConfig(
         val cameraDeviceStateCallback: CameraDevice.StateCallback? = null,
@@ -105,7 +103,6 @@
     /**
      * Application level configuration for default thread and executors. If set, these executors
      * will be used to run asynchronous background work across [CameraPipe].
-     *
      * - [defaultLightweightExecutor] is used to run fast, non-blocking, lightweight tasks.
      * - [defaultBackgroundExecutor] is used to run blocking and/or io bound tasks.
      * - [defaultCameraExecutor] is used on newer API versions to interact with CameraAPIs. This is
@@ -144,8 +141,8 @@
     /**
      * Configure the default and available [CameraBackend] instances that are available.
      *
-     * @param internalBackend will override the default camera backend defined by [CameraPipe].
-     *   This may be used to mock and replace all interactions with camera2.
+     * @param internalBackend will override the default camera backend defined by [CameraPipe]. This
+     *   may be used to mock and replace all interactions with camera2.
      * @param defaultBackend defines which camera backend instance should be used by default. If
      *   this value is specified, it must appear in the list of [cameraBackends]. If no value is
      *   specified, the [internalBackend] instance will be used. If [internalBackend] is null, the
@@ -169,28 +166,25 @@
     override fun toString(): String = "CameraPipe-$debugId"
 
     /**
-     * External may be used if the underlying implementation needs to delegate to another library
-     * or system.
+     * External may be used if the underlying implementation needs to delegate to another library or
+     * system.
      */
     @Deprecated(
-        "CameraPipe.External is deprecated, use customCameraBackend on " +
-            "GraphConfig instead."
-    )
+        "CameraPipe.External is deprecated, use customCameraBackend on " + "GraphConfig instead.")
     class External(threadConfig: ThreadConfig = ThreadConfig()) {
-        private val component: ExternalCameraPipeComponent = DaggerExternalCameraPipeComponent
-            .builder()
-            .threadConfigModule(ThreadConfigModule(threadConfig))
-            .build()
+        private val component: ExternalCameraPipeComponent =
+            DaggerExternalCameraPipeComponent.builder()
+                .threadConfigModule(ThreadConfigModule(threadConfig))
+                .build()
 
         /**
-         * This creates a new [CameraGraph] instance that is configured to use an externally
-         * defined [RequestProcessor].
+         * This creates a new [CameraGraph] instance that is configured to use an externally defined
+         * [RequestProcessor].
          */
         @Suppress("DEPRECATION")
         @Deprecated(
             "CameraPipe.External is deprecated, use customCameraBackend on " +
-                "GraphConfig instead."
-        )
+                "GraphConfig instead.")
         public fun create(
             config: CameraGraph.Config,
             cameraMetadata: CameraMetadata,
@@ -200,14 +194,11 @@
                 "Invalid camera config: ${config.camera} does not match ${cameraMetadata.camera}"
             }
             val componentBuilder = component.cameraGraphBuilder()
-            val component: ExternalCameraGraphComponent = componentBuilder
-                .externalCameraGraphConfigModule(
-                    ExternalCameraGraphConfigModule(
-                        config,
-                        cameraMetadata,
-                        requestProcessor
-                    )
-                ).build()
+            val component: ExternalCameraGraphComponent =
+                componentBuilder
+                    .externalCameraGraphConfigModule(
+                        ExternalCameraGraphConfigModule(config, cameraMetadata, requestProcessor))
+                    .build()
             return component.cameraGraph()
         }
     }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraSurfaceManager.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraSurfaceManager.kt
index b48552c..4f6e14c 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraSurfaceManager.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraSurfaceManager.kt
@@ -22,7 +22,6 @@
 import androidx.camera.camera2.pipe.CameraSurfaceManager.SurfaceListener
 import androidx.camera.camera2.pipe.CameraSurfaceManager.SurfaceToken
 import androidx.camera.camera2.pipe.core.Log
-import java.io.Closeable
 import javax.inject.Inject
 import javax.inject.Singleton
 import kotlinx.atomicfu.atomic
@@ -41,8 +40,8 @@
  * After all outstanding tokens have been closed, the listeners receive
  * SurfaceListener.onSurfaceInactive] for that surface.
  *
- * If the same [Surface] is used in a subsequent [CameraGraph], it will be issued a different
- * token. Essentially each token means a single use on a [Surface].
+ * If the same [Surface] is used in a subsequent [CameraGraph], it will be issued a different token.
+ * Essentially each token means a single use on a [Surface].
  */
 @Singleton
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@@ -50,20 +49,15 @@
 
     private val lock = Any()
 
-    @GuardedBy("lock")
-    private val useCountMap: MutableMap<Surface, Int> = mutableMapOf()
+    @GuardedBy("lock") private val useCountMap: MutableMap<Surface, Int> = mutableMapOf()
 
-    @GuardedBy("lock")
-    private val listeners: MutableSet<SurfaceListener> = mutableSetOf()
+    @GuardedBy("lock") private val listeners: MutableSet<SurfaceListener> = mutableSetOf()
 
     /**
-     * A new [SurfaceToken] is issued when a [Surface] is registered in CameraSurfaceManager.
-     * When all [SurfaceToken]s issued for a [Surface] is closed, the [Surface] is considered
-     * "inactive".
+     * A new [SurfaceToken] is issued when a [Surface] is registered in CameraSurfaceManager. When
+     * all [SurfaceToken]s issued for a [Surface] is closed, the [Surface] is considered "inactive".
      */
-    inner class SurfaceToken(
-        internal val surface: Surface
-    ) : Closeable {
+    inner class SurfaceToken(internal val surface: Surface) : AutoCloseable {
         private val closed = atomic(false)
         override fun close() {
             if (closed.compareAndSet(expect = false, update = true)) {
@@ -86,39 +80,35 @@
         /**
          * Called when a [Surface] is considered "inactive" and no longer in use by [CameraGraph].
          * This can happen under a few different scenarios:
-         *
-         *   1. A [Surface] is unset or replaced in a [CameraGraph].
-         *   2. A CaptureSession is closed or fails to configure and the [CameraGraph] has been
-         *      closed.
-         *   3. [CameraGraph] is closed, and the [Surface] isn't not in use by some other
-         *      camera subsystem.
+         * 1. A [Surface] is unset or replaced in a [CameraGraph].
+         * 2. A CaptureSession is closed or fails to configure and the [CameraGraph] has been
+         *    closed.
+         * 3. [CameraGraph] is closed, and the [Surface] isn't not in use by some other camera
+         *    subsystem.
          */
         fun onSurfaceInactive(surface: Surface)
     }
 
     /**
-     * Adds a [SurfaceListener] to receive [Surface] lifetime updates. When a listener is added,
-     * it will receive [SurfaceListener.onSurfaceActive] for all active Surfaces.
+     * Adds a [SurfaceListener] to receive [Surface] lifetime updates. When a listener is added, it
+     * will receive [SurfaceListener.onSurfaceActive] for all active Surfaces.
      */
     public fun addListener(listener: SurfaceListener) {
-        val activeSurfaces = synchronized(lock) {
-            listeners.add(listener)
-            useCountMap.filter { it.value > 0 }.keys
-        }
+        val activeSurfaces =
+            synchronized(lock) {
+                listeners.add(listener)
+                useCountMap.filter { it.value > 0 }.keys
+            }
 
         activeSurfaces.forEach { listener.onSurfaceActive(it) }
     }
 
-    /**
-     * Removes a [SurfaceListener] to stop receiving [Surface] lifetime updates.
-     */
+    /** Removes a [SurfaceListener] to stop receiving [Surface] lifetime updates. */
     public fun removeListener(listener: SurfaceListener) {
-        synchronized(lock) {
-            listeners.remove(listener)
-        }
+        synchronized(lock) { listeners.remove(listener) }
     }
 
-    internal fun registerSurface(surface: Surface): Closeable {
+    internal fun registerSurface(surface: Surface): AutoCloseable {
         check(surface.isValid) { "Surface $surface isn't valid!" }
         val surfaceToken: SurfaceToken
         var listenersToInvoke: List<SurfaceListener>? = null
@@ -155,4 +145,4 @@
 
         listenersToInvoke?.forEach { it.onSurfaceInactive(surface) }
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CaptureSequence.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CaptureSequence.kt
index 3f2cf42..a85bc64 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CaptureSequence.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CaptureSequence.kt
@@ -38,9 +38,7 @@
     }
 }
 
-/**
- * Utility functions for interacting with [CaptureSequence] callbacks and listeners.
- */
+/** Utility functions for interacting with [CaptureSequence] callbacks and listeners. */
 object CaptureSequences {
     /**
      * Efficient, inlined utility function for invoking a call on each of the listeners defined on a
@@ -86,4 +84,4 @@
             fn(request.request.listeners[i])
         }
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CaptureSequenceProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CaptureSequenceProcessor.kt
index 0fcecd7..8e33af1 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CaptureSequenceProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CaptureSequenceProcessor.kt
@@ -24,23 +24,22 @@
      * Build a [CaptureSequence] instance.
      *
      * @param isRepeating determines if this CaptureSequence should repeat until replaced by another
-     * repeating CaptureSequence, or closed, or stopRepeating is invoked.
+     *   repeating CaptureSequence, or closed, or stopRepeating is invoked.
      * @param requests the list of [Request] to use when constructing this [CaptureSequence]
      * @param defaultParameters are the parameters to start with when building an individual
-     * [TCaptureRequest] object. Parameters not specified on a [Request] will use these parameters
-     * by default.
+     *   [TCaptureRequest] object. Parameters not specified on a [Request] will use these parameters
+     *   by default.
      * @param requiredParameters are parameters that will override all [defaultParameters] *and*
-     * parameters that are defined on the [Request].
+     *   parameters that are defined on the [Request].
      * @param listeners are global and internal [Request.Listener]s that should be invoked every
-     * time the listeners on the [Request] are invoked. Since these often track and update internal
-     * state they should be invoked before listeners on the individual [Request].
+     *   time the listeners on the [Request] are invoked. Since these often track and update
+     *   internal state they should be invoked before listeners on the individual [Request].
      * @param sequenceListener is an extra listener that should be invoked whenever a specific
-     * [CaptureSequence] should no longer receive any additional events.
-     *
+     *   [CaptureSequence] should no longer receive any additional events.
      * @return a [TCaptureSequence] instance that can be used to capture images using the underlying
-     * camera by passing this [submit]. This method will return null if the underlying camera has
-     * been closed or disconnected, and will throw unchecked exceptions if invalid values are passed
-     * to the [build] call.
+     *   camera by passing this [submit]. This method will return null if the underlying camera has
+     *   been closed or disconnected, and will throw unchecked exceptions if invalid values are
+     *   passed to the [build] call.
      */
     fun build(
         isRepeating: Boolean,
@@ -68,4 +67,4 @@
      * to be processed, and [abortCaptures] and [stopRepeating] may still be invoked.
      */
     fun close()
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
index 2c1067c..1529c70 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
@@ -36,22 +36,17 @@
     public operator fun <T> get(key: Key<T>): T?
     public fun <T> getOrDefault(key: Key<T>, default: T): T
 
-    /**
-     * Metadata keys provide values or controls that are provided or computed by CameraPipe.
-     */
+    /** Metadata keys provide values or controls that are provided or computed by CameraPipe. */
     public class Key<T> private constructor(private val name: String) {
         public companion object {
-            @JvmStatic
-            internal val keys: MutableSet<String> = HashSet()
+            @JvmStatic internal val keys: MutableSet<String> = HashSet()
 
             /**
              * This will create a new Key instance, and will check to see that the key has not been
              * previously created somewhere else.
              */
             public fun <T> create(name: String): Key<T> {
-                synchronized(keys) {
-                    check(keys.add(name)) { "$name is already defined!" }
-                }
+                synchronized(keys) { check(keys.add(name)) { "$name is already defined!" } }
                 return Key(name)
             }
         }
@@ -78,9 +73,9 @@
     public val template: RequestTemplate
 
     /**
-     * A Map of StreamId(s) that were submitted with this CaptureRequest and the Surface(s) used
-     * for this request. It's possible that not all of the streamId's specified in the [Request]
-     * are present in the [CaptureRequest].
+     * A Map of StreamId(s) that were submitted with this CaptureRequest and the Surface(s) used for
+     * this request. It's possible that not all of the streamId's specified in the [Request] are
+     * present in the [CaptureRequest].
      */
     public val streams: Map<StreamId, Surface>
 
@@ -94,9 +89,7 @@
     public val requestNumber: RequestNumber
 }
 
-/**
- * [FrameInfo] is a wrapper around [TotalCaptureResult].
- */
+/** [FrameInfo] is a wrapper around [TotalCaptureResult]. */
 public interface FrameInfo : UnsafeWrapper {
     public val metadata: FrameMetadata
 
@@ -111,9 +104,7 @@
     public val requestMetadata: RequestMetadata
 }
 
-/**
- * [FrameMetadata] is a wrapper around [CaptureResult].
- */
+/** [FrameMetadata] is a wrapper around [CaptureResult]. */
 public interface FrameMetadata : Metadata, UnsafeWrapper {
     public operator fun <T> get(key: CaptureResult.Key<T>): T?
     public fun <T> getOrDefault(key: CaptureResult.Key<T>, default: T): T
@@ -137,16 +128,16 @@
  */
 public data class MetadataTransform(
     /**
-     * This defines the number of historical [TotalCaptureResult] objects this transform is
-     * allowed to look at. Setting this value to > 0 increases the number of [TotalCaptureResult]
-     * the [CameraGraph] will hold on to.
+     * This defines the number of historical [TotalCaptureResult] objects this transform is allowed
+     * to look at. Setting this value to > 0 increases the number of [TotalCaptureResult] the
+     * [CameraGraph] will hold on to.
      */
     val past: Int = 0,
 
     /**
      * This defines the number of future [TotalCaptureResult] objects this transform is allowed to
-     * look at. Setting this value to > 0 will cause [Request.Listener.onComplete] to be delayed
-     * by the number of frames specified here.
+     * look at. Setting this value to > 0 will cause [Request.Listener.onComplete] to be delayed by
+     * the number of frames specified here.
      */
     val future: Int = 0,
 
@@ -197,16 +188,14 @@
  * A [RequestNumber] is an artificial identifier that is created for each request that is submitted
  * to the Camera.
  */
-@JvmInline
-public value class RequestNumber(public val value: Long)
+@JvmInline public value class RequestNumber(public val value: Long)
 
 /**
  * A [FrameNumber] is the identifier that represents a specific exposure by the Camera. FrameNumbers
  * increase within a specific CameraCaptureSession, and are not created until the HAL begins
  * processing a request.
  */
-@JvmInline
-public value class FrameNumber(public val value: Long)
+@JvmInline public value class FrameNumber(public val value: Long)
 
 /**
  * This is a timestamp from the Camera, and corresponds to the nanosecond exposure time of a Frame.
@@ -219,25 +208,19 @@
  * operate based on a real-time clock, while audio/visual systems commonly operate based on a
  * monotonic clock.
  */
-@JvmInline
-public value class CameraTimestamp(public val value: Long)
+@JvmInline public value class CameraTimestamp(public val value: Long)
 
-/**
- * Utility function to help deal with the unsafe nature of the typed Key/Value pairs.
- */
+/** Utility function to help deal with the unsafe nature of the typed Key/Value pairs. */
 public fun CaptureRequest.Builder.writeParameters(parameters: Map<*, Any?>) {
     for ((key, value) in parameters) {
         writeParameter(key, value)
     }
 }
 
-/**
- * Utility function to help deal with the unsafe nature of the typed Key/Value pairs.
- */
+/** Utility function to help deal with the unsafe nature of the typed Key/Value pairs. */
 public fun CaptureRequest.Builder.writeParameter(key: Any?, value: Any?) {
     if (key != null && key is CaptureRequest.Key<*>) {
-        @Suppress("UNCHECKED_CAST")
-        this.set(key as CaptureRequest.Key<Any>, value)
+        @Suppress("UNCHECKED_CAST") this.set(key as CaptureRequest.Key<Any>, value)
     }
 }
 
@@ -246,6 +229,5 @@
  * cast is necessary since CameraGraph.Config uses Map<*, Any?> as the standard type for parameters.
  */
 fun MutableMap<Any, Any?>.putAllMetadata(metadata: Map<*, Any?>) {
-    @Suppress("UNCHECKED_CAST")
-    this.putAll(metadata as Map<Any, Any?>)
+    @Suppress("UNCHECKED_CAST") this.putAll(metadata as Map<Any, Any?>)
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Request.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Request.kt
index 037dbec..b79096f 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Request.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Request.kt
@@ -28,14 +28,14 @@
  * A [Request] is an immutable package of outputs and parameters needed to issue a [CaptureRequest]
  * to a Camera2 [CameraCaptureSession].
  *
- * [Request] objects are handled by camera2 via the [RequestProcessor] interface, and will
- * translate each [Request] object into a corresponding [CaptureRequest] object using the active
+ * [Request] objects are handled by camera2 via the [RequestProcessor] interface, and will translate
+ * each [Request] object into a corresponding [CaptureRequest] object using the active
  * [CameraDevice], [CameraCaptureSession], and [CameraGraph.Config]. Requests may be queued up and
  * submitted after a delay, or reused (in the case of repeating requests) if the
  * [CameraCaptureSession] is reconfigured or recreated.
  *
- * Depending on the [CameraGraph.Config], it is possible that not all parameters that are set on
- * the [Request] will be honored when a [Request] is sent to the camera. Specifically, Camera2
+ * Depending on the [CameraGraph.Config], it is possible that not all parameters that are set on the
+ * [Request] will be honored when a [Request] is sent to the camera. Specifically, Camera2
  * parameters related to 3A State and any required parameters specified on the [CameraGraph.Config]
  * will override parameters specified in a [Request]
  *
@@ -63,62 +63,56 @@
          * exposure time depending on the device, and may be in a different timebase from the
          * timestamps that are returned from the underlying buffers.
          *
-         * @see android.hardware.camera2.CameraCaptureSession.CaptureCallback.onCaptureStarted
-         *
          * @param requestMetadata the data about the camera2 request that was sent to the camera.
          * @param frameNumber the android frame number for this exposure
          * @param timestamp the android timestamp in nanos for this exposure
+         * @see android.hardware.camera2.CameraCaptureSession.CaptureCallback.onCaptureStarted
          */
         public fun onStarted(
             requestMetadata: RequestMetadata,
             frameNumber: FrameNumber,
             timestamp: CameraTimestamp
-        ) {
-        }
+        ) {}
 
         /**
          * This event indicates that the camera sensor has additional information about the frame
          * associated with this Request. This method may be invoked 0 or more times before the frame
          * receives onComplete.
          *
-         * @see android.hardware.camera2.CameraCaptureSession.CaptureCallback.onCaptureStarted
-         *
          * @param requestMetadata the data about the camera2 request that was sent to the camera.
          * @param frameNumber the android frame number for this exposure
          * @param captureResult the current android capture result for this exposure
+         * @see android.hardware.camera2.CameraCaptureSession.CaptureCallback.onCaptureStarted
          */
         public fun onPartialCaptureResult(
             requestMetadata: RequestMetadata,
             frameNumber: FrameNumber,
             captureResult: FrameMetadata
-        ) {
-        }
+        ) {}
 
         /**
          * This event indicates that all of the metadata associated with this frame has been
-         * produced. If [onPartialCaptureResult] was invoked, the values returned in
-         * the totalCaptureResult map be a superset of the values produced from the
+         * produced. If [onPartialCaptureResult] was invoked, the values returned in the
+         * totalCaptureResult map be a superset of the values produced from the
          * [onPartialCaptureResult] calls.
          *
-         * @see android.hardware.camera2.CameraCaptureSession.CaptureCallback.onCaptureStarted
-         *
          * @param requestMetadata the data about the camera2 request that was sent to the camera.
          * @param frameNumber the android frame number for this exposure
          * @param totalCaptureResult the final android capture result for this exposure
+         * @see android.hardware.camera2.CameraCaptureSession.CaptureCallback.onCaptureStarted
          */
         public fun onTotalCaptureResult(
             requestMetadata: RequestMetadata,
             frameNumber: FrameNumber,
             totalCaptureResult: FrameInfo
-        ) {
-        }
+        ) {}
 
         /**
-         * This is an artificial event that will be invoked after onTotalCaptureResult. This may
-         * be invoked several frames after onTotalCaptureResult due to incorrect HAL implementations
-         * that return metadata that get shifted several frames in the future. See b/154568653
-         * for real examples of this. The actual amount of shifting and required transformations
-         * may vary per device.
+         * This is an artificial event that will be invoked after onTotalCaptureResult. This may be
+         * invoked several frames after onTotalCaptureResult due to incorrect HAL implementations
+         * that return metadata that get shifted several frames in the future. See b/154568653 for
+         * real examples of this. The actual amount of shifting and required transformations may
+         * vary per device.
          *
          * @param requestMetadata the data about the camera2 request that was sent to the camera.
          * @param frameNumber the android frame number for this exposure
@@ -128,8 +122,7 @@
             requestMetadata: RequestMetadata,
             frameNumber: FrameNumber,
             result: FrameInfo
-        ) {
-        }
+        ) {}
 
         /**
          * onFailed occurs when a CaptureRequest failed in some way and the frame will not receive
@@ -137,57 +130,51 @@
          *
          * Surfaces may not received images if "wasImagesCaptured" is set to false.
          *
-         * @see android.hardware.camera2.CameraCaptureSession.CaptureCallback.onCaptureFailed
-         *
          * @param requestMetadata the data about the camera2 request that was sent to the camera.
          * @param frameNumber the android frame number for this exposure
          * @param captureFailure the android [CaptureFailure] data
+         * @see android.hardware.camera2.CameraCaptureSession.CaptureCallback.onCaptureFailed
          */
         public fun onFailed(
             requestMetadata: RequestMetadata,
             frameNumber: FrameNumber,
             captureFailure: CaptureFailure
-        ) {
-        }
+        ) {}
 
         /**
          * onBufferLost occurs when a CaptureRequest failed to create an image for a given output
          * stream. This method may be invoked multiple times per frame if multiple buffers were
          * lost. This method may not be invoked when an image is lost in some situations.
          *
-         * @see android.hardware.camera2.CameraCaptureSession.CaptureCallback.onCaptureBufferLost
-         *
          * @param requestMetadata the data about the camera2 request that was sent to the camera.
          * @param frameNumber the android frame number for this exposure
          * @param stream the internal stream that will not receive a buffer for this frame.
+         * @see android.hardware.camera2.CameraCaptureSession.CaptureCallback.onCaptureBufferLost
          */
         public fun onBufferLost(
             requestMetadata: RequestMetadata,
             frameNumber: FrameNumber,
             stream: StreamId
-        ) {
-        }
+        ) {}
 
         /**
          * This is an artificial callback that will be invoked if a specific request was pending or
          * had already been submitted to when an abort was requested. The behavior of the request is
-         * undefined if this method is invoked and images or metadata may or may not be produced
-         * for this request. Repeating requests will not receive onAborted.
+         * undefined if this method is invoked and images or metadata may or may not be produced for
+         * this request. Repeating requests will not receive onAborted.
          *
          * @param request information about this specific request.
          */
-        public fun onAborted(request: Request) {
-        }
+        public fun onAborted(request: Request) {}
 
         /**
          * Invoked after the CaptureRequest(s) have been created, but before the request is
-         * submitted to the Camera. This method may be invoked multiple times if the request
-         * fails to submit or if this is a repeating request.
+         * submitted to the Camera. This method may be invoked multiple times if the request fails
+         * to submit or if this is a repeating request.
          *
          * @param requestMetadata information about this specific request.
          */
-        public fun onRequestSequenceCreated(requestMetadata: RequestMetadata) {
-        }
+        public fun onRequestSequenceCreated(requestMetadata: RequestMetadata) {}
 
         /**
          * Invoked after the CaptureRequest(s) has been submitted. This method may be invoked
@@ -195,43 +182,39 @@
          *
          * @param requestMetadata the data about the camera2 request that was sent to the camera.
          */
-        public fun onRequestSequenceSubmitted(requestMetadata: RequestMetadata) {
-        }
+        public fun onRequestSequenceSubmitted(requestMetadata: RequestMetadata) {}
 
         /**
-         * Invoked by Camera2 if the request was aborted after having been submitted. This method
-         * is distinct from onAborted, which is directly invoked when aborting captures.
-         *
-         * @see android.hardware.camera2.CameraCaptureSession.CaptureCallback.onCaptureSequenceAborted
+         * Invoked by Camera2 if the request was aborted after having been submitted. This method is
+         * distinct from onAborted, which is directly invoked when aborting captures.
          *
          * @param requestMetadata the data about the camera2 request that was sent to the camera.
+         * @see
+         *   android.hardware.camera2.CameraCaptureSession.CaptureCallback.onCaptureSequenceAborted
          */
-        public fun onRequestSequenceAborted(requestMetadata: RequestMetadata) {
-        }
+        public fun onRequestSequenceAborted(requestMetadata: RequestMetadata) {}
 
         /**
          * Invoked by Camera2 if the request was completed after having been submitted. This method
          * is distinct from onCompleted which is invoked for each frame when used with a repeating
          * request.
          *
-         * @see android.hardware.camera2.CameraCaptureSession.CaptureCallback.onCaptureSequenceCompleted
-         *
          * @param requestMetadata the data about the camera2 request that was sent to the camera.
          * @param frameNumber the final frame number of this sequence.
+         * @see
+         *   android.hardware.camera2.CameraCaptureSession.CaptureCallback.onCaptureSequenceCompleted
          */
         public fun onRequestSequenceCompleted(
             requestMetadata: RequestMetadata,
             frameNumber: FrameNumber
-        ) {
-        }
+        ) {}
     }
 
     public operator fun <T> get(key: CaptureRequest.Key<T>): T? = getUnchecked(key)
     public operator fun <T> get(key: Metadata.Key<T>): T? = getUnchecked(key)
 
     @Suppress("UNCHECKED_CAST")
-    private fun <T> Request.getUnchecked(key: Metadata.Key<T>): T? =
-        this.extras[key] as T?
+    private fun <T> Request.getUnchecked(key: Metadata.Key<T>): T? = this.extras[key] as T?
 
     @Suppress("UNCHECKED_CAST")
     private fun <T> Request.getUnchecked(key: CaptureRequest.Key<T>): T? =
@@ -239,6 +222,7 @@
 }
 
 public fun <T> Request.getOrDefault(key: Metadata.Key<T>, default: T): T = this[key] ?: default
+
 public fun <T> Request.getOrDefault(key: CaptureRequest.Key<T>, default: T): T =
     this[key] ?: default
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/RequestProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/RequestProcessor.kt
index 97475b8..6b26be9 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/RequestProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/RequestProcessor.kt
@@ -42,7 +42,7 @@
      * Set the repeating [Request] with an optional set of parameters and listeners. Parameters are
      * applied, in order, to each request in the list:
      *
-     *   [Request.template] -> defaultParameters -> [Request.parameters] -> requiredParameters
+     * [Request.template] -> defaultParameters -> [Request.parameters] -> requiredParameters
      *
      * Parameters where values are set to null will physically remove a particular key from the
      * final map of parameters.
@@ -51,7 +51,7 @@
      * @param defaultParameters will not override parameters specified in the [Request].
      * @param requiredParameters will override parameters specified in the [Request].
      * @param defaultListeners are internal and/or global listeners that should be invoked in
-     * addition to listeners that are specified on each [Request]
+     *   addition to listeners that are specified on each [Request]
      * @return false if the repeating request was not successfully updated.
      */
     fun startRepeating(
@@ -62,8 +62,8 @@
     ): Boolean
 
     /**
-     * Stops the current repeating request, but does *not* close the session. The current
-     * repeating request can be resumed by invoking [startRepeating] again.
+     * Stops the current repeating request, but does *not* close the session. The current repeating
+     * request can be resumed by invoking [startRepeating] again.
      */
     fun stopRepeating()
 
@@ -71,7 +71,7 @@
      * Submit a single [Request] with optional sets of parameters and listeners. Parameters are
      * applied, in order, to each request in the list:
      *
-     *   [Request.template] -> defaultParameters -> [Request.parameters] -> requiredParameters
+     * [Request.template] -> defaultParameters -> [Request.parameters] -> requiredParameters
      *
      * Parameters where values are set to null will physically remove a particular key from the
      * final map of parameters.
@@ -80,7 +80,7 @@
      * @param defaultParameters will not override parameters specified in the [Request].
      * @param requiredParameters will override parameters specified in the [Request].
      * @param defaultListeners are internal and/or global listeners that should be invoked in
-     * addition to listeners that are specified on each [Request]
+     *   addition to listeners that are specified on each [Request]
      * @return false if this request was not submitted to the camera for any reason.
      */
     fun submit(
@@ -94,7 +94,7 @@
      * Submit a list of [Request]s with optional sets of parameters and listeners. Parameters are
      * applied, in order, to each request in the list:
      *
-     *   [Request.template] -> defaultParameters -> [Request.parameters] -> requiredParameters
+     * [Request.template] -> defaultParameters -> [Request.parameters] -> requiredParameters
      *
      * Parameters where values are set to null will physically remove a particular key from the
      * final map of parameters.
@@ -103,7 +103,7 @@
      * @param defaultParameters will not override parameters specified in the [Request].
      * @param requiredParameters will override parameters specified in the [Request].
      * @param defaultListeners are internal and/or global listeners that should be invoked in
-     * addition to listeners that are specified on each [Request]
+     *   addition to listeners that are specified on each [Request]
      * @return false if these requests were not submitted to the camera for any reason.
      */
     fun submit(
@@ -113,14 +113,12 @@
         defaultListeners: List<Request.Listener>
     ): Boolean
 
-    /**
-     * Abort requests that have been submitted but not completed.
-     */
+    /** Abort requests that have been submitted but not completed. */
     fun abortCaptures()
 
     /**
-     * Puts the RequestProcessor into a closed state where it should immediately reject all
-     * incoming requests. This should NOT call stopRepeating() or abortCaptures().
+     * Puts the RequestProcessor into a closed state where it should immediately reject all incoming
+     * requests. This should NOT call stopRepeating() or abortCaptures().
      */
     fun close()
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamFormat.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamFormat.kt
index ea9dc79..960357a 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamFormat.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamFormat.kt
@@ -21,9 +21,9 @@
 /**
  * Platform-independent Android ImageFormats and their associated values.
  *
- * Using this inline class prevents missing values on platforms where the format is not present
- * or not listed.
- * // TODO: Consider adding data-space as a separate property, or finding a way to work it in.
+ * Using this inline class prevents missing values on platforms where the format is not present or
+ * not listed. // TODO: Consider adding data-space as a separate property, or finding a way to work
+ * it in.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 @JvmInline
@@ -64,8 +64,8 @@
     /**
      * This function returns the number of bits per pixel for a given stream format.
      *
-     * @return the number of bits per pixel or -1 if the format does not have a well defined
-     * number of bits per pixel.
+     * @return the number of bits per pixel or -1 if the format does not have a well defined number
+     *   of bits per pixel.
      */
     public val bitsPerPixel: Int
         get() {
@@ -103,7 +103,6 @@
             when (this) {
                 UNKNOWN -> return "UNKNOWN"
                 PRIVATE -> return "PRIVATE"
-
                 DEPTH16 -> return "DEPTH16"
                 DEPTH_JPEG -> return "DEPTH_JPEG"
                 DEPTH_POINT_CLOUD -> return "DEPTH_POINT_CLOUD"
@@ -130,4 +129,4 @@
             }
             return "UNKNOWN-${this.value.toString(16)}"
         }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamGraph.kt
index e94d2c4..0527026 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamGraph.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamGraph.kt
@@ -31,4 +31,4 @@
     public val outputs: List<OutputStream>
 
     public operator fun get(config: CameraStream.Config): CameraStream?
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
index 69f3df0..a36c50c 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
@@ -25,7 +25,6 @@
 
 /**
  * A [CameraStream] is used on a [CameraGraph] to control what outputs that graph produces.
- *
  * - Each [CameraStream] must have a surface associated with it in the [CameraGraph]. This surface
  *   may be changed, although this may cause the camera to stall and reconfigure.
  * - [CameraStream]'s may be added to [Request]'s that are sent to the [CameraGraph]. This causes
@@ -36,18 +35,20 @@
  * [CameraStream] may only represent a single surface that is sent to Camera2, and that each
  * [CameraStream] must produce one or more distinct outputs.
  *
- * There are three main components that will be wired together, the [CameraStream], the
- * Camera2 [OutputConfiguration], and the [OutputStream]'s. In each of these examples a
- * [CameraStream] is associated with a distinct surface that may be sent to camera2 to produce 1
- * or more distinct outputs defined in the list of [OutputStream]'s.
+ * There are three main components that will be wired together, the [CameraStream], the Camera2
+ * [OutputConfiguration], and the [OutputStream]'s. In each of these examples a [CameraStream] is
+ * associated with a distinct surface that may be sent to camera2 to produce 1 or more distinct
+ * outputs defined in the list of [OutputStream]'s.
  *
  * Simple 1:1 configuration
+ *
  *   ```
  *   CameraStream-1 -> OutputConfig-1 -> OutputStream-1
  *   CameraStream-2 -> OutputConfig-2 -> OutputStream-2
  *   ```
  *
  * Stream sharing (Multiple surfaces use the same OutputConfig object)
+ *
  *   ```
  *   CameraStream-1 --------------------> OutputStream-1
  *                  >- OutputConfig-1 -<
@@ -55,21 +56,18 @@
  *   ```
  *
  * Multi-Output / External OutputConfiguration (Camera2 may produce one or more of the outputs)
+ *
  *   ```
  *   CameraStream-1 -> OutputConfig-1 -> OutputStream-1
  *                 \-> OutputConfig-2 -> OutputStream-2
  *   ```
  */
-public class CameraStream internal constructor(
-    public val id: StreamId,
-    public val outputs: List<OutputStream>
-) {
+public class CameraStream
+internal constructor(public val id: StreamId, public val outputs: List<OutputStream>) {
     override fun toString(): String = id.toString()
 
     /** Configuration that may be used to define a [CameraStream] on a [CameraGraph] */
-    public class Config internal constructor(
-        val outputs: List<OutputStream.Config>
-    ) {
+    public class Config internal constructor(val outputs: List<OutputStream.Config>) {
         companion object {
             /** Create a simple [CameraStream] to [OutputStream] configuration */
             fun create(
@@ -81,22 +79,22 @@
                 timestampBase: OutputStream.TimestampBase? = null,
                 dynamicRangeProfile: OutputStream.DynamicRangeProfile? = null,
                 streamUseCase: OutputStream.StreamUseCase? = null,
-            ): Config = create(
-                OutputStream.Config.create(
-                    size,
-                    format,
-                    camera,
-                    outputType,
-                    mirrorMode,
-                    timestampBase,
-                    dynamicRangeProfile,
-                    streamUseCase,
-                )
-            )
+            ): Config =
+                create(
+                    OutputStream.Config.create(
+                        size,
+                        format,
+                        camera,
+                        outputType,
+                        mirrorMode,
+                        timestampBase,
+                        dynamicRangeProfile,
+                        streamUseCase,
+                    ))
 
             /**
-             * Create a simple [CameraStream] using a previously defined [OutputStream.Config].
-             * This allows multiple [CameraStream]s to share the same [OutputConfiguration].
+             * Create a simple [CameraStream] using a previously defined [OutputStream.Config]. This
+             * allows multiple [CameraStream]s to share the same [OutputConfiguration].
              */
             fun create(output: OutputStream.Config) = Config(listOf(output))
 
@@ -121,8 +119,8 @@
 /**
  * A [OutputStream] represents one of the possible outputs that may be produced from a
  * [CameraStream]. Because some sensors are capable of producing images at different resolutions,
- * the underlying HAL on the device may produce different sized images for the same request.
- * This represents one of those potential outputs.
+ * the underlying HAL on the device may produce different sized images for the same request. This
+ * represents one of those potential outputs.
  */
 public interface OutputStream {
     // Every output comes from one, and exactly one, CameraStream
@@ -139,7 +137,8 @@
     // TODO: Consider adding sensor mode and/or other metadata
 
     /**
-     * Configuration object that provides the parameters for a specific input / output stream on Camera.
+     * Configuration object that provides the parameters for a specific input / output stream on
+     * Camera.
      */
     sealed class Config(
         public val size: Size,
@@ -161,10 +160,8 @@
                 dynamicRangeProfile: DynamicRangeProfile? = null,
                 streamUseCase: StreamUseCase? = null,
             ): Config =
-                if (
-                    outputType == OutputType.SURFACE_TEXTURE ||
-                    outputType == OutputType.SURFACE_VIEW
-                ) {
+                if (outputType == OutputType.SURFACE_TEXTURE ||
+                    outputType == OutputType.SURFACE_VIEW) {
                     LazyOutputConfig(
                         size,
                         format,
@@ -173,8 +170,7 @@
                         mirrorMode,
                         timestampBase,
                         dynamicRangeProfile,
-                        streamUseCase
-                    )
+                        streamUseCase)
                 } else {
                     check(outputType == OutputType.SURFACE)
                     SimpleOutputConfig(
@@ -200,9 +196,7 @@
             }
         }
 
-        /**
-         * Most outputs only need to define size, format, and cameraId.
-         */
+        /** Most outputs only need to define size, format, and cameraId. */
         internal class SimpleOutputConfig(
             size: Size,
             format: StreamFormat,
@@ -211,18 +205,19 @@
             timestampBase: TimestampBase?,
             dynamicRangeProfile: DynamicRangeProfile?,
             streamUseCase: StreamUseCase?,
-        ) : Config(
+        ) :
+            Config(
                 size,
                 format,
                 camera,
                 mirrorMode,
                 timestampBase,
                 dynamicRangeProfile,
-                streamUseCase
-            )
+                streamUseCase)
 
         /**
-         * Used to configure an output with a surface that may be provided after the camera is running.
+         * Used to configure an output with a surface that may be provided after the camera is
+         * running.
          *
          * This behavior is allowed on newer versions of the OS and allows the camera to start
          * running before the UI is fully available. This configuration mode is only allowed for
@@ -238,7 +233,8 @@
             timestampBase: TimestampBase?,
             dynamicRangeProfile: DynamicRangeProfile?,
             streamUseCase: StreamUseCase?,
-        ) : Config(
+        ) :
+            Config(
                 size,
                 format,
                 camera,
@@ -249,12 +245,14 @@
             )
 
         /**
-         * Used to define an output that comes from an externally managed OutputConfiguration object.
+         * Used to define an output that comes from an externally managed OutputConfiguration
+         * object.
          *
          * The configuration logic has the following behavior:
          * - Assumes [OutputConfiguration] has a valid surface
          * - Assumes [OutputConfiguration] surfaces will not be added / removed / changed.
-         * - If the CameraCaptureSession must be recreated, the [OutputConfiguration] will be reused.
+         * - If the CameraCaptureSession must be recreated, the [OutputConfiguration] will be
+         *   reused.
          */
         @RequiresApi(33)
         internal class ExternalOutputConfig(
@@ -262,7 +260,8 @@
             format: StreamFormat,
             camera: CameraId?,
             val output: OutputConfiguration,
-        ) : Config(
+        ) :
+            Config(
                 size,
                 format,
                 camera,
@@ -280,10 +279,10 @@
     }
 
     /**
-     * Adds the ability to define the mirrorMode of the OutputStream.
-     * [MIRROR_MODE_AUTO] is the default mirroring mode for the camera device.
-     * With this mode, the camera output is mirrored horizontally for front-facing cameras,
-     * and there is no mirroring for rear-facing and external cameras.
+     * Adds the ability to define the mirrorMode of the OutputStream. [MIRROR_MODE_AUTO] is the
+     * default mirroring mode for the camera device. With this mode, the camera output is mirrored
+     * horizontally for front-facing cameras, and there is no mirroring for rear-facing and external
+     * cameras.
      *
      * See the documentation on [OutputConfiguration.setMirrorMode] for more details.
      */
@@ -298,9 +297,9 @@
     }
 
     /**
-     * Adds the ability to define the timestamp base of the OutputStream.
-     * [TIMESTAMP_BASE_DEFAULT] is the default timestamp base, with which the
-     * camera device adjusts timestamps based on the output target.
+     * Adds the ability to define the timestamp base of the OutputStream. [TIMESTAMP_BASE_DEFAULT]
+     * is the default timestamp base, with which the camera device adjusts timestamps based on the
+     * output target.
      *
      * See the documentation on [OutputConfiguration.setTimestampBase] for more details.
      */
@@ -316,9 +315,9 @@
     }
 
     /**
-     * Adds the ability to define the dynamic range profile of the OutputStream.
-     * [STANDARD] is the default dynamic range profile for the camera device, with which
-     * the camera device uses an 8-bit standard profile.
+     * Adds the ability to define the dynamic range profile of the OutputStream. [STANDARD] is the
+     * default dynamic range profile for the camera device, with which the camera device uses an
+     * 8-bit standard profile.
      *
      * See the documentation on [OutputConfiguration.setDynamicRangeProfile] for more details.
      */
@@ -342,10 +341,10 @@
     }
 
     /**
-     * Adds the ability to define the stream specific use case of the OutputStream.
-     * [DEFAULT] is the default stream use case, with which
-     * the camera device uses the properties of the output target, such as format, dataSpace,
-     * or surface class type, to optimize the image processing pipeline.
+     * Adds the ability to define the stream specific use case of the OutputStream. [DEFAULT] is the
+     * default stream use case, with which the camera device uses the properties of the output
+     * target, such as format, dataSpace, or surface class type, to optimize the image processing
+     * pipeline.
      *
      * See the documentation on [OutputConfiguration.setStreamUseCase] for more details.
      */
@@ -362,18 +361,13 @@
     }
 }
 
-/**
- * This identifies a single output.
- */
+/** This identifies a single output. */
 @JvmInline
 public value class OutputId(public val value: Int) {
     override fun toString(): String = "Output-$value"
 }
 
-/**
- * Configuration for defining the properties of a Camera2 InputStream for reprocessing
- * requests.
- */
+/** Configuration for defining the properties of a Camera2 InputStream for reprocessing requests. */
 public interface InputStream {
     public val id: InputId
     public val format: StreamFormat
@@ -382,9 +376,7 @@
     public class Config(val stream: CameraStream.Config)
 }
 
-/**
- * This identifies a single input.
- */
+/** This identifies a single input. */
 @JvmInline
 public value class InputId(public val value: Int) {
     override fun toString(): String = "Input-$value"
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
index 8d8bec7..40b5167 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
@@ -35,6 +35,7 @@
 import androidx.annotation.DoNotInline
 import androidx.annotation.RequiresApi
 import androidx.annotation.RequiresPermission
+import androidx.camera.camera2.pipe.CameraMetadata
 import java.util.concurrent.Executor
 
 @RequiresApi(Build.VERSION_CODES.M)
@@ -63,11 +64,7 @@
         stateCallback: CameraCaptureSession.StateCallback,
         handler: Handler?
     ) {
-        cameraDevice.createConstrainedHighSpeedCaptureSession(
-            outputs,
-            stateCallback,
-            handler
-        )
+        cameraDevice.createConstrainedHighSpeedCaptureSession(outputs, stateCallback, handler)
     }
 
     @JvmStatic
@@ -118,10 +115,7 @@
         handler: Handler?
     ) {
         cameraDevice.createCaptureSessionByOutputConfigurations(
-            outputConfig,
-            stateCallback,
-            handler
-        )
+            outputConfig, stateCallback, handler)
     }
 
     @JvmStatic
@@ -136,11 +130,7 @@
         handler: Handler?
     ) {
         cameraDevice.createReprocessableCaptureSessionByConfigurations(
-            inputConfig,
-            outputs,
-            stateCallback,
-            handler
-        )
+            inputConfig, outputs, stateCallback, handler)
     }
 
     @JvmStatic
@@ -218,9 +208,7 @@
 
     @JvmStatic
     @DoNotInline
-    fun getPhysicalCameraIds(
-        cameraCharacteristics: CameraCharacteristics
-    ): Set<String> {
+    fun getPhysicalCameraIds(cameraCharacteristics: CameraCharacteristics): Set<String> {
         return cameraCharacteristics.physicalCameraIds
     }
 
@@ -254,10 +242,7 @@
 
     @JvmStatic
     @DoNotInline
-    fun setSessionParameters(
-        sessionConfig: SessionConfiguration,
-        params: CaptureRequest
-    ) {
+    fun setSessionParameters(sessionConfig: SessionConfiguration, params: CaptureRequest) {
         sessionConfig.sessionParameters = params
     }
 
@@ -338,6 +323,12 @@
 
     @JvmStatic
     @DoNotInline
+    fun getAvailableStreamUseCases(cameraMetadata: CameraMetadata): LongArray? {
+        return cameraMetadata[CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES]
+    }
+
+    @JvmStatic
+    @DoNotInline
     fun getStreamUseCase(outputConfig: OutputConfiguration): Long {
         return outputConfig.streamUseCase
     }
@@ -353,4 +344,4 @@
     fun getTimestampBase(outputConfig: OutputConfiguration): Int {
         return outputConfig.timestampBase
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Backend.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Backend.kt
index f2a53d3..5ac418d 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Backend.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Backend.kt
@@ -16,8 +16,6 @@
 
 package androidx.camera.camera2.pipe.compat
 
-import android.hardware.camera2.CameraAccessException
-import android.hardware.camera2.CameraManager
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraBackend
 import androidx.camera.camera2.pipe.CameraBackendId
@@ -29,20 +27,20 @@
 import androidx.camera.camera2.pipe.StreamGraph
 import androidx.camera.camera2.pipe.config.Camera2ControllerComponent
 import androidx.camera.camera2.pipe.config.Camera2ControllerConfig
-import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.core.Threads
 import androidx.camera.camera2.pipe.graph.GraphListener
 import androidx.camera.camera2.pipe.graph.StreamGraphImpl
 import javax.inject.Inject
-import javax.inject.Provider
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Deferred
 
-/**
- * This is the default [CameraBackend] implementation for CameraPipe based on Camera2.
- */
+/** This is the default [CameraBackend] implementation for CameraPipe based on Camera2. */
 @RequiresApi(21)
-internal class Camera2Backend @Inject constructor(
-    private val cameraManager: Provider<CameraManager>,
+internal class Camera2Backend
+@Inject
+constructor(
+    private val threads: Threads,
+    private val camera2DeviceCache: Camera2DeviceCache,
     private val camera2MetadataCache: Camera2MetadataCache,
     private val virtualCameraManager: VirtualCameraManager,
     private val camera2CameraControllerComponent: Camera2ControllerComponent.Builder,
@@ -50,25 +48,15 @@
     override val id: CameraBackendId
         get() = CameraBackendId("CXCP-Camera2")
 
-    override fun readCameraIdList(): List<CameraId> {
-        val cameraManager = cameraManager.get()
-        val cameraIdArray = try {
-            // WARNING: This method can, at times, return an empty list of cameras on devices that
-            //  will normally return a valid list of cameras (b/159052778)
-            cameraManager.cameraIdList
-        } catch (e: CameraAccessException) {
-            Log.warn(e) { "Failed to query CameraManager#getCameraIdList!" }
-            null
-        }
-        if (cameraIdArray?.isEmpty() == true) {
-            Log.warn { "Failed to query CameraManager#getCameraIdList: No values returned." }
-        }
+    override suspend fun getCameraIds(): List<CameraId>? = camera2DeviceCache.getCameraIds()
 
-        return cameraIdArray?.map { CameraId(it) } ?: listOf()
-    }
+    override fun awaitCameraIds(): List<CameraId>? = camera2DeviceCache.awaitCameraIds()
 
-    override fun readCameraMetadata(cameraId: CameraId): CameraMetadata =
-        camera2MetadataCache.readCameraMetadata(cameraId)
+    override suspend fun getCameraMetadata(cameraId: CameraId): CameraMetadata? =
+        camera2MetadataCache.getCameraMetadata(cameraId)
+
+    override fun awaitCameraMetadata(cameraId: CameraId): CameraMetadata? =
+        camera2MetadataCache.awaitCameraMetadata(cameraId)
 
     override fun disconnectAllAsync(): Deferred<Unit> {
         // TODO: VirtualCameraManager needs to be extended to support a suspendable future that can
@@ -91,16 +79,14 @@
         streamGraph: StreamGraph
     ): CameraController {
         // Use Dagger to create the camera2 controller component, then create the CameraController.
-        val cameraControllerComponent = camera2CameraControllerComponent.camera2ControllerConfig(
-            Camera2ControllerConfig(
-                this,
-                graphConfig,
-                graphListener,
-                streamGraph as StreamGraphImpl
-            )
-        ).build()
+        val cameraControllerComponent =
+            camera2CameraControllerComponent
+                .camera2ControllerConfig(
+                    Camera2ControllerConfig(
+                        this, graphConfig, graphListener, streamGraph as StreamGraphImpl))
+                .build()
 
         // Create and return a Camera2 CameraController object.
         return cameraControllerComponent.cameraController()
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
index e32f04b..d35dffa 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
@@ -40,7 +40,9 @@
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 @Camera2ControllerScope
-internal class Camera2CameraController @Inject constructor(
+internal class Camera2CameraController
+@Inject
+constructor(
     private val scope: CoroutineScope,
     private val config: CameraGraph.Config,
     private val graphListener: GraphListener,
@@ -56,11 +58,9 @@
     private var currentSurfaceMap: Map<StreamId, Surface>? = null
 
     override fun start() {
-        val camera = virtualCameraManager.open(
-            config.camera,
-            config.flags.allowMultipleActiveCameras,
-            graphListener
-        )
+        val camera =
+            virtualCameraManager.open(
+                config.camera, config.flags.allowMultipleActiveCameras, graphListener)
         synchronized(this) {
             if (closed) {
                 return
@@ -70,14 +70,14 @@
             check(currentSession == null)
 
             currentCamera = camera
-            val session = CaptureSessionState(
-                graphListener,
-                captureSessionFactory,
-                captureSequenceProcessorFactory,
-                cameraSurfaceManager,
-                timeSource,
-                scope
-            )
+            val session =
+                CaptureSessionState(
+                    graphListener,
+                    captureSessionFactory,
+                    captureSequenceProcessorFactory,
+                    cameraSurfaceManager,
+                    timeSource,
+                    scope)
             currentSession = session
 
             val surfaces: Map<StreamId, Surface>? = currentSurfaceMap
@@ -133,12 +133,13 @@
     override fun updateSurfaceMap(surfaceMap: Map<StreamId, Surface>) {
         // TODO: Add logic to decide if / when to re-configure the Camera2 CaptureSession.
         synchronized(this) {
-            if (closed) {
-                return
+                if (closed) {
+                    return
+                }
+                currentSurfaceMap = surfaceMap
+                currentSession
             }
-            currentSurfaceMap = surfaceMap
-            currentSession
-        }?.configureSurfaceMap(surfaceMap)
+            ?.configureSurfaceMap(surfaceMap)
     }
 
     private suspend fun bindSessionToCamera() {
@@ -160,4 +161,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraDevices.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraDevices.kt
deleted file mode 100644
index 1a8b860..0000000
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraDevices.kt
+++ /dev/null
@@ -1,49 +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.camera.camera2.pipe.compat
-
-import androidx.annotation.RequiresApi
-import androidx.camera.camera2.pipe.CameraDevices
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CameraMetadata
-import javax.inject.Inject
-import javax.inject.Singleton
-import kotlinx.coroutines.runBlocking
-
-/**
- * Provides utilities for querying cameras and accessing metadata about those cameras.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@Singleton
-internal class Camera2CameraDevices @Inject constructor(
-    private val deviceCache: Camera2DeviceCache,
-    private val metadataCache: Camera2MetadataCache
-) : CameraDevices {
-    @Deprecated(
-        message = "findAll may block the calling thread and is deprecated.",
-        replaceWith = ReplaceWith("ids"),
-        level = DeprecationLevel.WARNING
-    )
-    override fun findAll(): List<CameraId> = runBlocking { deviceCache.getCameras() }
-    override suspend fun ids(): List<CameraId> = deviceCache.getCameras()
-
-    override suspend fun getMetadata(camera: CameraId): CameraMetadata =
-        metadataCache.getMetadata(camera)
-
-    override fun awaitMetadata(camera: CameraId): CameraMetadata =
-        metadataCache.awaitMetadata(camera)
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt
index 652299c..426cc51 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt
@@ -31,23 +31,22 @@
 
 /**
  * This implementation provides access to [CameraCharacteristics] and lazy caching of properties
- * that are either expensive to create and access, or that only exist on newer versions of the
- * OS. This allows all fields to be accessed and return reasonable values on all OS versions.
+ * that are either expensive to create and access, or that only exist on newer versions of the OS.
+ * This allows all fields to be accessed and return reasonable values on all OS versions.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-internal class Camera2CameraMetadata constructor(
+internal class Camera2CameraMetadata
+constructor(
     override val camera: CameraId,
     override val isRedacted: Boolean,
     private val characteristics: CameraCharacteristics,
-    private val metadataProvider: CameraMetadataProvider,
+    private val metadataProvider: Camera2MetadataProvider,
     private val metadata: Map<Metadata.Key<*>, Any?>,
     private val cacheBlocklist: Set<CameraCharacteristics.Key<*>>,
 ) : CameraMetadata {
-    @GuardedBy("values")
-    private val values = ArrayMap<CameraCharacteristics.Key<*>, Any?>()
+    @GuardedBy("values") private val values = ArrayMap<CameraCharacteristics.Key<*>, Any?>()
 
-    @Suppress("UNCHECKED_CAST")
-    override fun <T> get(key: Metadata.Key<T>): T? = metadata[key] as T?
+    @Suppress("UNCHECKED_CAST") override fun <T> get(key: Metadata.Key<T>): T? = metadata[key] as T?
 
     @Suppress("UNCHECKED_CAST")
     override fun <T> getOrDefault(key: Metadata.Key<T>, default: T): T =
@@ -72,14 +71,11 @@
         // 3. Duplicate non-null values are expected to be identical (even if the object instance
         //    is different), and so it does not matter which value is cached if two calls from
         //    different threads try to read the value simultaneously.
-        @Suppress("UNCHECKED_CAST")
-        var result = synchronized(values) { values[key] } as T?
+        @Suppress("UNCHECKED_CAST") var result = synchronized(values) { values[key] } as T?
         if (result == null) {
             result = characteristics.get(key)
             if (result != null) {
-                synchronized(values) {
-                    values[key] = result
-                }
+                synchronized(values) { values[key] = result }
             }
         }
         return result
@@ -89,16 +85,22 @@
         get(key) ?: default
 
     @Suppress("UNCHECKED_CAST")
-    override fun <T : Any> unwrapAs(type: KClass<T>): T? = when (type) {
-        CameraCharacteristics::class -> characteristics as T
-        else -> null
-    }
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? =
+        when (type) {
+            CameraCharacteristics::class -> characteristics as T
+            else -> null
+        }
 
-    override val keys: Set<CameraCharacteristics.Key<*>> get() = _keys.value
-    override val requestKeys: Set<CaptureRequest.Key<*>> get() = _requestKeys.value
-    override val resultKeys: Set<CaptureResult.Key<*>> get() = _resultKeys.value
-    override val sessionKeys: Set<CaptureRequest.Key<*>> get() = _sessionKeys.value
-    override val physicalCameraIds: Set<CameraId> get() = _physicalCameraIds.value
+    override val keys: Set<CameraCharacteristics.Key<*>>
+        get() = _keys.value
+    override val requestKeys: Set<CaptureRequest.Key<*>>
+        get() = _requestKeys.value
+    override val resultKeys: Set<CaptureResult.Key<*>>
+        get() = _resultKeys.value
+    override val sessionKeys: Set<CaptureRequest.Key<*>>
+        get() = _sessionKeys.value
+    override val physicalCameraIds: Set<CameraId>
+        get() = _physicalCameraIds.value
     override val physicalRequestKeys: Set<CaptureRequest.Key<*>>
         get() = _physicalRequestKeys.value
 
@@ -106,22 +108,21 @@
         check(physicalCameraIds.contains(cameraId)) {
             "$cameraId is not a valid physical camera on $this"
         }
-        return metadataProvider.getMetadata(cameraId)
+        return metadataProvider.getCameraMetadata(cameraId)
     }
 
     override fun awaitPhysicalMetadata(cameraId: CameraId): CameraMetadata {
         check(physicalCameraIds.contains(cameraId)) {
             "$cameraId is not a valid physical camera on $this"
         }
-        return metadataProvider.awaitMetadata(cameraId)
+        return metadataProvider.awaitCameraMetadata(cameraId)
     }
 
     private val _keys: Lazy<Set<CameraCharacteristics.Key<*>>> =
         lazy(LazyThreadSafetyMode.PUBLICATION) {
             try {
                 Debug.trace("Camera-${camera.value}#keys") {
-                    @Suppress("UselessCallOnNotNull")
-                    characteristics.keys.orEmpty().toSet()
+                    @Suppress("UselessCallOnNotNull") characteristics.keys.orEmpty().toSet()
                 }
             } catch (ignored: AssertionError) {
                 emptySet()
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
index 59b7f4f..48f45e5 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
@@ -25,18 +25,18 @@
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.CameraTimestamp
+import androidx.camera.camera2.pipe.CaptureSequence
+import androidx.camera.camera2.pipe.CaptureSequences.invokeOnRequest
+import androidx.camera.camera2.pipe.CaptureSequences.invokeOnRequests
 import androidx.camera.camera2.pipe.FrameNumber
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.RequestMetadata
 import androidx.camera.camera2.pipe.RequestNumber
 import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.CaptureSequence
-import androidx.camera.camera2.pipe.CaptureSequences.invokeOnRequest
-import androidx.camera.camera2.pipe.CaptureSequences.invokeOnRequests
 
 /**
- * This class responds to events from a set of one or more requests. It uses the tag field on
- * a [CaptureRequest] object to lookup and invoke per-request listeners so that a listener can be
+ * This class responds to events from a set of one or more requests. It uses the tag field on a
+ * [CaptureRequest] object to lookup and invoke per-request listeners so that a listener can be
  * defined on a specific request within a burst.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@@ -52,16 +52,18 @@
 ) : CameraCaptureSession.CaptureCallback(), CaptureSequence<CaptureRequest> {
     private val debugId = captureSequenceDebugIds.incrementAndGet()
 
-    @Volatile
-    private var _sequenceNumber: Int? = null
+    @Volatile private var _sequenceNumber: Int? = null
     override var sequenceNumber: Int
         get() {
             if (_sequenceNumber == null) {
                 // If the sequence id has not been submitted, it means the call to capture or
-                // setRepeating has not yet returned. The callback methods should never be synchronously
+                // setRepeating has not yet returned. The callback methods should never be
+                // synchronously
                 // invoked, so the only case this should happen is if a second thread attempted to
-                // invoke one of the callbacks before the initial call completed. By locking against the
-                // captureSequence object here and in the capture call, we can block the callback thread
+                // invoke one of the callbacks before the initial call completed. By locking against
+                // the
+                // captureSequence object here and in the capture call, we can block the callback
+                // thread
                 // until the sequenceId is available.
                 synchronized(this) {
                     return checkNotNull(_sequenceNumber) {
@@ -69,9 +71,7 @@
                     }
                 }
             }
-            return checkNotNull(_sequenceNumber) {
-                "SequenceNumber has not been set for $this!"
-            }
+            return checkNotNull(_sequenceNumber) { "SequenceNumber has not been set for $this!" }
         }
         set(value) {
             _sequenceNumber = value
@@ -91,13 +91,7 @@
         // normal circumstances this should never happen.
         val request = readRequestMetadata(requestNumber)
 
-        invokeOnRequest(request) {
-            it.onStarted(
-                request,
-                frameNumber,
-                timestamp
-            )
-        }
+        invokeOnRequest(request) { it.onStarted(request, frameNumber, timestamp) }
     }
 
     override fun onCaptureProgressed(
@@ -113,13 +107,7 @@
         // normal circumstances this should never happen.
         val request = readRequestMetadata(requestNumber)
 
-        invokeOnRequest(request) {
-            it.onPartialCaptureResult(
-                request,
-                frameNumber,
-                frameMetadata
-            )
-        }
+        invokeOnRequest(request) { it.onPartialCaptureResult(request, frameNumber, frameMetadata) }
     }
 
     override fun onCaptureCompleted(
@@ -136,29 +124,13 @@
         // normal circumstances this should never happen.
         val request = readRequestMetadata(requestNumber)
 
-        val frameInfo = AndroidFrameInfo(
-            captureResult,
-            cameraId,
-            request
-        )
+        val frameInfo = AndroidFrameInfo(captureResult, cameraId, request)
 
-        invokeOnRequest(request) {
-            it.onTotalCaptureResult(
-                request,
-                frameNumber,
-                frameInfo
-            )
-        }
+        invokeOnRequest(request) { it.onTotalCaptureResult(request, frameNumber, frameInfo) }
 
         // TODO: Implement a proper mechanism to delay the firing of onComplete(). See
         // androidx.camera.camera2.pipe.Request.Listener for context.
-        invokeOnRequest(request) {
-            it.onComplete(
-                request,
-                frameNumber,
-                frameInfo
-            )
-        }
+        invokeOnRequest(request) { it.onComplete(request, frameNumber, frameInfo) }
     }
 
     override fun onCaptureFailed(
@@ -175,13 +147,7 @@
         // normal circumstances this should never happen.
         val request = readRequestMetadata(requestNumber)
 
-        invokeOnRequest(request) {
-            it.onFailed(
-                request,
-                frameNumber,
-                captureFailure
-            )
-        }
+        invokeOnRequest(request) { it.onFailed(request, frameNumber, captureFailure) }
     }
 
     override fun onCaptureBufferLost(
@@ -192,21 +158,16 @@
     ) {
         val requestNumber = readRequestNumber(captureRequest)
         val frameNumber = FrameNumber(frameId)
-        val streamId = checkNotNull(surfaceMap[surface]) {
-            "Unable to find the streamId for $surface on frame $frameNumber"
-        }
+        val streamId =
+            checkNotNull(surfaceMap[surface]) {
+                "Unable to find the streamId for $surface on frame $frameNumber"
+            }
 
         // Load the request and throw if we are not able to find an associated request. Under
         // normal circumstances this should never happen.
         val request = readRequestMetadata(requestNumber)
 
-        invokeOnRequest(request) {
-            it.onBufferLost(
-                request,
-                frameNumber,
-                streamId
-            )
-        }
+        invokeOnRequest(request) { it.onBufferLost(request, frameNumber, streamId) }
     }
 
     override fun onCaptureSequenceCompleted(
@@ -238,9 +199,7 @@
                 "$captureSequenceId!"
         }
 
-        invokeOnRequests { request, _, listener ->
-            listener.onRequestSequenceAborted(request)
-        }
+        invokeOnRequests { request, _, listener -> listener.onRequestSequenceAborted(request) }
     }
 
     private fun readRequestNumber(request: CaptureRequest): RequestNumber =
@@ -253,4 +212,4 @@
     }
 
     override fun toString(): String = "Camera2CaptureSequence-$debugId"
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
index 6b0e98b..b0374d6 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
@@ -49,7 +49,9 @@
     ): CaptureSequenceProcessor<*, *>
 }
 
-internal class StandardCamera2CaptureSequenceProcessorFactory @Inject constructor(
+internal class StandardCamera2CaptureSequenceProcessorFactory
+@Inject
+constructor(
     private val threads: Threads,
     private val graphConfig: CameraGraph.Config,
     private val streamGraph: StreamGraphImpl
@@ -61,18 +63,15 @@
     ): CaptureSequenceProcessor<*, CaptureSequence<Any>> {
         @Suppress("SyntheticAccessor")
         return Camera2CaptureSequenceProcessor(
-            session,
-            threads,
-            graphConfig.defaultTemplate,
-            surfaceMap,
-            streamGraph
-        ) as CaptureSequenceProcessor<Any, CaptureSequence<Any>>
+            session, threads, graphConfig.defaultTemplate, surfaceMap, streamGraph)
+            as CaptureSequenceProcessor<Any, CaptureSequence<Any>>
     }
 }
 
 internal val captureSequenceProcessorDebugIds = atomic(0)
 internal val captureSequenceDebugIds = atomic(0L)
 internal val requestTags = atomic(0L)
+
 internal fun nextRequestTag(): RequestNumber = RequestNumber(requestTags.incrementAndGet())
 
 private const val REQUIRE_SURFACE_FOR_ALL_STREAMS = false
@@ -120,7 +119,8 @@
 
             val requestTemplate = request.template ?: template
 
-            // Create the request builder. There is a risk this will throw an exception or return null
+            // Create the request builder. There is a risk this will throw an exception or return
+            // null
             // if the CameraDevice has been closed or disconnected. If this fails, indicate that the
             // request was not submitted.
             val requestBuilder: CaptureRequest.Builder
@@ -178,8 +178,9 @@
                 // Check if video stream use case is present
                 val containsVideoStream =
                     request.streams.any {
-                        streamGraph.outputs
-                            .any { it.streamUseCase == OutputStream.StreamUseCase.VIDEO_RECORD }
+                        streamGraph.outputs.any {
+                            it.streamUseCase == OutputStream.StreamUseCase.VIDEO_RECORD
+                        }
                     }
 
                 // If preview stream is present with no recording stream, then only submit the first
@@ -189,40 +190,41 @@
                 // the same value instead of smoothly changing across each frame.
                 if (!containsVideoStream) {
                     captureRequests.add(highSpeedRequestList[0])
-                    // If recording video with or without preview stream, then add all requests to list
+                    // If recording video with or without preview stream, then add all requests to
+                    // list
                 } else {
                     captureRequests.addAll(highSpeedRequestList)
                 }
 
                 @Suppress("SyntheticAccessor")
-                val metadata = Camera2RequestMetadata(
-                    session,
-                    highSpeedRequestList[0],
-                    defaultParameters,
-                    requiredParameters,
-                    streamToSurfaceMap,
-                    requestTemplate,
-                    isRepeating,
-                    request,
-                    requestTag
-                )
+                val metadata =
+                    Camera2RequestMetadata(
+                        session,
+                        highSpeedRequestList[0],
+                        defaultParameters,
+                        requiredParameters,
+                        streamToSurfaceMap,
+                        requestTemplate,
+                        isRepeating,
+                        request,
+                        requestTag)
                 requestMap[requestTag] = metadata
                 requestList.add(metadata)
             } else {
                 captureRequests.add(captureRequest)
 
                 @Suppress("SyntheticAccessor")
-                val metadata = Camera2RequestMetadata(
-                    session,
-                    captureRequest,
-                    defaultParameters,
-                    requiredParameters,
-                    streamToSurfaceMap,
-                    requestTemplate,
-                    isRepeating,
-                    request,
-                    requestTag
-                )
+                val metadata =
+                    Camera2RequestMetadata(
+                        session,
+                        captureRequest,
+                        defaultParameters,
+                        requiredParameters,
+                        streamToSurfaceMap,
+                        requestTemplate,
+                        isRepeating,
+                        request,
+                        requestTag)
                 requestMap[requestTag] = metadata
                 requestList.add(metadata)
             }
@@ -238,42 +240,28 @@
             listeners,
             sequenceListener,
             requestMap,
-            surfaceToStreamMap
-        )
+            surfaceToStreamMap)
     }
 
     override fun submit(captureSequence: Camera2CaptureSequence): Int {
         val captureCallback = captureSequence as CameraCaptureSession.CaptureCallback
         // TODO: Update these calls to use executors on newer versions of the OS
         return if (captureSequence.captureRequestList.size == 1 &&
-            session !is CameraConstrainedHighSpeedCaptureSessionWrapper
-        ) {
+            session !is CameraConstrainedHighSpeedCaptureSessionWrapper) {
             if (captureSequence.repeating) {
                 session.setRepeatingRequest(
-                    captureSequence.captureRequestList[0],
-                    captureCallback,
-                    threads.camera2Handler
-                )
+                    captureSequence.captureRequestList[0], captureCallback, threads.camera2Handler)
             } else {
                 session.capture(
-                    captureSequence.captureRequestList[0],
-                    captureSequence,
-                    threads.camera2Handler
-                )
+                    captureSequence.captureRequestList[0], captureSequence, threads.camera2Handler)
             }
         } else {
             if (captureSequence.repeating) {
                 session.setRepeatingBurst(
-                    captureSequence.captureRequestList,
-                    captureSequence,
-                    threads.camera2Handler
-                )
+                    captureSequence.captureRequestList, captureSequence, threads.camera2Handler)
             } else {
                 session.captureBurst(
-                    captureSequence.captureRequestList,
-                    captureSequence,
-                    threads.camera2Handler
-                )
+                    captureSequence.captureRequestList, captureSequence, threads.camera2Handler)
             }
         }
     }
@@ -316,8 +304,9 @@
                 // Check if preview stream use case is present
                 containsPreviewStream =
                     request.streams.any {
-                        streamGraph.outputs
-                            .any { it.streamUseCase == OutputStream.StreamUseCase.PREVIEW }
+                        streamGraph.outputs.any {
+                            it.streamUseCase == OutputStream.StreamUseCase.PREVIEW
+                        }
                     }
 
                 // Check if all high speed requests have the same preview use case
@@ -336,8 +325,9 @@
                 // Check if video stream use case is present
                 containsVideoStream =
                     request.streams.any {
-                        streamGraph.outputs
-                            .any { it.streamUseCase == OutputStream.StreamUseCase.VIDEO_RECORD }
+                        streamGraph.outputs.any {
+                            it.streamUseCase == OutputStream.StreamUseCase.VIDEO_RECORD
+                        }
                     }
 
                 // Check if all high speed requests have the same video use case
@@ -420,9 +410,7 @@
     }
 }
 
-/**
- * This class packages together information about a request that was submitted to the camera.
- */
+/** This class packages together information about a request that was submitted to the camera. */
 @RequiresApi(21)
 @Suppress("SyntheticAccessor") // Using an inline class generates a synthetic constructor
 internal class Camera2RequestMetadata(
@@ -437,32 +425,30 @@
     override val requestNumber: RequestNumber
 ) : RequestMetadata {
     override fun <T> get(key: CaptureRequest.Key<T>): T? = captureRequest[key]
-    override fun <T> getOrDefault(key: CaptureRequest.Key<T>, default: T): T =
-        get(key) ?: default
+    override fun <T> getOrDefault(key: CaptureRequest.Key<T>, default: T): T = get(key) ?: default
 
     @Suppress("UNCHECKED_CAST")
-    override fun <T> get(key: Metadata.Key<T>): T? = when {
-        requiredParameters.containsKey(key) -> {
-            requiredParameters[key] as T?
+    override fun <T> get(key: Metadata.Key<T>): T? =
+        when {
+            requiredParameters.containsKey(key) -> {
+                requiredParameters[key] as T?
+            }
+            request.extras.containsKey(key) -> {
+                request.extras[key] as T?
+            }
+            else -> {
+                defaultParameters[key] as T?
+            }
         }
 
-        request.extras.containsKey(key) -> {
-            request.extras[key] as T?
-        }
-
-        else -> {
-            defaultParameters[key] as T?
-        }
-    }
-
     override fun <T> getOrDefault(key: Metadata.Key<T>, default: T): T = get(key) ?: default
 
     @Suppress("UNCHECKED_CAST")
-    override fun <T : Any> unwrapAs(type: KClass<T>): T? = when (type) {
-        CaptureRequest::class -> captureRequest as T
-        CameraCaptureSession::class ->
-            cameraCaptureSessionWrapper.unwrapAs(CameraCaptureSession::class) as? T
-
-        else -> null
-    }
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? =
+        when (type) {
+            CaptureRequest::class -> captureRequest as T
+            CameraCaptureSession::class ->
+                cameraCaptureSessionWrapper.unwrapAs(CameraCaptureSession::class) as? T
+            else -> null
+        }
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCache.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCache.kt
index 68c2be7..a5f7457 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCache.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCache.kt
@@ -31,30 +31,29 @@
 
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 @Singleton
-internal class Camera2DeviceCache @Inject constructor(
+internal class Camera2DeviceCache
+@Inject
+constructor(
     private val cameraManager: Provider<CameraManager>,
     private val threads: Threads,
 ) {
     private val lock = Any()
 
-    @GuardedBy("lock")
-    private var openableCameras: List<CameraId>? = null
+    @GuardedBy("lock") private var openableCameras: List<CameraId>? = null
 
-    suspend fun getCameras(): List<CameraId> {
+    suspend fun getCameraIds(): List<CameraId>? {
         val cameras = synchronized(lock) { openableCameras }
-        if (cameras?.isNotEmpty() == true) {
+        if (!cameras.isNullOrEmpty()) {
             return cameras
         }
 
         // Suspend and query the list of Cameras on the ioDispatcher
         return withContext(threads.backgroundDispatcher) {
             Debug.trace("readCameraIds") {
-                val cameraIds = readCameraIdList()
+                val cameraIds = awaitCameraIds()
 
-                if (cameraIds.isNotEmpty()) {
-                    synchronized(lock) {
-                        openableCameras = cameraIds
-                    }
+                if (!cameraIds.isNullOrEmpty()) {
+                    synchronized(lock) { openableCameras = cameraIds }
                     return@trace cameraIds
                 }
 
@@ -69,25 +68,27 @@
         }
     }
 
-    private fun readCameraIdList(): List<CameraId> {
+    fun awaitCameraIds(): List<CameraId>? {
         val cameras = synchronized(lock) { openableCameras }
-        if (cameras?.isNotEmpty() == true) {
+        if (!cameras.isNullOrEmpty()) {
             return cameras
         }
 
         val cameraManager = cameraManager.get()
-        val cameraIdArray = try {
-            // WARNING: This method can, at times, return an empty list of cameras on devices that
-            //  will normally return a valid list of cameras (b/159052778)
-            cameraManager.cameraIdList
-        } catch (e: CameraAccessException) {
-            Log.warn(e) { "Failed to query CameraManager#getCameraIdList!" }
-            null
-        }
-        if (cameraIdArray?.isEmpty() == true) {
+        val cameraIdArray =
+            try {
+                // WARNING: This method can, at times, return an empty list of cameras on devices
+                // that
+                //  will normally return a valid list of cameras (b/159052778)
+                cameraManager.cameraIdList
+            } catch (e: CameraAccessException) {
+                Log.warn(e) { "Failed to query CameraManager#getCameraIdList!" }
+                return null
+            }
+        if (cameraIdArray.isEmpty()) {
             Log.warn { "Failed to query CameraManager#getCameraIdList: No values returned." }
+            return emptyList()
         }
-
-        return cameraIdArray?.map { CameraId(it) } ?: listOf()
+        return cameraIdArray.map { CameraId(it) }
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt
index 52b9d83..5cf6757 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt
@@ -24,6 +24,7 @@
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.CameraMetadata
 import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.config.CameraPipeContext
 import androidx.camera.camera2.pipe.core.Debug
 import androidx.camera.camera2.pipe.core.Log
 import androidx.camera.camera2.pipe.core.Permissions
@@ -43,17 +44,18 @@
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 @Singleton
-internal class Camera2MetadataCache @Inject constructor(
-    private val context: Context,
+internal class Camera2MetadataCache
+@Inject
+constructor(
+    @CameraPipeContext private val cameraPipeContext: Context,
     private val threads: Threads,
     private val permissions: Permissions,
     private val cameraMetadataConfig: CameraPipe.CameraMetadataConfig,
     private val timeSource: TimeSource
-) : CameraMetadataProvider {
-    @GuardedBy("cache")
-    private val cache = ArrayMap<String, CameraMetadata>()
+) : Camera2MetadataProvider {
+    @GuardedBy("cache") private val cache = ArrayMap<String, CameraMetadata>()
 
-    override suspend fun getMetadata(cameraId: CameraId): CameraMetadata {
+    override suspend fun getCameraMetadata(cameraId: CameraId): CameraMetadata {
         synchronized(cache) {
             val existing = cache[cameraId.value]
             if (existing != null) {
@@ -62,12 +64,10 @@
         }
 
         // Suspend and query CameraMetadata on a background thread.
-        return withContext(threads.backgroundDispatcher) {
-            awaitMetadata(cameraId)
-        }
+        return withContext(threads.backgroundDispatcher) { awaitCameraMetadata(cameraId) }
     }
 
-    override fun awaitMetadata(cameraId: CameraId): CameraMetadata {
+    override fun awaitCameraMetadata(cameraId: CameraId): CameraMetadata {
         return Debug.trace("Camera-${cameraId.value}#awaitMetadata") {
             synchronized(cache) {
                 val existing = cache[cameraId.value]
@@ -83,19 +83,14 @@
         }
     }
 
-    fun readCameraMetadata(cameraId: CameraId): CameraMetadata {
-        return createCameraMetadata(cameraId, isMetadataRedacted())
-    }
-
     private fun createCameraMetadata(cameraId: CameraId, redacted: Boolean): Camera2CameraMetadata {
         val start = Timestamps.now(timeSource)
 
         return Debug.trace("Camera-${cameraId.value}#readCameraMetadata") {
             try {
                 val cameraManager =
-                    context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
-                val characteristics =
-                    cameraManager.getCameraCharacteristics(cameraId.value)
+                    cameraPipeContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+                val characteristics = cameraManager.getCameraCharacteristics(cameraId.value)
 
                 // This technically shouldn't be null per documentation, but we suspect it could be
                 // under certain devices in certain situations.
@@ -107,28 +102,24 @@
                 // Merge the camera specific and global cache blocklists together.
                 // this will prevent these values from being cached after first access.
                 val cameraBlocklist = cameraMetadataConfig.cameraCacheBlocklist[cameraId]
-                val cacheBlocklist = if (cameraBlocklist == null) {
-                    cameraMetadataConfig.cacheBlocklist
-                } else {
-                    cameraMetadataConfig.cacheBlocklist + cameraBlocklist
-                }
+                val cacheBlocklist =
+                    if (cameraBlocklist == null) {
+                        cameraMetadataConfig.cacheBlocklist
+                    } else {
+                        cameraMetadataConfig.cacheBlocklist + cameraBlocklist
+                    }
 
                 val cameraMetadata =
                     Camera2CameraMetadata(
-                        cameraId,
-                        redacted,
-                        characteristics,
-                        this,
-                        emptyMap(),
-                        cacheBlocklist
-                    )
+                        cameraId, redacted, characteristics, this, emptyMap(), cacheBlocklist)
 
                 Log.info {
                     val duration = Timestamps.now(timeSource) - start
-                    val redactedString = when (redacted) {
-                        false -> ""
-                        true -> " (redacted)"
-                    }
+                    val redactedString =
+                        when (redacted) {
+                            false -> ""
+                            true -> " (redacted)"
+                        }
                     "Loaded metadata for $cameraId in ${duration.formatMs()}$redactedString"
                 }
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataProvider.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataProvider.kt
new file mode 100644
index 0000000..4680eef
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataProvider.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.camera.camera2.pipe.compat
+
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
+
+/** Interface that can be used to query for [CameraMetadata] using an existing [CameraId]. */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+internal interface Camera2MetadataProvider {
+    /** Attempt to retrieve [CameraMetadata], suspending the caller if it is not yet available. */
+    suspend fun getCameraMetadata(cameraId: CameraId): CameraMetadata
+
+    /**
+     * Attempt to retrieve [CameraMetadata], blocking the calling thread if it is not yet available.
+     */
+    fun awaitCameraMetadata(cameraId: CameraId): CameraMetadata
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
index 72207b2..9303923 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
@@ -41,7 +41,8 @@
 import kotlin.reflect.KClass
 import kotlinx.atomicfu.atomic
 
-/** Interface around a [CameraDevice] with minor modifications.
+/**
+ * Interface around a [CameraDevice] with minor modifications.
  *
  * This interface has been modified to correct nullness, adjust exceptions, and to return or produce
  * wrapper interfaces instead of the native Camera2 types.
@@ -110,7 +111,7 @@
     @Throws(ObjectUnavailableException::class)
     fun createCaptureSession(config: SessionConfigData)
 
-    /** Invoked when the [CameraDevice] has been closed **/
+    /** Invoked when the [CameraDevice] has been closed */
     fun onDeviceClosed()
 }
 
@@ -126,9 +127,7 @@
     this?.let {
         val start = Timestamps.now(timeSource)
         Log.info { "Closing Camera ${it.id}" }
-        Debug.trace("CameraDevice-${it.id}#close") {
-            it.close()
-        }
+        Debug.trace("CameraDevice-${it.id}#close") { it.close() }
         val duration = Timestamps.now(timeSource) - start
         Log.info { "Closed Camera ${it.id} in ${duration.formatMs()}" }
     }
@@ -148,7 +147,6 @@
         stateCallback: CameraCaptureSessionWrapper.StateCallback,
         handler: Handler?
     ) = rethrowCamera2Exceptions {
-
         val previousStateCallback = _lastStateCallback.value
         check(_lastStateCallback.compareAndSet(previousStateCallback, stateCallback))
 
@@ -158,13 +156,8 @@
         cameraDevice.createCaptureSession(
             outputs,
             AndroidCaptureSessionStateCallback(
-                this,
-                stateCallback,
-                previousStateCallback,
-                interopSessionStateCallback
-            ),
-            handler
-        )
+                this, stateCallback, previousStateCallback, interopSessionStateCallback),
+            handler)
     }
 
     @RequiresApi(23)
@@ -174,7 +167,6 @@
         stateCallback: CameraCaptureSessionWrapper.StateCallback,
         handler: Handler?
     ) = rethrowCamera2Exceptions {
-
         val previousStateCallback = _lastStateCallback.value
         check(_lastStateCallback.compareAndSet(previousStateCallback, stateCallback))
 
@@ -185,13 +177,8 @@
             input,
             outputs,
             AndroidCaptureSessionStateCallback(
-                this,
-                stateCallback,
-                previousStateCallback,
-                interopSessionStateCallback
-            ),
-            handler
-        )
+                this, stateCallback, previousStateCallback, interopSessionStateCallback),
+            handler)
     }
 
     @RequiresApi(23)
@@ -200,7 +187,6 @@
         stateCallback: CameraCaptureSessionWrapper.StateCallback,
         handler: Handler?
     ) = rethrowCamera2Exceptions {
-
         val previousStateCallback = _lastStateCallback.value
         check(_lastStateCallback.compareAndSet(previousStateCallback, stateCallback))
 
@@ -210,13 +196,8 @@
             cameraDevice,
             outputs,
             AndroidCaptureSessionStateCallback(
-                this,
-                stateCallback,
-                previousStateCallback,
-                interopSessionStateCallback
-            ),
-            handler
-        )
+                this, stateCallback, previousStateCallback, interopSessionStateCallback),
+            handler)
     }
 
     @RequiresApi(24)
@@ -225,7 +206,6 @@
         stateCallback: CameraCaptureSessionWrapper.StateCallback,
         handler: Handler?
     ) = rethrowCamera2Exceptions {
-
         val previousStateCallback = _lastStateCallback.value
         check(_lastStateCallback.compareAndSet(previousStateCallback, stateCallback))
 
@@ -235,13 +215,8 @@
             cameraDevice,
             outputConfigurations.map { it.unwrapAs(OutputConfiguration::class) },
             AndroidCaptureSessionStateCallback(
-                this,
-                stateCallback,
-                previousStateCallback,
-                interopSessionStateCallback
-            ),
-            handler
-        )
+                this, stateCallback, previousStateCallback, interopSessionStateCallback),
+            handler)
     }
 
     @RequiresApi(24)
@@ -251,7 +226,6 @@
         stateCallback: CameraCaptureSessionWrapper.StateCallback,
         handler: Handler?
     ) = rethrowCamera2Exceptions {
-
         val previousStateCallback = _lastStateCallback.value
         check(_lastStateCallback.compareAndSet(previousStateCallback, stateCallback))
 
@@ -260,17 +234,11 @@
         Api24Compat.createCaptureSessionByOutputConfigurations(
             cameraDevice,
             Api23Compat.newInputConfiguration(
-                inputConfig.width, inputConfig.height, inputConfig.format
-            ),
+                inputConfig.width, inputConfig.height, inputConfig.format),
             outputs.map { it.unwrapAs(OutputConfiguration::class) },
             AndroidCaptureSessionStateCallback(
-                this,
-                stateCallback,
-                previousStateCallback,
-                interopSessionStateCallback
-            ),
-            handler
-        )
+                this, stateCallback, previousStateCallback, interopSessionStateCallback),
+            handler)
     }
 
     @RequiresApi(28)
@@ -279,17 +247,13 @@
         val previousStateCallback = _lastStateCallback.value
         check(_lastStateCallback.compareAndSet(previousStateCallback, stateCallback))
 
-        val sessionConfig = Api28Compat.newSessionConfiguration(
-            config.sessionType,
-            config.outputConfigurations.map { it.unwrapAs(OutputConfiguration::class) },
-            config.executor,
-            AndroidCaptureSessionStateCallback(
-                this,
-                stateCallback,
-                previousStateCallback,
-                interopSessionStateCallback
-            )
-        )
+        val sessionConfig =
+            Api28Compat.newSessionConfiguration(
+                config.sessionType,
+                config.outputConfigurations.map { it.unwrapAs(OutputConfiguration::class) },
+                config.executor,
+                AndroidCaptureSessionStateCallback(
+                    this, stateCallback, previousStateCallback, interopSessionStateCallback))
 
         if (config.inputConfiguration != null) {
             Api28Compat.setInputConfiguration(
@@ -297,9 +261,7 @@
                 Api23Compat.newInputConfiguration(
                     config.inputConfiguration.width,
                     config.inputConfiguration.height,
-                    config.inputConfiguration.format
-                )
-            )
+                    config.inputConfiguration.format))
         }
 
         val requestBuilder = cameraDevice.createCaptureRequest(config.sessionTemplateId)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraMetadataProvider.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraMetadataProvider.kt
deleted file mode 100644
index 99ab1f45..0000000
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraMetadataProvider.kt
+++ /dev/null
@@ -1,37 +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.camera.camera2.pipe.compat
-
-import androidx.annotation.RequiresApi
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CameraMetadata
-
-/**
- * Interface that can be used to query for [CameraMetadata] using an existing [CameraId].
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-interface CameraMetadataProvider {
-    /**
-     * Attempt to retrieve [CameraMetadata], suspending the caller if it is not yet available.
-     */
-    suspend fun getMetadata(cameraId: CameraId): CameraMetadata
-
-    /**
-     * Attempt to retrieve [CameraMetadata], blocking the calling thread if it is not yet available.
-     */
-    fun awaitMetadata(cameraId: CameraId): CameraMetadata
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt
index 2e3f922..f9767ba 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt
@@ -24,6 +24,7 @@
 import android.view.Surface
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.StreamId
 import androidx.camera.camera2.pipe.compat.OutputConfigurationWrapper.Companion.SURFACE_GROUP_ID_NONE
 import androidx.camera.camera2.pipe.config.Camera2ControllerScope
@@ -35,13 +36,11 @@
 import javax.inject.Inject
 import javax.inject.Provider
 
-/**
- * Creates a Camera2 CaptureSession from a CameraDevice
- */
+/** Creates a Camera2 CaptureSession from a CameraDevice */
 internal interface CaptureSessionFactory {
     /**
-     * Create a Camera2 CaptureSession using the given device, surfaces, and listener and return
-     * a map of outputs that are not yet available.
+     * Create a Camera2 CaptureSession using the given device, surfaces, and listener and return a
+     * map of outputs that are not yet available.
      */
     fun create(
         cameraDevice: CameraDeviceWrapper,
@@ -85,17 +84,14 @@
         check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
             "CameraPipe is not supported below Android L"
         }
-        check(graphConfig.input == null) {
-            "Reprocessing is not supported on Android L"
-        }
+        check(graphConfig.input == null) { "Reprocessing is not supported on Android L" }
 
         return androidLProvider.get()
     }
 }
 
-internal class AndroidLSessionFactory @Inject constructor(
-    private val threads: Threads
-) : CaptureSessionFactory {
+internal class AndroidLSessionFactory @Inject constructor(private val threads: Threads) :
+    CaptureSessionFactory {
     override fun create(
         cameraDevice: CameraDeviceWrapper,
         surfaces: Map<StreamId, Surface>,
@@ -103,10 +99,7 @@
     ): Map<StreamId, OutputConfigurationWrapper> {
         try {
             cameraDevice.createCaptureSession(
-                surfaces.map { it.value },
-                captureSessionState,
-                threads.camera2Handler
-            )
+                surfaces.map { it.value }, captureSessionState, threads.camera2Handler)
         } catch (e: Throwable) {
             Log.warn {
                 "Failed to create capture session from $cameraDevice for $captureSessionState!"
@@ -118,10 +111,10 @@
 }
 
 @RequiresApi(Build.VERSION_CODES.M)
-internal class AndroidMSessionFactory @Inject constructor(
-    private val threads: Threads,
-    private val graphConfig: CameraGraph.Config
-) : CaptureSessionFactory {
+internal class AndroidMSessionFactory
+@Inject
+constructor(private val threads: Threads, private val graphConfig: CameraGraph.Config) :
+    CaptureSessionFactory {
     override fun create(
         cameraDevice: CameraDeviceWrapper,
         surfaces: Map<StreamId, Surface>,
@@ -134,12 +127,10 @@
                     InputConfiguration(
                         outputConfig.size.width,
                         outputConfig.size.height,
-                        outputConfig.format.value
-                    ),
+                        outputConfig.format.value),
                     surfaces.map { it.value },
                     captureSessionState,
-                    threads.camera2Handler
-                )
+                    threads.camera2Handler)
             } catch (e: Throwable) {
                 Log.warn {
                     "Failed to create reprocessable captures session from $cameraDevice for" +
@@ -150,10 +141,7 @@
         } else {
             try {
                 cameraDevice.createCaptureSession(
-                    surfaces.map { it.value },
-                    captureSessionState,
-                    threads.camera2Handler
-                )
+                    surfaces.map { it.value }, captureSessionState, threads.camera2Handler)
             } catch (e: Throwable) {
                 Log.warn {
                     "Failed to create captures session from $cameraDevice for $captureSessionState!"
@@ -166,9 +154,8 @@
 }
 
 @RequiresApi(Build.VERSION_CODES.M)
-internal class AndroidMHighSpeedSessionFactory @Inject constructor(
-    private val threads: Threads
-) : CaptureSessionFactory {
+internal class AndroidMHighSpeedSessionFactory @Inject constructor(private val threads: Threads) :
+    CaptureSessionFactory {
     override fun create(
         cameraDevice: CameraDeviceWrapper,
         surfaces: Map<StreamId, Surface>,
@@ -176,10 +163,7 @@
     ): Map<StreamId, OutputConfigurationWrapper> {
         try {
             cameraDevice.createConstrainedHighSpeedCaptureSession(
-                surfaces.map { it.value },
-                captureSessionState,
-                threads.camera2Handler
-            )
+                surfaces.map { it.value }, captureSessionState, threads.camera2Handler)
         } catch (e: Throwable) {
             Log.warn {
                 "Failed to create ConstrainedHighSpeedCaptureSession " +
@@ -192,10 +176,13 @@
 }
 
 @RequiresApi(Build.VERSION_CODES.N)
-internal class AndroidNSessionFactory @Inject constructor(
+internal class AndroidNSessionFactory
+@Inject
+constructor(
     private val threads: Threads,
     private val streamGraph: StreamGraphImpl,
-    private val graphConfig: CameraGraph.Config
+    private val graphConfig: CameraGraph.Config,
+    private val camera2MetadataProvider: Camera2MetadataProvider
 ) : CaptureSessionFactory {
     override fun create(
         cameraDevice: CameraDeviceWrapper,
@@ -205,7 +192,9 @@
         val outputs = buildOutputConfigurations(
             graphConfig,
             streamGraph,
-            surfaces
+            surfaces,
+            camera2MetadataProvider,
+            cameraDevice.cameraId
         )
         if (outputs.all.isEmpty()) {
             Log.warn { "Failed to create OutputConfigurations for $graphConfig" }
@@ -215,22 +204,17 @@
         try {
             if (graphConfig.input == null) {
                 cameraDevice.createCaptureSessionByOutputConfigurations(
-                    outputs.all,
-                    captureSessionState,
-                    threads.camera2Handler
-                )
+                    outputs.all, captureSessionState, threads.camera2Handler)
             } else {
                 val outputConfig = graphConfig.input.stream.outputs.single()
                 cameraDevice.createReprocessableCaptureSessionByConfigurations(
                     InputConfigData(
                         outputConfig.size.width,
                         outputConfig.size.height,
-                        outputConfig.format.value
-                    ),
+                        outputConfig.format.value),
                     outputs.all,
                     captureSessionState,
-                    threads.camera2Handler
-                )
+                    threads.camera2Handler)
             }
         } catch (e: Throwable) {
             Log.warn {
@@ -243,10 +227,13 @@
 }
 
 @RequiresApi(Build.VERSION_CODES.P)
-internal class AndroidPSessionFactory @Inject constructor(
+internal class AndroidPSessionFactory
+@Inject
+constructor(
     private val threads: Threads,
     private val graphConfig: CameraGraph.Config,
-    private val streamGraph: StreamGraphImpl
+    private val streamGraph: StreamGraphImpl,
+    private val camera2MetadataProvider: Camera2MetadataProvider
 ) : CaptureSessionFactory {
     override fun create(
         cameraDevice: CameraDeviceWrapper,
@@ -263,31 +250,31 @@
         val outputs = buildOutputConfigurations(
             graphConfig,
             streamGraph,
-            surfaces
+            surfaces,
+            camera2MetadataProvider,
+            cameraDevice.cameraId
         )
         if (outputs.all.isEmpty()) {
             Log.warn { "Failed to create OutputConfigurations for $graphConfig" }
             return emptyMap()
         }
 
-        val input = graphConfig.input?.let {
-            val outputConfig = it.stream.outputs.single()
-            InputConfigData(
-                outputConfig.size.width,
-                outputConfig.size.height,
-                outputConfig.format.value
-            )
-        }
+        val input =
+            graphConfig.input?.let {
+                val outputConfig = it.stream.outputs.single()
+                InputConfigData(
+                    outputConfig.size.width, outputConfig.size.height, outputConfig.format.value)
+            }
 
-        val sessionConfig = SessionConfigData(
-            operatingMode,
-            input,
-            outputs.all,
-            threads.camera2Executor,
-            captureSessionState,
-            graphConfig.sessionTemplate.value,
-            graphConfig.sessionParameters
-        )
+        val sessionConfig =
+            SessionConfigData(
+                operatingMode,
+                input,
+                outputs.all,
+                threads.camera2Executor,
+                captureSessionState,
+                graphConfig.sessionTemplate.value,
+                graphConfig.sessionParameters)
 
         try {
             cameraDevice.createCaptureSession(sessionConfig)
@@ -305,7 +292,9 @@
 internal fun buildOutputConfigurations(
     graphConfig: CameraGraph.Config,
     streamGraph: StreamGraphImpl,
-    surfaces: Map<StreamId, Surface>
+    surfaces: Map<StreamId, Surface>,
+    camera2MetadataProvider: Camera2MetadataProvider,
+    cameraId: CameraId
 ): OutputConfigurations {
     val allOutputs = arrayListOf<OutputConfigurationWrapper>()
     val deferredOutputs = mutableMapOf<StreamId, OutputConfigurationWrapper>()
@@ -326,8 +315,7 @@
                     surfaceSharing = false, // No way to read this value.
                     maxSharedSurfaceCount = 1, // Hardcoded
                     physicalCameraId = null, // No way to read this value.
-                )
-            )
+                ))
             continue
         }
 
@@ -346,7 +334,9 @@
                     outputConfig.camera
                 } else {
                     null
-                }
+                },
+                cameraId = cameraId,
+                camera2MetadataProvider = camera2MetadataProvider
             )
             if (output == null) {
                 Log.warn { "Failed to create AndroidOutputConfiguration for $outputConfig" }
@@ -378,7 +368,9 @@
                 outputConfig.camera
             } else {
                 null
-            }
+            },
+            cameraId = cameraId,
+            camera2MetadataProvider = camera2MetadataProvider
         )
         if (output == null) {
             Log.warn { "Failed to create AndroidOutputConfiguration for $outputConfig" }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt
index 33149bc..7a3fbd2 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt
@@ -32,7 +32,6 @@
 import androidx.camera.camera2.pipe.core.Timestamps.formatMs
 import androidx.camera.camera2.pipe.graph.GraphListener
 import androidx.camera.camera2.pipe.graph.GraphRequestProcessor
-import java.io.Closeable
 import java.util.Collections.synchronizedMap
 import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.CoroutineScope
@@ -46,9 +45,9 @@
  * it to the [GraphListener].
  *
  * After this object is created, it waits for:
- *  - A valid CameraDevice via [cameraDevice]
- *  - A valid map of Surfaces via [configureSurfaceMap]
- * Once these objects are available, it will create the [CameraCaptureSession].
+ * - A valid CameraDevice via [cameraDevice]
+ * - A valid map of Surfaces via [configureSurfaceMap] Once these objects are available, it will
+ *   create the [CameraCaptureSession].
  *
  * If at any time this object is put into a COSING or CLOSED state the session will either never be
  * created, or if the session has already been created, it will be de-referenced and ignored. This
@@ -57,6 +56,7 @@
  *
  * This class is thread safe.
  */
+@RequiresApi(21)
 internal class CaptureSessionState(
     private val graphListener: GraphListener,
     private val captureSessionFactory: CaptureSessionFactory,
@@ -72,32 +72,29 @@
     private val activeSurfaceMap = synchronizedMap(HashMap<StreamId, Surface>())
     private var sessionCreatingTimestamp: TimestampNs? = null
 
-    @GuardedBy("lock")
-    private var _cameraDevice: CameraDeviceWrapper? = null
+    @GuardedBy("lock") private var _cameraDevice: CameraDeviceWrapper? = null
     var cameraDevice: CameraDeviceWrapper?
         get() = synchronized(lock) { _cameraDevice }
-        set(value) = synchronized(lock) {
-            if (state == State.CLOSING || state == State.CLOSED) {
-                return
+        set(value) =
+            synchronized(lock) {
+                if (state == State.CLOSING || state == State.CLOSED) {
+                    return
+                }
+
+                _cameraDevice = value
+                if (value != null) {
+                    scope.launch { tryCreateCaptureSession() }
+                }
             }
 
-            _cameraDevice = value
-            if (value != null) {
-                scope.launch { tryCreateCaptureSession() }
-            }
-        }
-
-    @GuardedBy("lock")
-    private var cameraCaptureSession: ConfiguredCameraCaptureSession? = null
+    @GuardedBy("lock") private var cameraCaptureSession: ConfiguredCameraCaptureSession? = null
 
     @GuardedBy("lock")
     private var pendingOutputMap: Map<StreamId, OutputConfigurationWrapper>? = null
 
-    @GuardedBy("lock")
-    private var pendingSurfaceMap: Map<StreamId, Surface>? = null
+    @GuardedBy("lock") private var pendingSurfaceMap: Map<StreamId, Surface>? = null
 
-    @GuardedBy("lock")
-    private var state = State.PENDING
+    @GuardedBy("lock") private var state = State.PENDING
 
     private enum class State {
         PENDING,
@@ -107,11 +104,10 @@
         CLOSED
     }
 
-    @GuardedBy("lock")
-    private var _surfaceMap: Map<StreamId, Surface>? = null
+    @GuardedBy("lock") private var _surfaceMap: Map<StreamId, Surface>? = null
 
     @GuardedBy("lock")
-    private val _surfaceTokenMap: MutableMap<Surface, Closeable> = mutableMapOf()
+    private val _surfaceTokenMap: MutableMap<Surface, AutoCloseable> = mutableMapOf()
     fun configureSurfaceMap(surfaces: Map<StreamId, Surface>) {
         synchronized(lock) {
             if (state == State.CLOSING || state == State.CLOSED) {
@@ -197,12 +193,11 @@
             }
 
             if (cameraCaptureSession == null && session != null) {
-                captureSession = ConfiguredCameraCaptureSession(
-                    session,
-                    GraphRequestProcessor.from(
-                        captureSequenceProcessorFactory.create(session, activeSurfaceMap)
-                    )
-                )
+                captureSession =
+                    ConfiguredCameraCaptureSession(
+                        session,
+                        GraphRequestProcessor.from(
+                            captureSequenceProcessorFactory.create(session, activeSurfaceMap)))
                 cameraCaptureSession = captureSession
             } else {
                 captureSession = cameraCaptureSession
@@ -306,11 +301,12 @@
     }
 
     private fun finalizeSession() {
-        val tokenList = synchronized(lock) {
-            val tokens = _surfaceTokenMap.values.toList()
-            _surfaceTokenMap.clear()
-            tokens
-        }
+        val tokenList =
+            synchronized(lock) {
+                val tokens = _surfaceTokenMap.values.toList()
+                _surfaceTokenMap.clear()
+                tokens
+            }
         tokenList.forEach { it.close() }
     }
 
@@ -384,11 +380,10 @@
             "Creating CameraCaptureSession from ${device?.cameraId} using $this with $surfaces"
         }
 
-        val deferred = Debug.trace(
-            "CameraDevice-${device?.cameraId?.value}#createCaptureSession"
-        ) {
-            captureSessionFactory.create(device!!, surfaces!!, this)
-        }
+        val deferred =
+            Debug.trace("CameraDevice-${device?.cameraId?.value}#createCaptureSession") {
+                captureSessionFactory.create(device!!, surfaces!!, this)
+            }
 
         synchronized(lock) {
             if (state == State.CLOSING || state == State.CLOSED) {
@@ -406,13 +401,10 @@
                 }
                 pendingOutputMap = deferred
 
-                val availableDeferredSurfaces = _surfaceMap?.filter {
-                    deferred.containsKey(it.key)
-                }
+                val availableDeferredSurfaces = _surfaceMap?.filter { deferred.containsKey(it.key) }
 
                 if (availableDeferredSurfaces != null &&
-                    availableDeferredSurfaces.size == deferred.size
-                ) {
+                    availableDeferredSurfaces.size == deferred.size) {
                     pendingSurfaceMap = availableDeferredSurfaces
                 }
             }
@@ -453,4 +445,4 @@
         val session: CameraCaptureSessionWrapper,
         val processor: GraphRequestProcessor
     )
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionWrapper.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionWrapper.kt
index a6ae372..2a693a9 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionWrapper.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionWrapper.kt
@@ -29,7 +29,6 @@
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.UnsafeWrapper
 import androidx.camera.camera2.pipe.core.Log
-import java.io.Closeable
 import kotlin.reflect.KClass
 import kotlinx.atomicfu.atomic
 
@@ -39,44 +38,37 @@
  * This interface has been modified to correct nullness, adjust exceptions, and to return or produce
  * wrapper interfaces instead of the native Camera2 types.
  */
-internal interface CameraCaptureSessionWrapper : UnsafeWrapper, Closeable {
+internal interface CameraCaptureSessionWrapper : UnsafeWrapper, AutoCloseable {
 
     /**
-     * @see [CameraCaptureSession.getDevice]
-     *
      * @return The [CameraDeviceWrapper] that created this CameraCaptureSession
+     * @see [CameraCaptureSession.getDevice]
      */
     val device: CameraDeviceWrapper
 
     /**
+     * @return True if the application can submit reprocess capture requests with this camera
+     *   capture session. false otherwise.
      * @see [CameraCaptureSession.isReprocessable].
-     *
-     * @return True if the application can submit reprocess capture requests with this camera capture
-     * session. false otherwise.
      */
     val isReprocessable: Boolean
 
     /**
-     * @see [CameraCaptureSession.getInputSurface]
-     *
      * @return The Surface where reprocessing capture requests get the input images from.
+     * @see [CameraCaptureSession.getInputSurface]
      */
     val inputSurface: Surface?
 
-    /**
-     * @see [CameraCaptureSession.abortCaptures].
-     */
-    @Throws(ObjectUnavailableException::class)
-    fun abortCaptures()
+    /** @see [CameraCaptureSession.abortCaptures]. */
+    @Throws(ObjectUnavailableException::class) fun abortCaptures()
 
     /**
-     * @see [CameraCaptureSession.capture].
-     *
      * @param request The settings for this exposure
      * @param listener The callback object to notify once this request has been processed.
      * @param handler The handler on which the listener should be invoked, or null to use the
-     * current thread's looper.
+     *   current thread's looper.
      * @return An unique capture sequence id.
+     * @see [CameraCaptureSession.capture].
      */
     @Throws(ObjectUnavailableException::class)
     fun capture(
@@ -86,14 +78,13 @@
     ): Int
 
     /**
-     * @see [CameraCaptureSession.captureBurst].
-     *
      * @param requests A list of CaptureRequest(s) for this sequence of exposures
-     * @param listener A callback object to notify each time one of the requests in the burst has been
-     * processed.
-     * @param handler The handler on which the listener should be invoked, or null to use the current
-     * thread's looper.
+     * @param listener A callback object to notify each time one of the requests in the burst has
+     *   been processed.
+     * @param handler The handler on which the listener should be invoked, or null to use the
+     *   current thread's looper.
      * @return An unique capture sequence id.
+     * @see [CameraCaptureSession.captureBurst].
      */
     @Throws(ObjectUnavailableException::class)
     fun captureBurst(
@@ -103,14 +94,13 @@
     ): Int
 
     /**
-     * @see [CameraCaptureSession.setRepeatingBurst]
-     *
      * @param requests A list of settings to cycle through indefinitely.
      * @param listener A callback object to notify each time one of the requests in the repeating
-     * bursts has finished processing.
-     * @param handler The handler on which the listener should be invoked, or null to use the current
-     * thread's looper.
+     *   bursts has finished processing.
+     * @param handler The handler on which the listener should be invoked, or null to use the
+     *   current thread's looper.
      * @return An unique capture sequence ID.
+     * @see [CameraCaptureSession.setRepeatingBurst]
      */
     @Throws(ObjectUnavailableException::class)
     fun setRepeatingBurst(
@@ -120,13 +110,12 @@
     ): Int
 
     /**
-     * @see [CameraCaptureSession.setRepeatingRequest].
-     *
      * @param request The request to repeat indefinitely.
      * @param listener The callback object to notify every time the request finishes processing.
-     * @param handler The handler on which the listener should be invoked, or null to use the current
-     * thread's looper.
+     * @param handler The handler on which the listener should be invoked, or null to use the
+     *   current thread's looper.
      * @return An unique capture sequence ID.
+     * @see [CameraCaptureSession.setRepeatingRequest].
      */
     @Throws(ObjectUnavailableException::class)
     fun setRepeatingRequest(
@@ -135,13 +124,10 @@
         handler: Handler?
     ): Int
 
-    /**
-     * @see [CameraCaptureSession.stopRepeating].
-     */
-    @Throws(ObjectUnavailableException::class)
-    fun stopRepeating()
+    /** @see [CameraCaptureSession.stopRepeating]. */
+    @Throws(ObjectUnavailableException::class) fun stopRepeating()
 
-    /** Forwards to CameraCaptureSession#finalizeOutputConfigurations  */
+    /** Forwards to CameraCaptureSession#finalizeOutputConfigurations */
     @Throws(ObjectUnavailableException::class)
     fun finalizeOutputConfigurations(outputConfigs: List<OutputConfigurationWrapper>)
 
@@ -166,10 +152,10 @@
         fun onCaptureQueueEmpty(session: CameraCaptureSessionWrapper)
 
         /**
-         * Artificial event indicating the session is no longer in use and may be called
-         * several times. [onClosed] and [onConfigureFailed] will call this method directly. This
-         * method should also be called whenever the underlying camera devices is closed, and
-         * whenever a subsequent capture session is configured on the same camera device.
+         * Artificial event indicating the session is no longer in use and may be called several
+         * times. [onClosed] and [onConfigureFailed] will call this method directly. This method
+         * should also be called whenever the underlying camera devices is closed, and whenever a
+         * subsequent capture session is configured on the same camera device.
          *
          * See b/249258992 for more details.
          */
@@ -256,8 +242,7 @@
         // this happens, several methods are not allowed, the behavior is different, and interacting
         // with the session requires several behavior changes for these interactions to work well.
         return if (Build.VERSION.SDK_INT >= 23 &&
-            session is CameraConstrainedHighSpeedCaptureSession
-        ) {
+            session is CameraConstrainedHighSpeedCaptureSession) {
             AndroidCameraConstrainedHighSpeedCaptureSession(device, session)
         } else {
             AndroidCameraCaptureSession(device, session)
@@ -272,9 +257,7 @@
     private fun finalizeLastSession() {
         // Clear out the reference to the previous session, if one was set.
         val previousSession = _lastStateCallback.getAndSet(null)
-        previousSession?.let {
-            previousSession.onSessionFinalized()
-        }
+        previousSession?.let { previousSession.onSessionFinalized() }
     }
 
     @RequiresApi(Build.VERSION_CODES.O)
@@ -296,9 +279,7 @@
     private val cameraCaptureSession: CameraCaptureSession
 ) : CameraCaptureSessionWrapper {
     override fun abortCaptures() {
-        rethrowCamera2Exceptions {
-            cameraCaptureSession.abortCaptures()
-        }
+        rethrowCamera2Exceptions { cameraCaptureSession.abortCaptures() }
     }
 
     override fun capture(
@@ -306,9 +287,7 @@
         listener: CameraCaptureSession.CaptureCallback,
         handler: Handler?
     ): Int {
-        return rethrowCamera2Exceptions {
-            cameraCaptureSession.capture(request, listener, handler)
-        }
+        return rethrowCamera2Exceptions { cameraCaptureSession.capture(request, listener, handler) }
     }
 
     override fun captureBurst(
@@ -327,10 +306,7 @@
         handler: Handler?
     ): Int {
         return rethrowCamera2Exceptions {
-            cameraCaptureSession.setRepeatingBurst(
-                requests, listener,
-                handler
-            )
+            cameraCaptureSession.setRepeatingBurst(requests, listener, handler)
         }
     }
 
@@ -345,9 +321,7 @@
     }
 
     override fun stopRepeating() {
-        rethrowCamera2Exceptions {
-            cameraCaptureSession.stopRepeating()
-        }
+        rethrowCamera2Exceptions { cameraCaptureSession.stopRepeating() }
     }
 
     override val isReprocessable: Boolean
@@ -379,17 +353,16 @@
 
         rethrowCamera2Exceptions {
             Api26Compat.finalizeOutputConfigurations(
-                cameraCaptureSession,
-                outputConfigs.map { it.unwrapAs(OutputConfiguration::class) }
-            )
+                cameraCaptureSession, outputConfigs.map { it.unwrapAs(OutputConfiguration::class) })
         }
     }
 
     @Suppress("UNCHECKED_CAST")
-    override fun <T : Any> unwrapAs(type: KClass<T>): T? = when (type) {
-        CameraCaptureSession::class -> cameraCaptureSession as T?
-        else -> null
-    }
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? =
+        when (type) {
+            CameraCaptureSession::class -> cameraCaptureSession as T?
+            else -> null
+        }
 
     override fun close() {
         return cameraCaptureSession.close()
@@ -397,11 +370,12 @@
 }
 
 /**
- * An implementation of [CameraConstrainedHighSpeedCaptureSessionWrapper] forwards calls to a
- * real [CameraConstrainedHighSpeedCaptureSession].
+ * An implementation of [CameraConstrainedHighSpeedCaptureSessionWrapper] forwards calls to a real
+ * [CameraConstrainedHighSpeedCaptureSession].
  */
 @RequiresApi(23)
-internal class AndroidCameraConstrainedHighSpeedCaptureSession internal constructor(
+internal class AndroidCameraConstrainedHighSpeedCaptureSession
+internal constructor(
     device: CameraDeviceWrapper,
     private val session: CameraConstrainedHighSpeedCaptureSession
 ) : AndroidCameraCaptureSession(device, session), CameraConstrainedHighSpeedCaptureSessionWrapper {
@@ -434,8 +408,9 @@
     }
 
     @Suppress("UNCHECKED_CAST")
-    override fun <T : Any> unwrapAs(type: KClass<T>): T? = when (type) {
-        CameraConstrainedHighSpeedCaptureSession::class -> session as T?
-        else -> super.unwrapAs(type)
-    }
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? =
+        when (type) {
+            CameraConstrainedHighSpeedCaptureSession::class -> session as T?
+            else -> super.unwrapAs(type)
+        }
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
index a4fc62e..d5b60d1 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
@@ -51,7 +51,6 @@
     val outputConfigurations: List<OutputConfigurationWrapper>,
     val executor: Executor,
     val stateCallback: CameraCaptureSessionWrapper.StateCallback,
-
     val sessionTemplateId: Int,
     val sessionParameters: Map<*, Any?>
 ) {
@@ -67,11 +66,7 @@
  * that a real instance can be constructed when creating a
  * [android.hardware.camera2.CameraCaptureSession] on newer versions of the OS.
  */
-internal data class InputConfigData(
-    val width: Int,
-    val height: Int,
-    val format: Int
-)
+internal data class InputConfigData(val width: Int, val height: Int, val format: Int)
 
 /**
  * An interface for [OutputConfiguration] with minor modifications.
@@ -84,8 +79,8 @@
  */
 internal interface OutputConfigurationWrapper : UnsafeWrapper {
     /**
-     * This method will return null if the output configuration was created without a Surface,
-     * and until addSurface is called for the first time.
+     * This method will return null if the output configuration was created without a Surface, and
+     * until addSurface is called for the first time.
      *
      * @see OutputConfiguration.getSurface
      */
@@ -93,8 +88,8 @@
 
     /**
      * This method returns the current list of surfaces for this [OutputConfiguration]. Since the
-     * [OutputConfiguration] is stateful, this value may change as a result of calling addSurface
-     * or removeSurface.
+     * [OutputConfiguration] is stateful, this value may change as a result of calling addSurface or
+     * removeSurface.
      *
      * @see OutputConfiguration.getSurfaces
      */
@@ -147,7 +142,9 @@
             size: Size? = null,
             surfaceSharing: Boolean = false,
             surfaceGroupId: Int = SURFACE_GROUP_ID_NONE,
-            physicalCameraId: CameraId? = null
+            physicalCameraId: CameraId? = null,
+            cameraId: CameraId? = null,
+            camera2MetadataProvider: Camera2MetadataProvider? = null,
         ): OutputConfigurationWrapper? {
             check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
 
@@ -163,11 +160,12 @@
                 // Because there's no way to cleanly synchronize and check the value, we catch the
                 // exception for these cases.
                 try {
-                    configuration = if (surfaceGroupId != SURFACE_GROUP_ID_NONE) {
-                        OutputConfiguration(surfaceGroupId, surface)
-                    } else {
-                        OutputConfiguration(surface)
-                    }
+                    configuration =
+                        if (surfaceGroupId != SURFACE_GROUP_ID_NONE) {
+                            OutputConfiguration(surfaceGroupId, surface)
+                        } else {
+                            OutputConfiguration(surface)
+                        }
                 } catch (e: Throwable) {
                     Log.warn(e) { "Failed to create an OutputConfiguration for $surface!" }
                     return null
@@ -183,13 +181,13 @@
                 check(size != null) {
                     "Size must defined when creating a deferred OutputConfiguration."
                 }
-                val outputKlass = when (outputType) {
-                    OutputType.SURFACE_TEXTURE -> SurfaceTexture::class.java
-                    OutputType.SURFACE_VIEW -> SurfaceHolder::class.java
-                    OutputType.SURFACE -> throw IllegalStateException(
-                        "Unsupported OutputType: $outputType"
-                    )
-                }
+                val outputKlass =
+                    when (outputType) {
+                        OutputType.SURFACE_TEXTURE -> SurfaceTexture::class.java
+                        OutputType.SURFACE_VIEW -> SurfaceHolder::class.java
+                        OutputType.SURFACE ->
+                            throw IllegalStateException("Unsupported OutputType: $outputType")
+                    }
                 configuration = Api26Compat.newOutputConfiguration(size, outputKlass)
             }
 
@@ -208,9 +206,10 @@
                     Api33Compat.setMirrorMode(configuration, mirrorMode.value)
                 } else {
                     if (mirrorMode != MirrorMode.MIRROR_MODE_AUTO) {
-                        Log.warn { "Cannot set mirrorMode to a non-default value on API " +
-                            "${Build.VERSION.SDK_INT}. This may result in unexpected behavior. " +
-                            "Requested $mirrorMode"
+                        Log.warn {
+                            "Cannot set mirrorMode to a non-default value on " +
+                                "API ${Build.VERSION.SDK_INT}. This may result in unexpected " +
+                                "behavior. Requested $mirrorMode"
                         }
                     }
                 }
@@ -221,10 +220,12 @@
                     Api33Compat.setTimestampBase(configuration, timestampBase.value)
                 } else {
                     if (timestampBase != TimestampBase.TIMESTAMP_BASE_DEFAULT) {
-                        Log.info { "The timestamp base on API ${Build.VERSION.SDK_INT} will " +
-                            "default to TIMESTAMP_BASE_DEFAULT, with which the camera device" +
-                            " adjusts timestamps based on the output target. " +
-                            "Requested $timestampBase" }
+                        Log.info {
+                            "The timestamp base on API ${Build.VERSION.SDK_INT} will " +
+                                "default to TIMESTAMP_BASE_DEFAULT, with which the camera device" +
+                                " adjusts timestamps based on the output target. " +
+                                "Requested $timestampBase"
+                        }
                     }
                 }
             }
@@ -243,9 +244,14 @@
                 }
             }
 
-            if (streamUseCase != null) {
+            if (streamUseCase != null && cameraId != null && camera2MetadataProvider != null) {
                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-                    Api33Compat.setStreamUseCase(configuration, streamUseCase.value)
+                    val cameraMetadata = camera2MetadataProvider.awaitCameraMetadata(cameraId)
+                    val availableStreamUseCases =
+                        Api33Compat.getAvailableStreamUseCases(cameraMetadata)
+                    if (availableStreamUseCases?.contains(streamUseCase.value) == true) {
+                        Api33Compat.setStreamUseCase(configuration, streamUseCase.value)
+                    }
                 }
             }
 
@@ -308,10 +314,11 @@
         get() = output.surfaceGroupId
 
     @Suppress("UNCHECKED_CAST")
-    override fun <T : Any> unwrapAs(type: KClass<T>): T? = when (type) {
-        OutputConfiguration::class -> output as T
-        else -> null
-    }
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? =
+        when (type) {
+            OutputConfiguration::class -> output as T
+            else -> null
+        }
 
     override fun toString(): String = output.toString()
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt
index 059f01a..7367a5f 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt
@@ -23,14 +23,12 @@
 import androidx.camera.camera2.pipe.core.Log
 
 /**
- * Thrown when an operation cannot be executed because underlying object is closed or in an
- * unusable state.
+ * Thrown when an operation cannot be executed because underlying object is closed or in an unusable
+ * state.
  */
 internal class ObjectUnavailableException(e: Throwable) : Exception(e)
 
-/**
- * Catch specific exceptions that are not normally thrown, log them, then rethrow.
- */
+/** Catch specific exceptions that are not normally thrown, log them, then rethrow. */
 @Throws(ObjectUnavailableException::class)
 internal inline fun <T> rethrowCamera2Exceptions(crossinline block: () -> T): T {
     // Camera2 has, at different points in time, thrown a large number of checked and/or
@@ -63,4 +61,4 @@
             else -> e
         }
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExternalRequestProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExternalRequestProcessor.kt
index 1fcd0e4..79ef27d 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExternalRequestProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExternalRequestProcessor.kt
@@ -46,9 +46,8 @@
     private val requestProcessor: RequestProcessor
 ) : CameraController {
     private val sequenceProcessor = ExternalCaptureSequenceProcessor(graphConfig, requestProcessor)
-    private val graphProcessor: GraphRequestProcessor = GraphRequestProcessor.from(
-        sequenceProcessor
-    )
+    private val graphProcessor: GraphRequestProcessor =
+        GraphRequestProcessor.from(sequenceProcessor)
     private var started = atomic(false)
 
     override fun start() {
@@ -103,18 +102,18 @@
             Log.warn { "Cannot create an ExternalCaptureSequence until Surfaces are available!" }
             return null
         }
-        val metadata = requests.map { request ->
-            val parameters = defaultParameters + request.parameters + requiredParameters
+        val metadata =
+            requests.map { request ->
+                val parameters = defaultParameters + request.parameters + requiredParameters
 
-            ExternalRequestMetadata(
-                graphConfig.defaultTemplate,
-                streamToSurfaceMap,
-                parameters,
-                isRepeating,
-                request,
-                RequestNumber(internalRequestNumbers.incrementAndGet())
-            )
-        }
+                ExternalRequestMetadata(
+                    graphConfig.defaultTemplate,
+                    streamToSurfaceMap,
+                    parameters,
+                    isRepeating,
+                    request,
+                    RequestNumber(internalRequestNumbers.incrementAndGet()))
+            }
 
         return ExternalCaptureSequence(
             graphConfig.camera,
@@ -124,8 +123,7 @@
             defaultParameters,
             requiredParameters,
             listeners,
-            sequenceListener
-        )
+            sequenceListener)
     }
 
     override fun submit(captureSequence: ExternalCaptureSequence): Int {
@@ -138,23 +136,20 @@
                 captureSequence.captureRequestList.single(),
                 captureSequence.defaultParameters,
                 captureSequence.requiredParameters,
-                captureSequence.listeners
-            )
+                captureSequence.listeners)
         } else {
             if (captureSequence.captureRequestList.size == 1) {
                 processor.submit(
                     captureSequence.captureRequestList.single(),
                     captureSequence.defaultParameters,
                     captureSequence.requiredParameters,
-                    captureSequence.listeners
-                )
+                    captureSequence.listeners)
             } else {
                 processor.submit(
                     captureSequence.captureRequestList,
                     captureSequence.defaultParameters,
                     captureSequence.requiredParameters,
-                    captureSequence.listeners
-                )
+                    captureSequence.listeners)
             }
         }
         return internalSequenceNumbers.incrementAndGet()
@@ -184,8 +179,7 @@
         override val listeners: List<Request.Listener>,
         override val sequenceListener: CaptureSequence.CaptureSequenceListener,
     ) : CaptureSequence<Request> {
-        @Volatile
-        private var _sequenceNumber: Int? = null
+        @Volatile private var _sequenceNumber: Int? = null
         override var sequenceNumber: Int
             get() {
                 if (_sequenceNumber == null) {
@@ -229,4 +223,4 @@
 
         override fun <T : Any> unwrapAs(type: KClass<T>): T? = null
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/FrameMetadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/FrameMetadata.kt
index 8a648c6..efbe87b 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/FrameMetadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/FrameMetadata.kt
@@ -31,15 +31,12 @@
 import androidx.camera.camera2.pipe.RequestMetadata
 import kotlin.reflect.KClass
 
-/**
- * An implementation of [FrameMetadata] that retrieves values from a [CaptureResult] object
- */
+/** An implementation of [FrameMetadata] that retrieves values from a [CaptureResult] object */
 @Suppress("SyntheticAccessor") // Using an inline class generates a synthetic constructor
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-internal class AndroidFrameMetadata constructor(
-    private val captureResult: CaptureResult,
-    override val camera: CameraId
-) : FrameMetadata {
+internal class AndroidFrameMetadata
+constructor(private val captureResult: CaptureResult, override val camera: CameraId) :
+    FrameMetadata {
     override fun <T> get(key: Metadata.Key<T>): T? = null
 
     override fun <T> getOrDefault(key: Metadata.Key<T>, default: T): T = default
@@ -56,23 +53,21 @@
     override val extraMetadata: Map<*, Any?> = emptyMap<Any, Any>()
 
     @Suppress("UNCHECKED_CAST")
-    override fun <T : Any> unwrapAs(type: KClass<T>): T? = when (type) {
-        CaptureResult::class -> captureResult as T
-        else -> null
-    }
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? =
+        when (type) {
+            CaptureResult::class -> captureResult as T
+            else -> null
+        }
 }
 
-/**
- * A version of [FrameMetadata] that can override (fix) metadata.
- */
+/** A version of [FrameMetadata] that can override (fix) metadata. */
 internal class CorrectedFrameMetadata(
     private var frameMetadata: FrameMetadata,
     override var extraMetadata: Map<*, Any?>
 ) : FrameMetadata {
 
     @Suppress("UNCHECKED_CAST")
-    override fun <T> get(key: Metadata.Key<T>): T? =
-        extraMetadata[key] as T? ?: frameMetadata[key]
+    override fun <T> get(key: Metadata.Key<T>): T? = extraMetadata[key] as T? ?: frameMetadata[key]
 
     override fun <T> getOrDefault(key: Metadata.Key<T>, default: T): T = get(key) ?: default
 
@@ -92,9 +87,7 @@
     override fun <T : Any> unwrapAs(type: KClass<T>): T? = frameMetadata.unwrapAs(type)
 }
 
-/**
- * An implementation of [FrameInfo] that retrieves values from a [TotalCaptureResult] object.
- */
+/** An implementation of [FrameInfo] that retrieves values from a [TotalCaptureResult] object. */
 @Suppress("SyntheticAccessor") // Using an inline class generates a synthetic constructor
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 internal class AndroidFrameInfo(
@@ -103,10 +96,7 @@
     override val requestMetadata: RequestMetadata
 ) : FrameInfo {
 
-    private val result = AndroidFrameMetadata(
-        totalCaptureResult,
-        camera
-    )
+    private val result = AndroidFrameMetadata(totalCaptureResult, camera)
     private val physicalResults: Map<CameraId, FrameMetadata>
 
     init {
@@ -118,11 +108,7 @@
                 val map = ArrayMap<CameraId, AndroidFrameMetadata>(physicalResults.size)
                 for (entry in physicalResults) {
                     val physicalCamera = CameraId(entry.key)
-                    map[physicalCamera] =
-                        AndroidFrameMetadata(
-                            entry.value,
-                            physicalCamera
-                        )
+                    map[physicalCamera] = AndroidFrameMetadata(entry.value, physicalCamera)
                 }
                 this.physicalResults = map
             } else {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpener.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpener.kt
index 671934d..4d79abc 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpener.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpener.kt
@@ -60,10 +60,10 @@
 }
 
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-internal class Camera2CameraOpener @Inject constructor(
-    private val cameraManager: Provider<CameraManager>,
-    private val threads: Threads
-) : CameraOpener {
+internal class Camera2CameraOpener
+@Inject
+constructor(private val cameraManager: Provider<CameraManager>, private val threads: Threads) :
+    CameraOpener {
 
     @SuppressLint(
         "MissingPermission", // Permissions are checked by calling methods.
@@ -73,67 +73,52 @@
         Debug.trace("CameraDevice-${cameraId.value}#openCamera") {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                 Api28Compat.openCamera(
-                    instance,
-                    cameraId.value,
-                    threads.camera2Executor,
-                    stateCallback
-                )
+                    instance, cameraId.value, threads.camera2Executor, stateCallback)
             } else {
-                instance.openCamera(
-                    cameraId.value,
-                    stateCallback,
-                    threads.camera2Handler
-                )
+                instance.openCamera(cameraId.value, stateCallback, threads.camera2Handler)
             }
         }
     }
 }
 
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-internal class Camera2CameraAvailabilityMonitor @Inject constructor(
-    private val cameraManager: Provider<CameraManager>,
-    private val threads: Threads
-) : CameraAvailabilityMonitor {
+internal class Camera2CameraAvailabilityMonitor
+@Inject
+constructor(private val cameraManager: Provider<CameraManager>, private val threads: Threads) :
+    CameraAvailabilityMonitor {
 
     override suspend fun awaitAvailableCamera(cameraId: CameraId, timeoutMillis: Long): Boolean =
-        withTimeoutOrNull(timeoutMillis) {
-            awaitAvailableCamera(cameraId)
-        } ?: false
+        withTimeoutOrNull(timeoutMillis) { awaitAvailableCamera(cameraId) } ?: false
 
     private suspend fun awaitAvailableCamera(cameraId: CameraId) =
         suspendCancellableCoroutine { continuation ->
-            val availabilityCallback = object : CameraManager.AvailabilityCallback() {
-                private val awaitComplete = atomic(false)
+            val availabilityCallback =
+                object : CameraManager.AvailabilityCallback() {
+                    private val awaitComplete = atomic(false)
 
-                override fun onCameraAvailable(cameraIdString: String) {
-                    if (cameraIdString == cameraId.value) {
-                        Log.debug { "$cameraId is now available." }
+                    override fun onCameraAvailable(cameraIdString: String) {
+                        if (cameraIdString == cameraId.value) {
+                            Log.debug { "$cameraId is now available." }
+                            if (awaitComplete.compareAndSet(expect = false, update = true)) {
+                                continuation.resume(true)
+                            }
+                        }
+                    }
+
+                    override fun onCameraAccessPrioritiesChanged() {
+                        Log.debug { "Access priorities changed." }
                         if (awaitComplete.compareAndSet(expect = false, update = true)) {
                             continuation.resume(true)
                         }
                     }
                 }
 
-                override fun onCameraAccessPrioritiesChanged() {
-                    Log.debug { "Access priorities changed." }
-                    if (awaitComplete.compareAndSet(expect = false, update = true)) {
-                        continuation.resume(true)
-                    }
-                }
-            }
-
             val manager = cameraManager.get()
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                 Api28Compat.registerAvailabilityCallback(
-                    manager,
-                    threads.camera2Executor,
-                    availabilityCallback
-                )
+                    manager, threads.camera2Executor, availabilityCallback)
             } else {
-                manager.registerAvailabilityCallback(
-                    availabilityCallback,
-                    threads.camera2Handler
-                )
+                manager.registerAvailabilityCallback(availabilityCallback, threads.camera2Handler)
             }
 
             continuation.invokeOnCancellation {
@@ -143,13 +128,14 @@
 }
 
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-internal class AndroidDevicePolicyManagerWrapper @Inject constructor(
-    private val devicePolicyManager: DevicePolicyManager
-) : DevicePolicyManagerWrapper {
+internal class AndroidDevicePolicyManagerWrapper
+@Inject
+constructor(private val devicePolicyManager: DevicePolicyManager) : DevicePolicyManagerWrapper {
     override val camerasDisabled: Boolean
-        get() = Debug.trace("DevicePolicyManager#getCameraDisabled") {
-            devicePolicyManager.getCameraDisabled(null)
-        }
+        get() =
+            Debug.trace("DevicePolicyManager#getCameraDisabled") {
+                devicePolicyManager.getCameraDisabled(null)
+            }
 }
 
 internal data class OpenCameraResult(
@@ -158,9 +144,11 @@
 )
 
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-internal class CameraStateOpener @Inject constructor(
+internal class CameraStateOpener
+@Inject
+constructor(
     private val cameraOpener: CameraOpener,
-    private val cameraMetadataProvider: CameraMetadataProvider,
+    private val camera2MetadataProvider: Camera2MetadataProvider,
     private val timeSource: TimeSource,
     private val cameraInteropConfig: CameraPipe.CameraInteropConfig?
 ) {
@@ -169,38 +157,32 @@
         attempts: Int,
         requestTimestamp: TimestampNs,
     ): OpenCameraResult {
-        val metadata = cameraMetadataProvider.getMetadata(cameraId)
-        val cameraState = AndroidCameraState(
-            cameraId,
-            metadata,
-            attempts,
-            requestTimestamp,
-            timeSource,
-            cameraInteropConfig?.cameraDeviceStateCallback,
-            cameraInteropConfig?.cameraSessionStateCallback
-        )
+        val metadata = camera2MetadataProvider.getCameraMetadata(cameraId)
+        val cameraState =
+            AndroidCameraState(
+                cameraId,
+                metadata,
+                attempts,
+                requestTimestamp,
+                timeSource,
+                cameraInteropConfig?.cameraDeviceStateCallback,
+                cameraInteropConfig?.cameraSessionStateCallback)
 
         try {
             cameraOpener.openCamera(cameraId, cameraState)
 
             // Suspend until we are no longer in a "starting" state.
-            val result = cameraState.state.first {
-                it !is CameraStateUnopened
-            }
+            val result = cameraState.state.first { it !is CameraStateUnopened }
             when (result) {
-                is CameraStateOpen ->
-                    return OpenCameraResult(cameraState = cameraState)
-
+                is CameraStateOpen -> return OpenCameraResult(cameraState = cameraState)
                 is CameraStateClosing -> {
                     cameraState.close()
                     return OpenCameraResult(errorCode = result.cameraErrorCode)
                 }
-
                 is CameraStateClosed -> {
                     cameraState.close()
                     return OpenCameraResult(errorCode = result.cameraErrorCode)
                 }
-
                 is CameraStateUnopened -> {
                     cameraState.close()
                     throw IllegalStateException("Unexpected CameraState: $result")
@@ -215,7 +197,9 @@
 }
 
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-internal class RetryingCameraStateOpener @Inject constructor(
+internal class RetryingCameraStateOpener
+@Inject
+constructor(
     private val cameraStateOpener: CameraStateOpener,
     private val cameraAvailabilityMonitor: CameraAvailabilityMonitor,
     private val timeSource: TimeSource,
@@ -249,13 +233,13 @@
                     return result
                 }
 
-                val willRetry = shouldRetry(
-                    errorCode,
-                    attempts,
-                    requestTimestamp,
-                    timeSource,
-                    devicePolicyManager.camerasDisabled
-                )
+                val willRetry =
+                    shouldRetry(
+                        errorCode,
+                        attempts,
+                        requestTimestamp,
+                        timeSource,
+                        devicePolicyManager.camerasDisabled)
                 // Always notify if the decision is to not retry the camera open, otherwise allow
                 // 1 open call to happen silently without generating an error, and notify about each
                 // error after that point.
@@ -310,7 +294,6 @@
                     } else {
                         true
                     }
-
                 CameraError.ERROR_CAMERA_LIMIT_EXCEEDED -> true
                 CameraError.ERROR_CAMERA_DISABLED ->
                     // The error indicates indicates that the current camera is currently disabled,
@@ -331,7 +314,6 @@
                     } else {
                         true
                     }
-
                 CameraError.ERROR_CAMERA_DEVICE -> true
                 CameraError.ERROR_CAMERA_SERVICE -> true
                 CameraError.ERROR_CAMERA_DISCONNECTED -> true
@@ -344,4 +326,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt
index de23f75..eae884e 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt
@@ -48,9 +48,13 @@
 import kotlinx.coroutines.launch
 
 internal sealed class CameraState
+
 internal object CameraStateUnopened : CameraState()
+
 internal data class CameraStateOpen(val cameraDevice: CameraDeviceWrapper) : CameraState()
+
 internal data class CameraStateClosing(val cameraErrorCode: CameraError? = null) : CameraState()
+
 internal data class CameraStateClosed(
     val cameraId: CameraId,
 
@@ -83,7 +87,6 @@
 internal enum class ClosedReason {
     APP_CLOSED,
     APP_DISCONNECTED,
-
     CAMERA2_CLOSED,
     CAMERA2_DISCONNECTED,
     CAMERA2_ERROR,
@@ -93,11 +96,11 @@
 /**
  * A [VirtualCamera] reflects and replays the state of a "Real" [CameraDevice.StateCallback].
  *
- * This behavior allows a virtual camera to be attached a [CameraDevice.StateCallback] and to
- * replay the open sequence. This behavior a camera manager to run multiple open attempts and to
- * recover from various classes of errors that will be invisible to the [VirtualCamera] by
- * allowing the [VirtualCamera] to be attached to the real camera after the camera is opened
- * successfully (Which may involve multiple calls to open).
+ * This behavior allows a virtual camera to be attached a [CameraDevice.StateCallback] and to replay
+ * the open sequence. This behavior a camera manager to run multiple open attempts and to recover
+ * from various classes of errors that will be invisible to the [VirtualCamera] by allowing the
+ * [VirtualCamera] to be attached to the real camera after the camera is opened successfully (Which
+ * may involve multiple calls to open).
  *
  * Disconnecting the VirtualCamera will cause an artificial close events to be generated on the
  * state property, but may not cause the underlying [CameraDevice] to be closed.
@@ -110,22 +113,18 @@
 
 internal val virtualCameraDebugIds = atomic(0)
 
-internal class VirtualCameraState(
-    val cameraId: CameraId
-) : VirtualCamera {
+internal class VirtualCameraState(val cameraId: CameraId) : VirtualCamera {
     private val debugId = virtualCameraDebugIds.incrementAndGet()
     private val lock = Any()
 
-    @GuardedBy("lock")
-    private var closed = false
+    @GuardedBy("lock") private var closed = false
 
     // This is intended so that it will only ever replay the most recent event to new subscribers,
     // but to never drop events for existing subscribers.
     private val _stateFlow = MutableSharedFlow<CameraState>(replay = 1, extraBufferCapacity = 3)
     private val _states = _stateFlow.distinctUntilChanged()
 
-    @GuardedBy("lock")
-    private var _lastState: CameraState = CameraStateUnopened
+    @GuardedBy("lock") private var _lastState: CameraState = CameraStateUnopened
     override val state: Flow<CameraState>
         get() = _states
 
@@ -147,15 +146,16 @@
                 return@coroutineScope
             }
 
-            job = launch(EmptyCoroutineContext) {
-                state.collect {
-                    synchronized(lock) {
-                        if (!closed) {
-                            emitState(it)
+            job =
+                launch(EmptyCoroutineContext) {
+                    state.collect {
+                        synchronized(lock) {
+                            if (!closed) {
+                                emitState(it)
+                            }
                         }
                     }
                 }
-            }
             this@VirtualCameraState.wakelockToken = wakelockToken
         }
     }
@@ -181,9 +181,7 @@
                     CameraStateClosed(
                         cameraId,
                         cameraClosedReason = ClosedReason.APP_DISCONNECTED,
-                        cameraErrorCode = lastCameraError
-                    )
-                )
+                        cameraErrorCode = lastCameraError))
             }
         }
     }
@@ -191,9 +189,7 @@
     @GuardedBy("lock")
     private fun emitState(state: CameraState) {
         _lastState = state
-        check(_stateFlow.tryEmit(state)) {
-            "Failed to emit $state in ${this@VirtualCameraState}"
-        }
+        check(_stateFlow.tryEmit(state)) { "Failed to emit $state in ${this@VirtualCameraState}" }
     }
 
     override fun toString(): String = "VirtualCamera-$debugId"
@@ -214,11 +210,9 @@
     private val debugId = androidCameraDebugIds.incrementAndGet()
     private val lock = Any()
 
-    @GuardedBy("lock")
-    private var opening = false
+    @GuardedBy("lock") private var opening = false
 
-    @GuardedBy("lock")
-    private var pendingClose: ClosingInfo? = null
+    @GuardedBy("lock") private var pendingClose: ClosingInfo? = null
 
     private val requestTimestampNanos: TimestampNs
     private var openTimestampNanos: TimestampNs? = null
@@ -239,19 +233,16 @@
 
     fun close() {
         val current = _state.value
-        val device = if (current is CameraStateOpen) {
-            current.cameraDevice
-        } else {
-            null
-        }
+        val device =
+            if (current is CameraStateOpen) {
+                current.cameraDevice
+            } else {
+                null
+            }
 
         closeWith(
             device?.unwrapAs(CameraDevice::class),
-            @Suppress("SyntheticAccessor")
-            ClosingInfo(
-                ClosedReason.APP_CLOSED
-            )
-        )
+            @Suppress("SyntheticAccessor") ClosingInfo(ClosedReason.APP_CLOSED))
     }
 
     suspend fun awaitClosed() {
@@ -293,20 +284,16 @@
 
         // Update _state.value _without_ holding the lock. This may block the calling thread for a
         // while if it synchronously calls createCaptureSession.
-        _state.value = CameraStateOpen(
-            AndroidCameraDevice(
-                metadata,
-                cameraDevice,
-                cameraId,
-                interopSessionStateCallback
-            )
-        )
+        _state.value =
+            CameraStateOpen(
+                AndroidCameraDevice(metadata, cameraDevice, cameraId, interopSessionStateCallback))
 
         // Check to see if we received close() or other events in the meantime.
-        val closeInfo = synchronized(lock) {
-            opening = false
-            pendingClose
-        }
+        val closeInfo =
+            synchronized(lock) {
+                opening = false
+                pendingClose
+            }
         if (closeInfo != null) {
             _state.value = CameraStateClosing(closeInfo.errorCode)
             cameraDevice.closeWithTrace()
@@ -325,9 +312,7 @@
             @Suppress("SyntheticAccessor")
             ClosingInfo(
                 ClosedReason.CAMERA2_DISCONNECTED,
-                errorCode = CameraError.ERROR_CAMERA_DISCONNECTED
-            )
-        )
+                errorCode = CameraError.ERROR_CAMERA_DISCONNECTED))
         interopDeviceStateCallback?.onDisconnected(cameraDevice)
         Debug.traceStop()
     }
@@ -340,11 +325,7 @@
         closeWith(
             cameraDevice,
             @Suppress("SyntheticAccessor")
-            ClosingInfo(
-                ClosedReason.CAMERA2_ERROR,
-                errorCode = CameraError.from(errorCode)
-            )
-        )
+            ClosingInfo(ClosedReason.CAMERA2_ERROR, errorCode = CameraError.from(errorCode)))
         interopDeviceStateCallback?.onError(cameraDevice, errorCode)
         Debug.traceStop()
     }
@@ -355,12 +336,7 @@
         Log.debug { "$cameraId: onClosed" }
 
         closeWith(
-            cameraDevice,
-            @Suppress("SyntheticAccessor")
-            ClosingInfo(
-                ClosedReason.CAMERA2_CLOSED
-            )
-        )
+            cameraDevice, @Suppress("SyntheticAccessor") ClosingInfo(ClosedReason.CAMERA2_CLOSED))
         interopDeviceStateCallback?.onClosed(cameraDevice)
         Debug.traceStop()
     }
@@ -380,33 +356,28 @@
             null,
             @Suppress("SyntheticAccessor")
             ClosingInfo(
-                ClosedReason.CAMERA2_EXCEPTION,
-                errorCode = cameraError,
-                exception = throwable
-            )
-        )
+                ClosedReason.CAMERA2_EXCEPTION, errorCode = cameraError, exception = throwable))
     }
 
-    private fun closeWith(
-        cameraDevice: CameraDevice?,
-        closeRequest: ClosingInfo
-    ) {
+    private fun closeWith(cameraDevice: CameraDevice?, closeRequest: ClosingInfo) {
         val currentState = _state.value
-        val cameraDeviceWrapper = if (currentState is CameraStateOpen) {
-            currentState.cameraDevice
-        } else {
-            null
-        }
-
-        val closeInfo = synchronized(lock) {
-            if (pendingClose == null) {
-                pendingClose = closeRequest
-                if (!opening) {
-                    return@synchronized closeRequest
-                }
+        val cameraDeviceWrapper =
+            if (currentState is CameraStateOpen) {
+                currentState.cameraDevice
+            } else {
+                null
             }
-            null
-        }
+
+        val closeInfo =
+            synchronized(lock) {
+                if (pendingClose == null) {
+                    pendingClose = closeRequest
+                    if (!opening) {
+                        return@synchronized closeRequest
+                    }
+                }
+                null
+            }
         if (closeInfo != null) {
             _state.value = CameraStateClosing(closeInfo.errorCode)
             cameraDeviceWrapper.closeWithTrace()
@@ -415,9 +386,7 @@
         }
     }
 
-    private fun computeClosedState(
-        closingInfo: ClosingInfo
-    ): CameraStateClosed {
+    private fun computeClosedState(closingInfo: ClosingInfo): CameraStateClosed {
         val now = Timestamps.now(timeSource)
         val openedTimestamp = openTimestampNanos
         val closingTimestamp = closingInfo.closingTimestamp
@@ -425,10 +394,11 @@
         val openDuration = openedTimestamp?.let { it - requestTimestampNanos }
 
         // opened -> closing (or now)
-        val activeDuration = when {
-            openedTimestamp == null -> null
-            else -> closingTimestamp - openedTimestamp
-        }
+        val activeDuration =
+            when {
+                openedTimestamp == null -> null
+                else -> closingTimestamp - openedTimestamp
+            }
 
         val closeDuration = closingTimestamp.let { now - it }
 
@@ -442,8 +412,7 @@
             cameraActiveDurationNs = activeDuration,
             cameraClosingDurationNs = closeDuration,
             cameraErrorCode = closingInfo.errorCode,
-            cameraException = closingInfo.exception
-        )
+            cameraException = closingInfo.exception)
     }
 
     private data class ClosingInfo(
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCameraManager.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCameraManager.kt
index 397e40d..a8e07d7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCameraManager.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCameraManager.kt
@@ -37,15 +37,15 @@
 import kotlinx.coroutines.launch
 
 internal sealed class CameraRequest
+
 internal data class RequestOpen(
     val virtualCamera: VirtualCameraState,
     val share: Boolean = false,
     val graphListener: GraphListener
 ) : CameraRequest()
 
-internal data class RequestClose(
-    val activeCamera: VirtualCameraManager.ActiveCamera
-) : CameraRequest()
+internal data class RequestClose(val activeCamera: VirtualCameraManager.ActiveCamera) :
+    CameraRequest()
 
 internal object RequestCloseAll : CameraRequest()
 
@@ -54,7 +54,9 @@
 @Suppress("EXPERIMENTAL_API_USAGE")
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 @Singleton
-internal class VirtualCameraManager @Inject constructor(
+internal class VirtualCameraManager
+@Inject
+constructor(
     private val permissions: Permissions,
     private val retryingCameraStateOpener: RetryingCameraStateOpener,
     private val threads: Threads
@@ -103,9 +105,7 @@
                     activeCameras.remove(closeRequest.activeCamera)
                 }
 
-                launch {
-                    closeRequest.activeCamera.close()
-                }
+                launch { closeRequest.activeCamera.close() }
                 closeRequest.activeCamera.awaitClosed()
                 continue
             }
@@ -123,9 +123,7 @@
 
                 // Close all active cameras.
                 for (activeCamera in activeCameras) {
-                    launch {
-                        activeCamera.close()
-                    }
+                    launch { activeCamera.close() }
                 }
                 for (camera in activeCameras) {
                     camera.awaitClosed()
@@ -152,11 +150,12 @@
             //   needed. Since close may block, we will re-evaluate the next request after the
             //   desired cameras are closed since new requests may have arrived.
             val cameraIdToOpen = request.virtualCamera.cameraId
-            val camerasToClose = if (request.share) {
-                emptyList()
-            } else {
-                activeCameras.filter { it.cameraId != cameraIdToOpen }
-            }
+            val camerasToClose =
+                if (request.share) {
+                    emptyList()
+                } else {
+                    activeCameras.filter { it.cameraId != cameraIdToOpen }
+                }
 
             if (camerasToClose.isNotEmpty()) {
                 // Shutdown of cameras should always happen first (and suspend until complete)
@@ -165,7 +164,8 @@
                     // TODO: This should be a dispatcher instead of scope.launch
 
                     launch {
-                        // TODO: Figure out if this should be blocking or not. If we are directly invoking
+                        // TODO: Figure out if this should be blocking or not. If we are directly
+                        // invoking
                         //   close this method could block for 0-1000ms
                         camera.close()
                     }
@@ -211,12 +211,9 @@
             return OpenVirtualCameraResult(lastCameraError = result.errorCode)
         }
         return OpenVirtualCameraResult(
-            activeCamera = ActiveCamera(
-                androidCameraState = result.cameraState,
-                scope = scope,
-                channel = requestQueue
-            )
-        )
+            activeCamera =
+                ActiveCamera(
+                    androidCameraState = result.cameraState, scope = scope, channel = requestQueue))
     }
 
     @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
@@ -243,23 +240,29 @@
         private val listenerJob: Job
         private var current: VirtualCameraState? = null
 
-        private val wakelock = WakeLock(
-            scope,
-            timeout = 1000,
-            callback = {
-                channel.trySend(RequestClose(this)).isSuccess
-            }
-        )
+        private val wakelock =
+            WakeLock(
+                scope,
+                timeout = 1000,
+                callback = { channel.trySend(RequestClose(this)).isSuccess },
+                // Every ActiveCamera is associated with an opened camera. We should ensure that we
+                // issue a RequestClose eventually for every ActiveCamera created.
+                //
+                // A notable bug is b/264396089 where, because camera opens took too long, we didn't
+                // acquire a WakeLockToken, and thereby not issuing the request to close camera
+                // eventually.
+                startTimeoutOnCreation = true)
 
         init {
-            listenerJob = scope.launch {
-                androidCameraState.state.collect {
-                    if (it is CameraStateClosing || it is CameraStateClosed) {
-                        wakelock.release()
-                        this.cancel()
+            listenerJob =
+                scope.launch {
+                    androidCameraState.state.collect {
+                        if (it is CameraStateClosing || it is CameraStateClosed) {
+                            wakelock.release()
+                            this.cancel()
+                        }
                     }
                 }
-            }
         }
 
         suspend fun connectTo(virtualCameraState: VirtualCameraState) {
@@ -284,7 +287,6 @@
     /**
      * There are 3 possible scenarios with [OpenVirtualCameraResult]. Suppose we denote the values
      * in pairs of ([activeCamera], [lastCameraError]):
-     *
      * - ([activeCamera], null): Camera opened without an issue.
      * - (null, [lastCameraError]): Camera opened failed and the last error was [lastCameraError].
      * - (null, null): Camera open didn't complete, likely due to CameraGraph being stopped or
@@ -294,4 +296,4 @@
         val activeCamera: ActiveCamera? = null,
         val lastCameraError: CameraError? = null
     )
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/Camera2Component.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/Camera2Component.kt
index 48a4ce9..99a3486 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/Camera2Component.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/Camera2Component.kt
@@ -28,8 +28,8 @@
 import androidx.camera.camera2.pipe.compat.Camera2CaptureSequenceProcessorFactory
 import androidx.camera.camera2.pipe.compat.Camera2CaptureSessionsModule
 import androidx.camera.camera2.pipe.compat.Camera2MetadataCache
+import androidx.camera.camera2.pipe.compat.Camera2MetadataProvider
 import androidx.camera.camera2.pipe.compat.CameraAvailabilityMonitor
-import androidx.camera.camera2.pipe.compat.CameraMetadataProvider
 import androidx.camera.camera2.pipe.compat.CameraOpener
 import androidx.camera.camera2.pipe.compat.StandardCamera2CaptureSequenceProcessorFactory
 import androidx.camera.camera2.pipe.core.Threads
@@ -44,23 +44,18 @@
 import kotlinx.coroutines.CoroutineScope
 
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@Module(
-    subcomponents = [
-        Camera2ControllerComponent::class
-    ]
-)
+@Module(subcomponents = [Camera2ControllerComponent::class])
 internal abstract class Camera2Module {
     @Binds
-    @CameraPipeCameraBackend
+    @DefaultCameraBackend
     abstract fun bindCameraPipeCameraBackend(camera2Backend: Camera2Backend): CameraBackend
 
-    @Binds
-    abstract fun bindCameraOpener(camera2CameraOpener: Camera2CameraOpener): CameraOpener
+    @Binds abstract fun bindCameraOpener(camera2CameraOpener: Camera2CameraOpener): CameraOpener
 
     @Binds
     abstract fun bindCameraMetadataProvider(
         camera2MetadataCache: Camera2MetadataCache
-    ): CameraMetadataProvider
+    ): Camera2MetadataProvider
 
     @Binds
     abstract fun bindCameraAvailabilityMonitor(
@@ -68,18 +63,16 @@
     ): CameraAvailabilityMonitor
 }
 
-@Scope
-internal annotation class Camera2ControllerScope
+@Scope internal annotation class Camera2ControllerScope
 
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 @Camera2ControllerScope
 @Subcomponent(
-    modules = [
-        Camera2ControllerConfig::class,
-        Camera2ControllerModule::class,
-        Camera2CaptureSessionsModule::class
-    ]
-)
+    modules =
+        [
+            Camera2ControllerConfig::class,
+            Camera2ControllerModule::class,
+            Camera2CaptureSessionsModule::class])
 internal interface Camera2ControllerComponent {
     fun cameraController(): CameraController
 
@@ -97,17 +90,13 @@
     private val graphListener: GraphListener,
     private val streamGraph: StreamGraph,
 ) {
-    @Provides
-    fun provideCameraGraphConfig() = graphConfig
+    @Provides fun provideCameraGraphConfig() = graphConfig
 
-    @Provides
-    fun provideCameraBackend() = cameraBackend
+    @Provides fun provideCameraBackend() = cameraBackend
 
-    @Provides
-    fun provideStreamGraph() = streamGraph as StreamGraphImpl
+    @Provides fun provideStreamGraph() = streamGraph as StreamGraphImpl
 
-    @Provides
-    fun provideGraphListener() = graphListener
+    @Provides fun provideGraphListener() = graphListener
 }
 
 @Module
@@ -127,8 +116,7 @@
         @Provides
         fun provideCoroutineScope(threads: Threads): CoroutineScope {
             return CoroutineScope(
-                threads.lightweightDispatcher.plus(CoroutineName("CXCP-Camera2Controller"))
-            )
+                threads.lightweightDispatcher.plus(CoroutineName("CXCP-Camera2Controller")))
         }
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraGraphComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraGraphComponent.kt
index b83853a..1468201 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraGraphComponent.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraGraphComponent.kt
@@ -18,6 +18,7 @@
 
 package androidx.camera.camera2.pipe.config
 
+import android.content.Context
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraBackend
 import androidx.camera.camera2.pipe.CameraBackends
@@ -42,20 +43,20 @@
 import kotlinx.coroutines.CoroutineName
 import kotlinx.coroutines.CoroutineScope
 
-@Scope
-internal annotation class CameraGraphScope
+@Scope internal annotation class CameraGraphScope
 
-@Qualifier
-internal annotation class ForCameraGraph
+@Qualifier internal annotation class ForCameraGraph
+
+@Qualifier internal annotation class CameraGraphContext
 
 @CameraGraphScope
 @Subcomponent(
-    modules = [
-        SharedCameraGraphModules::class,
-        InternalCameraGraphModules::class,
-        CameraGraphConfigModule::class,
-    ]
-)
+    modules =
+        [
+            SharedCameraGraphModules::class,
+            InternalCameraGraphModules::class,
+            CameraGraphConfigModule::class,
+        ])
 internal interface CameraGraphComponent {
     fun cameraGraph(): CameraGraph
 
@@ -67,32 +68,28 @@
 }
 
 @Module
-internal class CameraGraphConfigModule(
-    private val config: CameraGraph.Config
-) {
-    @Provides
-    fun provideCameraGraphConfig(): CameraGraph.Config = config
+internal class CameraGraphConfigModule(private val config: CameraGraph.Config) {
+    @Provides fun provideCameraGraphConfig(): CameraGraph.Config = config
 }
 
 @Module
 internal abstract class SharedCameraGraphModules {
-    @Binds
-    abstract fun bindCameraGraph(cameraGraph: CameraGraphImpl): CameraGraph
+    @Binds abstract fun bindCameraGraph(cameraGraph: CameraGraphImpl): CameraGraph
+
+    @Binds abstract fun bindGraphProcessor(graphProcessor: GraphProcessorImpl): GraphProcessor
+
+    @Binds abstract fun bindGraphListener(graphProcessor: GraphProcessorImpl): GraphListener
 
     @Binds
-    abstract fun bindGraphProcessor(graphProcessor: GraphProcessorImpl): GraphProcessor
-
-    @Binds
-    abstract fun bindGraphListener(graphProcessor: GraphProcessorImpl): GraphListener
+    @CameraGraphContext
+    abstract fun bindCameraGraphContext(@CameraPipeContext cameraPipeContext: Context): Context
 
     companion object {
         @CameraGraphScope
         @Provides
         @ForCameraGraph
         fun provideCameraGraphCoroutineScope(threads: Threads): CoroutineScope {
-            return CoroutineScope(
-                threads.lightweightDispatcher.plus(CoroutineName("CXCP-Graph"))
-            )
+            return CoroutineScope(threads.lightweightDispatcher.plus(CoroutineName("CXCP-Graph")))
         }
 
         @CameraGraphScope
@@ -146,7 +143,9 @@
         ): CameraMetadata {
             // TODO: It might be a good idea to cache and go through caches for some of these calls
             //   instead of reading it directly from the backend.
-            return cameraBackend.readCameraMetadata(graphConfig.camera)
+            return checkNotNull(cameraBackend.awaitCameraMetadata(graphConfig.camera)) {
+                "Failed to load metadata for ${graphConfig.camera}!"
+            }
         }
 
         @CameraGraphScope
@@ -159,11 +158,7 @@
             streamGraph: StreamGraphImpl,
         ): CameraController {
             return cameraBackend.createCameraController(
-                cameraContext,
-                graphConfig,
-                graphProcessor,
-                streamGraph
-            )
+                cameraContext, graphConfig, graphProcessor, streamGraph)
         }
     }
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt
index d718f2b..9f99f9e 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt
@@ -32,13 +32,13 @@
 import androidx.camera.camera2.pipe.CameraPipe.CameraMetadataConfig
 import androidx.camera.camera2.pipe.CameraSurfaceManager
 import androidx.camera.camera2.pipe.compat.AndroidDevicePolicyManagerWrapper
-import androidx.camera.camera2.pipe.compat.Camera2CameraDevices
 import androidx.camera.camera2.pipe.compat.DevicePolicyManagerWrapper
 import androidx.camera.camera2.pipe.core.Debug
 import androidx.camera.camera2.pipe.core.SystemTimeSource
 import androidx.camera.camera2.pipe.core.Threads
 import androidx.camera.camera2.pipe.core.TimeSource
 import androidx.camera.camera2.pipe.internal.CameraBackendsImpl
+import androidx.camera.camera2.pipe.internal.CameraDevicesImpl
 import dagger.Binds
 import dagger.Component
 import dagger.Module
@@ -48,30 +48,28 @@
 import javax.inject.Qualifier
 import javax.inject.Singleton
 
-@Qualifier
-internal annotation class CameraPipeCameraBackend
+@Qualifier internal annotation class DefaultCameraBackend
+
+/** Qualifier for requesting the CameraPipe scoped Context object */
+@Qualifier internal annotation class CameraPipeContext
 
 @Singleton
 @Component(
-    modules = [
-        CameraPipeConfigModule::class,
-        CameraPipeModules::class,
-        Camera2Module::class,
-    ]
-)
+    modules =
+        [
+            CameraPipeConfigModule::class,
+            CameraPipeModules::class,
+            Camera2Module::class,
+        ])
 internal interface CameraPipeComponent {
     fun cameraGraphComponentBuilder(): CameraGraphComponent.Builder
     fun cameras(): CameraDevices
     fun cameraSurfaceManager(): CameraSurfaceManager
 }
 
-@Module(
-    includes = [ThreadConfigModule::class],
-    subcomponents = [CameraGraphComponent::class]
-)
+@Module(includes = [ThreadConfigModule::class], subcomponents = [CameraGraphComponent::class])
 internal class CameraPipeConfigModule(private val config: CameraPipe.Config) {
-    @Provides
-    fun provideCameraPipeConfig(): CameraPipe.Config = config
+    @Provides fun provideCameraPipeConfig(): CameraPipe.Config = config
 
     @Provides
     fun provideCameraInteropConfig(
@@ -83,14 +81,13 @@
 
 @Module
 internal abstract class CameraPipeModules {
-    @Binds
-    abstract fun bindCameras(impl: Camera2CameraDevices): CameraDevices
+    @Binds abstract fun bindCameras(impl: CameraDevicesImpl): CameraDevices
 
-    @Binds
-    abstract fun bindTimeSource(timeSource: SystemTimeSource): TimeSource
+    @Binds abstract fun bindTimeSource(timeSource: SystemTimeSource): TimeSource
 
     companion object {
         @Provides
+        @CameraPipeContext
         fun provideContext(config: CameraPipe.Config): Context = config.appContext
 
         @Provides
@@ -99,25 +96,28 @@
 
         @Reusable
         @Provides
-        fun provideCameraManager(context: Context): CameraManager =
-            context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+        fun provideCameraManager(@CameraPipeContext cameraPipeContext: Context): CameraManager =
+            cameraPipeContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager
 
         @Reusable
         @Provides
-        fun provideDevicePolicyManagerWrapper(context: Context): DevicePolicyManagerWrapper =
-            AndroidDevicePolicyManagerWrapper(
-                context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
-            )
+        fun provideDevicePolicyManagerWrapper(
+            @CameraPipeContext cameraPipeContext: Context
+        ): DevicePolicyManagerWrapper {
+            val devicePolicyService =
+                cameraPipeContext.getSystemService(Context.DEVICE_POLICY_SERVICE)
+            return AndroidDevicePolicyManagerWrapper(devicePolicyService as DevicePolicyManager)
+        }
 
         @Singleton
         @Provides
         fun provideCameraContext(
-            context: Context,
+            @CameraPipeContext cameraPipeContext: Context,
             threads: Threads,
             cameraBackends: CameraBackends
         ): CameraContext =
             object : CameraContext {
-                override val appContext: Context = context
+                override val appContext: Context = cameraPipeContext
                 override val threads: Threads = threads
                 override val cameraBackends: CameraBackends = cameraBackends
             }
@@ -126,16 +126,15 @@
         @Provides
         fun provideCameraBackends(
             config: CameraPipe.Config,
-            @CameraPipeCameraBackend cameraPipeCameraBackend: Provider<CameraBackend>,
-            appContext: Context,
+            @DefaultCameraBackend defaultCameraBackend: Provider<CameraBackend>,
+            @CameraPipeContext cameraPipeContext: Context,
             threads: Threads,
         ): CameraBackends {
             // This is intentionally lazy. If an internalBackend is defined as part of the
             // CameraPipe configuration, we will never create the default cameraPipeCameraBackend.
-            val internalBackend = config.cameraBackendConfig.internalBackend
-                ?: Debug.trace("Initialize cameraPipeCameraBackend") {
-                    cameraPipeCameraBackend.get()
-                }
+            val internalBackend =
+                config.cameraBackendConfig.internalBackend
+                    ?: Debug.trace("Initialize defaultCameraBackend") { defaultCameraBackend.get() }
 
             // Make sure that the list of additional backends does not contain the
             check(!config.cameraBackendConfig.cameraBackends.containsKey(internalBackend.id)) {
@@ -151,7 +150,7 @@
                 "Failed to find $defaultBackendId in the list of available CameraPipe backends! " +
                     "Available values are ${allBackends.keys}"
             }
-            return CameraBackendsImpl(defaultBackendId, allBackends, appContext, threads)
+            return CameraBackendsImpl(defaultBackendId, allBackends, cameraPipeContext, threads)
         }
     }
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/ExternalCameraGraphComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/ExternalCameraGraphComponent.kt
index 33ecf4a..e54e05f 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/ExternalCameraGraphComponent.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/ExternalCameraGraphComponent.kt
@@ -32,12 +32,7 @@
 import dagger.Subcomponent
 
 @CameraGraphScope
-@Subcomponent(
-    modules = [
-        SharedCameraGraphModules::class,
-        ExternalCameraGraphConfigModule::class
-    ]
-)
+@Subcomponent(modules = [SharedCameraGraphModules::class, ExternalCameraGraphConfigModule::class])
 internal interface ExternalCameraGraphComponent {
     fun cameraGraph(): CameraGraph
 
@@ -54,18 +49,12 @@
     private val cameraMetadata: CameraMetadata,
     private val requestProcessor: RequestProcessor
 ) {
-    @Provides
-    fun provideCameraGraphConfig(): CameraGraph.Config = config
+    @Provides fun provideCameraGraphConfig(): CameraGraph.Config = config
 
-    @Provides
-    fun provideCameraMetadata(): CameraMetadata = cameraMetadata
+    @Provides fun provideCameraMetadata(): CameraMetadata = cameraMetadata
 
     @CameraGraphScope
     @Provides
     fun provideGraphController(graphListener: GraphListener): CameraController =
-        ExternalCameraController(
-            config,
-            graphListener,
-            requestProcessor
-        )
+        ExternalCameraController(config, graphListener, requestProcessor)
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/ExternalCameraPipeComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/ExternalCameraPipeComponent.kt
index 57417e3..233200c 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/ExternalCameraPipeComponent.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/ExternalCameraPipeComponent.kt
@@ -22,11 +22,7 @@
 
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 @Singleton
-@Component(
-    modules = [
-        ThreadConfigModule::class
-    ]
-)
+@Component(modules = [ThreadConfigModule::class])
 internal interface ExternalCameraPipeComponent {
     fun cameraGraphBuilder(): ExternalCameraGraphComponent.Builder
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/ThreadConfigModule.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/ThreadConfigModule.kt
index a7327a0..b4a6898 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/ThreadConfigModule.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/ThreadConfigModule.kt
@@ -38,9 +38,7 @@
 import kotlinx.coroutines.asCoroutineDispatcher
 import kotlinx.coroutines.asExecutor
 
-/**
- * Configure and provide a single [Threads] object to other parts of the library.
- */
+/** Configure and provide a single [Threads] object to other parts of the library. */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 @Module
 internal class ThreadConfigModule(private val threadConfig: CameraPipe.ThreadConfig) {
@@ -76,46 +74,45 @@
         }
 
         val blockingExecutor =
-            threadConfig.defaultBlockingExecutor ?: AndroidThreads.factory
-                .withPrefix("CXCP-IO-")
-                .withAndroidPriority(defaultThreadPriority)
-                .asCachedThreadPool()
+            threadConfig.defaultBlockingExecutor
+                ?: AndroidThreads.factory
+                    .withPrefix("CXCP-IO-")
+                    .withAndroidPriority(defaultThreadPriority)
+                    .asCachedThreadPool()
         val blockingDispatcher = blockingExecutor.asCoroutineDispatcher()
 
         val backgroundExecutor =
-            threadConfig.defaultBackgroundExecutor ?: AndroidThreads.factory
-                .withPrefix("CXCP-BG-")
-                .withAndroidPriority(defaultThreadPriority)
-                .asScheduledThreadPool(backgroundThreadCount)
+            threadConfig.defaultBackgroundExecutor
+                ?: AndroidThreads.factory
+                    .withPrefix("CXCP-BG-")
+                    .withAndroidPriority(defaultThreadPriority)
+                    .asScheduledThreadPool(backgroundThreadCount)
         val backgroundDispatcher = backgroundExecutor.asCoroutineDispatcher()
 
         val lightweightExecutor =
-            threadConfig.defaultLightweightExecutor ?: AndroidThreads.factory
-                .withPrefix("CXCP-")
-                .withAndroidPriority(cameraThreadPriority)
-                .asScheduledThreadPool(lightweightThreadCount)
+            threadConfig.defaultLightweightExecutor
+                ?: AndroidThreads.factory
+                    .withPrefix("CXCP-")
+                    .withAndroidPriority(cameraThreadPriority)
+                    .asScheduledThreadPool(lightweightThreadCount)
         val lightweightDispatcher = lightweightExecutor.asCoroutineDispatcher()
 
-        val cameraHandlerFn =
-            {
-                val handlerThread = threadConfig.defaultCameraHandler ?: HandlerThread(
-                    "CXCP-Camera-H",
-                    cameraThreadPriority
-                ).also {
-                    it.start()
-                }
-                Handler(handlerThread.looper)
-            }
+        val cameraHandlerFn = {
+            val handlerThread =
+                threadConfig.defaultCameraHandler
+                    ?: HandlerThread("CXCP-Camera-H", cameraThreadPriority).also { it.start() }
+            Handler(handlerThread.looper)
+        }
         val cameraExecutorFn = {
-            threadConfig.defaultCameraExecutor ?: AndroidThreads.factory
-                .withPrefix("CXCP-Camera-E")
-                .withAndroidPriority(cameraThreadPriority)
-                .asFixedSizeThreadPool(1)
+            threadConfig.defaultCameraExecutor
+                ?: AndroidThreads.factory
+                    .withPrefix("CXCP-Camera-E")
+                    .withAndroidPriority(cameraThreadPriority)
+                    .asFixedSizeThreadPool(1)
         }
 
-        val globalScope = CoroutineScope(
-            SupervisorJob() + lightweightDispatcher + CoroutineName("CXCP")
-        )
+        val globalScope =
+            CoroutineScope(SupervisorJob() + lightweightDispatcher + CoroutineName("CXCP"))
 
         return Threads(
             globalScope = globalScope,
@@ -126,8 +123,7 @@
             lightweightExecutor = lightweightExecutor,
             lightweightDispatcher = lightweightDispatcher,
             camera2Handler = cameraHandlerFn,
-            camera2Executor = cameraExecutorFn
-        )
+            camera2Executor = cameraExecutorFn)
     }
 
     private fun provideTestOnlyThreads(
@@ -138,12 +134,8 @@
 
         // TODO: This should delegate to the testDispatcher instead of using a HandlerThread.
         val cameraHandlerFn = {
-            val handlerThread = HandlerThread(
-                "CXCP-Camera-H",
-                cameraThreadPriority
-            ).also {
-                it.start()
-            }
+            val handlerThread =
+                HandlerThread("CXCP-Camera-H", cameraThreadPriority).also { it.start() }
             Handler(handlerThread.looper)
         }
 
@@ -156,7 +148,6 @@
             lightweightExecutor = testExecutor,
             lightweightDispatcher = testDispatcher,
             camera2Handler = cameraHandlerFn,
-            camera2Executor = { testExecutor }
-        )
+            camera2Executor = { testExecutor })
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/AndroidThreads.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/AndroidThreads.kt
index aa8b68c..7b7c2a7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/AndroidThreads.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/AndroidThreads.kt
@@ -38,30 +38,32 @@
      * - 5 is mapped to 0 (default)
      * - 10 is mapped to -8 (urgent display)
      */
-    private val NICE_VALUES = intArrayOf(
-        Process.THREAD_PRIORITY_LOWEST, // 1 (Thread.MIN_PRIORITY)
-        Process.THREAD_PRIORITY_BACKGROUND + 6,
-        Process.THREAD_PRIORITY_BACKGROUND + 3,
-        Process.THREAD_PRIORITY_BACKGROUND,
-        Process.THREAD_PRIORITY_DEFAULT, // 5 (Thread.NORM_PRIORITY)
-        Process.THREAD_PRIORITY_DEFAULT - 2,
-        Process.THREAD_PRIORITY_DEFAULT - 4,
-        Process.THREAD_PRIORITY_URGENT_DISPLAY + 3,
-        Process.THREAD_PRIORITY_URGENT_DISPLAY + 2,
-        Process.THREAD_PRIORITY_URGENT_DISPLAY // 10 (Thread.MAX_PRIORITY)
-    )
+    private val NICE_VALUES =
+        intArrayOf(
+            Process.THREAD_PRIORITY_LOWEST, // 1 (Thread.MIN_PRIORITY)
+            Process.THREAD_PRIORITY_BACKGROUND + 6,
+            Process.THREAD_PRIORITY_BACKGROUND + 3,
+            Process.THREAD_PRIORITY_BACKGROUND,
+            Process.THREAD_PRIORITY_DEFAULT, // 5 (Thread.NORM_PRIORITY)
+            Process.THREAD_PRIORITY_DEFAULT - 2,
+            Process.THREAD_PRIORITY_DEFAULT - 4,
+            Process.THREAD_PRIORITY_URGENT_DISPLAY + 3,
+            Process.THREAD_PRIORITY_URGENT_DISPLAY + 2,
+            Process.THREAD_PRIORITY_URGENT_DISPLAY // 10 (Thread.MAX_PRIORITY)
+            )
 
     public val factory: ThreadFactory = Executors.defaultThreadFactory()
 
-    /** Wraps `delegate` such that the threads created by it are set to `priority`.  */
+    /** Wraps `delegate` such that the threads created by it are set to `priority`. */
     fun ThreadFactory.withAndroidPriority(androidPriority: Int): ThreadFactory {
         return ThreadFactory { runnable ->
             val javaPriority = androidToJavaPriority(androidPriority)
-            val thread: Thread = this.newThread {
-                // Set the Android thread priority once the thread actually starts running.
-                Process.setThreadPriority(androidPriority)
-                runnable.run()
-            }
+            val thread: Thread =
+                this.newThread {
+                    // Set the Android thread priority once the thread actually starts running.
+                    Process.setThreadPriority(androidPriority)
+                    runnable.run()
+                }
 
             // Setting java priority internally sets the android priority, but not vice versa.
             // By setting the java priority here, we ensure that the priority is set to the same or
@@ -87,27 +89,20 @@
         }
     }
 
-    /**
-     * Create a new fixed size thread pool using [Executors.newFixedThreadPool].
-     */
+    /** Create a new fixed size thread pool using [Executors.newFixedThreadPool]. */
     fun ThreadFactory.asFixedSizeThreadPool(threads: Int): ExecutorService {
         require(threads > 0) { "Threads ($threads) must be > 0" }
         return Executors.newFixedThreadPool(threads, this)
     }
 
-    /**
-     * Create a new scheduled thread pool using [Executors.newScheduledThreadPool].
-     */
+    /** Create a new scheduled thread pool using [Executors.newScheduledThreadPool]. */
     fun ThreadFactory.asScheduledThreadPool(threads: Int): ScheduledExecutorService {
         require(threads > 0) { "Threads ($threads) must be > 0" }
         return Executors.newScheduledThreadPool(threads, this)
     }
 
-    /**
-     * Create a new cached thread pool using [Executors.newCachedThreadPool].
-     */
-    fun ThreadFactory.asCachedThreadPool(): ExecutorService =
-        Executors.newCachedThreadPool(this)
+    /** Create a new cached thread pool using [Executors.newCachedThreadPool]. */
+    fun ThreadFactory.asCachedThreadPool(): ExecutorService = Executors.newCachedThreadPool(this)
 
     private fun androidToJavaPriority(androidPriority: Int): Int {
         // Err on the side of increased priority.
@@ -118,4 +113,4 @@
         }
         return Thread.MAX_PRIORITY
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt
index c39cd6b..f739c99 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt
@@ -31,17 +31,15 @@
 import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.CameraMetadata
 
-/**
- * Internal debug utilities, constants, and checks.
- */
+/** Internal debug utilities, constants, and checks. */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public object Debug {
     public const val ENABLE_LOGGING: Boolean = true
     public const val ENABLE_TRACING: Boolean = true
 
     /**
-     * Wrap the specified [block] in calls to [Trace.beginSection] (with the supplied [label])
-     * and [Trace.endSection].
+     * Wrap the specified [block] in calls to [Trace.beginSection] (with the supplied [label]) and
+     * [Trace.endSection].
      *
      * @param label A name of the code section to appear in the trace.
      * @param block A block of code which is being traced.
@@ -55,18 +53,14 @@
         }
     }
 
-    /**
-     * Forwarding call to [Trace.beginSection] that can be statically disabled at compile time.
-     */
+    /** Forwarding call to [Trace.beginSection] that can be statically disabled at compile time. */
     public inline fun traceStart(crossinline label: () -> String) {
         if (ENABLE_TRACING) {
             Trace.beginSection(label())
         }
     }
 
-    /**
-     * Forwarding call to [Trace.endSection] that can be statically disabled at compile time.
-     */
+    /** Forwarding call to [Trace.endSection] that can be statically disabled at compile time. */
     public inline fun traceStop() {
         if (ENABLE_TRACING) {
             Trace.endSection()
@@ -79,17 +73,18 @@
                 append("$name: (None)\n")
             } else {
                 append("${name}\n")
-                val parametersString: List<Pair<String, Any?>> = parameters.map {
-                    when (val key = it.key) {
-                        is CameraCharacteristics.Key<*> -> key.name
-                        is CaptureRequest.Key<*> -> key.name
-                        is CaptureResult.Key<*> -> key.name
-                        else -> key.toString()
-                    } to it.value
-                }
-                parametersString.sortedBy { it.first }.forEach {
-                    append("  ${it.first.padEnd(50, ' ')}${it.second}\n")
-                }
+                val parametersString: List<Pair<String, Any?>> =
+                    parameters.map {
+                        when (val key = it.key) {
+                            is CameraCharacteristics.Key<*> -> key.name
+                            is CaptureRequest.Key<*> -> key.name
+                            is CaptureResult.Key<*> -> key.name
+                            else -> key.toString()
+                        } to it.value
+                    }
+                parametersString
+                    .sortedBy { it.first }
+                    .forEach { append("  ${it.first.padEnd(50, ' ')}${it.second}\n") }
             }
         }
     }
@@ -99,61 +94,65 @@
         graphConfig: CameraGraph.Config,
         cameraGraph: CameraGraph
     ): String {
-        val lensFacing = when (metadata[LENS_FACING]) {
-            CameraCharacteristics.LENS_FACING_FRONT -> "Front"
-            CameraCharacteristics.LENS_FACING_BACK -> "Back"
-            CameraCharacteristics.LENS_FACING_EXTERNAL -> "External"
-            else -> "Unknown"
-        }
-
-        val operatingMode = when (graphConfig.sessionMode) {
-            CameraGraph.OperatingMode.HIGH_SPEED -> "High Speed"
-            CameraGraph.OperatingMode.NORMAL -> "Normal"
-        }
-
-        val capabilities = metadata[REQUEST_AVAILABLE_CAPABILITIES]
-        val cameraType = if (capabilities != null &&
-            capabilities.contains(REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)
-        ) {
-            "Logical"
-        } else {
-            "Physical"
-        }
-
-        return StringBuilder().apply {
-            append("$cameraGraph (Camera ${graphConfig.camera.value})\n")
-            append("  Facing:    $lensFacing ($cameraType)\n")
-            append("  Mode:      $operatingMode\n")
-            append("Outputs:\n")
-            for (stream in cameraGraph.streams.streams) {
-                stream.outputs.forEachIndexed { i, output ->
-                    append("  ")
-                    val streamId = if (i == 0) output.stream.id.toString() else ""
-                    append(streamId.padEnd(10, ' '))
-                    append(output.id.toString().padEnd(10, ' '))
-                    append(output.size.toString().padEnd(12, ' '))
-                    append(output.format.name.padEnd(16, ' '))
-                    output.mirrorMode?.let { append(" [$it]") }
-                    output.timestampBase?.let { append(" [$it]") }
-                    output.dynamicRangeProfile?.let { append(" [$it]") }
-                    output.streamUseCase?.let { append(" [$it]") }
-                    if (output.camera != graphConfig.camera) {
-                        append(" [")
-                        append(output.camera)
-                        append("]")
-                    }
-                    append("\n")
-                }
+        val lensFacing =
+            when (metadata[LENS_FACING]) {
+                CameraCharacteristics.LENS_FACING_FRONT -> "Front"
+                CameraCharacteristics.LENS_FACING_BACK -> "Back"
+                CameraCharacteristics.LENS_FACING_EXTERNAL -> "External"
+                else -> "Unknown"
             }
 
-            append("Session Template: ${graphConfig.sessionTemplate.name}\n")
-            appendParameters(this, "Session Parameters", graphConfig.sessionParameters)
+        val operatingMode =
+            when (graphConfig.sessionMode) {
+                CameraGraph.OperatingMode.HIGH_SPEED -> "High Speed"
+                CameraGraph.OperatingMode.NORMAL -> "Normal"
+            }
 
-            append("Default Template: ${graphConfig.defaultTemplate.name}\n")
-            appendParameters(this, "Default Parameters", graphConfig.defaultParameters)
+        val capabilities = metadata[REQUEST_AVAILABLE_CAPABILITIES]
+        val cameraType =
+            if (capabilities != null &&
+                capabilities.contains(REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)) {
+                "Logical"
+            } else {
+                "Physical"
+            }
 
-            appendParameters(this, "Required Parameters", graphConfig.requiredParameters)
-        }.toString()
+        return StringBuilder()
+            .apply {
+                append("$cameraGraph (Camera ${graphConfig.camera.value})\n")
+                append("  Facing:    $lensFacing ($cameraType)\n")
+                append("  Mode:      $operatingMode\n")
+                append("Outputs:\n")
+                for (stream in cameraGraph.streams.streams) {
+                    stream.outputs.forEachIndexed { i, output ->
+                        append("  ")
+                        val streamId = if (i == 0) output.stream.id.toString() else ""
+                        append(streamId.padEnd(10, ' '))
+                        append(output.id.toString().padEnd(10, ' '))
+                        append(output.size.toString().padEnd(12, ' '))
+                        append(output.format.name.padEnd(16, ' '))
+                        output.mirrorMode?.let { append(" [$it]") }
+                        output.timestampBase?.let { append(" [$it]") }
+                        output.dynamicRangeProfile?.let { append(" [$it]") }
+                        output.streamUseCase?.let { append(" [$it]") }
+                        if (output.camera != graphConfig.camera) {
+                            append(" [")
+                            append(output.camera)
+                            append("]")
+                        }
+                        append("\n")
+                    }
+                }
+
+                append("Session Template: ${graphConfig.sessionTemplate.name}\n")
+                appendParameters(this, "Session Parameters", graphConfig.sessionParameters)
+
+                append("Default Template: ${graphConfig.defaultTemplate.name}\n")
+                appendParameters(this, "Default Parameters", graphConfig.defaultParameters)
+
+                appendParameters(this, "Required Parameters", graphConfig.requiredParameters)
+            }
+            .toString()
     }
 }
 
@@ -169,31 +168,25 @@
 }
 
 /** Asserts that this method was invoked on Android L (API 21) or higher. */
-public inline fun checkLOrHigher(methodName: String): Unit = checkApi(
-    Build.VERSION_CODES.LOLLIPOP, methodName
-)
+public inline fun checkLOrHigher(methodName: String): Unit =
+    checkApi(Build.VERSION_CODES.LOLLIPOP, methodName)
 
 /** Asserts that this method was invoked on Android M (API 23) or higher. */
-public inline fun checkMOrHigher(methodName: String): Unit = checkApi(
-    Build.VERSION_CODES.M, methodName
-)
+public inline fun checkMOrHigher(methodName: String): Unit =
+    checkApi(Build.VERSION_CODES.M, methodName)
 
 /** Asserts that this method was invoked on Android N (API 24) or higher. */
-public inline fun checkNOrHigher(methodName: String): Unit = checkApi(
-    Build.VERSION_CODES.N, methodName
-)
+public inline fun checkNOrHigher(methodName: String): Unit =
+    checkApi(Build.VERSION_CODES.N, methodName)
 
 /** Asserts that this method was invoked on Android O (API 26) or higher. */
-public inline fun checkOOrHigher(methodName: String): Unit = checkApi(
-    Build.VERSION_CODES.O, methodName
-)
+public inline fun checkOOrHigher(methodName: String): Unit =
+    checkApi(Build.VERSION_CODES.O, methodName)
 
 /** Asserts that this method was invoked on Android P (API 28) or higher. */
-public inline fun checkPOrHigher(methodName: String): Unit = checkApi(
-    Build.VERSION_CODES.P, methodName
-)
+public inline fun checkPOrHigher(methodName: String): Unit =
+    checkApi(Build.VERSION_CODES.P, methodName)
 
 /** Asserts that this method was invoked on Android Q (API 29) or higher. */
-public inline fun checkQOrHigher(methodName: String): Unit = checkApi(
-    Build.VERSION_CODES.Q, methodName
-)
+public inline fun checkQOrHigher(methodName: String): Unit =
+    checkApi(Build.VERSION_CODES.Q, methodName)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Log.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Log.kt
index 09d3ad1..0f301fd 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Log.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Log.kt
@@ -20,11 +20,11 @@
 import androidx.annotation.RequiresApi
 
 /**
- * This object provides a set of common log functions that are optimized for CameraPipe with
- * options to control which log levels are available to log at compile time via const val's.
+ * This object provides a set of common log functions that are optimized for CameraPipe with options
+ * to control which log levels are available to log at compile time via const val's.
  *
- * Log functions have been designed so that printing variables and doing string concatenation
- * will not occur if the log level is disabled, which leads to slightly unusual syntax:
+ * Log functions have been designed so that printing variables and doing string concatenation will
+ * not occur if the log level is disabled, which leads to slightly unusual syntax:
  *
  * Log.debug { "This is a log message with a $value" }
  */
@@ -47,9 +47,7 @@
     public val ERROR_LOGGABLE: Boolean =
         LOG_LEVEL <= LOG_LEVEL_ERROR || Log.isLoggable(TAG, Log.ERROR)
 
-    /**
-     * Debug functions log noisy information related to the internals of the system.
-     */
+    /** Debug functions log noisy information related to the internals of the system. */
     public inline fun debug(crossinline msg: () -> String) {
         if (Debug.ENABLE_LOGGING && DEBUG_LOGGABLE) Log.d(TAG, msg())
     }
@@ -58,9 +56,7 @@
         if (Debug.ENABLE_LOGGING && DEBUG_LOGGABLE) Log.d(TAG, msg(), throwable)
     }
 
-    /**
-     * Info functions log standard, useful information about the state of the system.
-     */
+    /** Info functions log standard, useful information about the state of the system. */
     public inline fun info(crossinline msg: () -> String) {
         if (Debug.ENABLE_LOGGING && INFO_LOGGABLE) Log.i(TAG, msg())
     }
@@ -92,19 +88,19 @@
         if (Debug.ENABLE_LOGGING && ERROR_LOGGABLE) Log.e(TAG, msg(), throwable)
     }
 
-    /**
-     * Read the stack trace of a calling method and join it to a formatted string.
-     */
+    /** Read the stack trace of a calling method and join it to a formatted string. */
     public fun readStackTrace(limit: Int = 4): String {
         val elements = Thread.currentThread().stackTrace
         // Ignore the first 3 elements, which ignores:
         // VMStack.getThreadStackTrace
         // Thread.currentThread().getStackTrace()
         // dumpStackTrace()
-        return elements.drop(3).joinToString(
-            prefix = "\n\t",
-            separator = "\t",
-            limit = limit,
-        )
+        return elements
+            .drop(3)
+            .joinToString(
+                prefix = "\n\t",
+                separator = "\t",
+                limit = limit,
+            )
     }
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Permissions.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Permissions.kt
index 81d1205..ce5adf8 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Permissions.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Permissions.kt
@@ -22,28 +22,32 @@
 import android.os.Build
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.compat.Api23Compat
+import androidx.camera.camera2.pipe.config.CameraPipeContext
 import javax.inject.Inject
 import javax.inject.Singleton
 
 /**
  * This tracks internal permission requests to avoid querying multiple times.
  *
- * This class assumes that permissions are one way - They can be granted, but not un-granted
- * without restarting the application process.
+ * This class assumes that permissions are one way - They can be granted, but not un-granted without
+ * restarting the application process.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 @Singleton
-internal class Permissions @Inject constructor(private val context: Context) {
-    @Volatile
-    private var _hasCameraPermission = false
+internal class Permissions
+@Inject
+constructor(@CameraPipeContext private val cameraPipeContext: Context) {
+    @Volatile private var _hasCameraPermission = false
     val hasCameraPermission: Boolean
-        get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            checkCameraPermission()
-        } else {
-            // On older versions of Android, permissions are required in order to install a package
-            // and so the permission check is redundant.
-            true
-        }
+        get() =
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                checkCameraPermission()
+            } else {
+                // On older versions of Android, permissions are required in order to install a
+                // package
+                // and so the permission check is redundant.
+                true
+            }
 
     @RequiresApi(23)
     private fun checkCameraPermission(): Boolean {
@@ -53,13 +57,12 @@
         // allowing the code to avoid re-querying after checkSelfPermission returns true.
         if (!_hasCameraPermission) {
             Debug.traceStart { "CXCP#checkCameraPermission" }
-            if (Api23Compat.checkSelfPermission(context, Manifest.permission.CAMERA)
-                == PERMISSION_GRANTED
-            ) {
+            if (Api23Compat.checkSelfPermission(cameraPipeContext, Manifest.permission.CAMERA) ==
+                PERMISSION_GRANTED) {
                 _hasCameraPermission = true
             }
             Debug.traceStop()
         }
         return _hasCameraPermission
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Threads.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Threads.kt
index 72b41c9..60dc81b 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Threads.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Threads.kt
@@ -29,16 +29,12 @@
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 class Threads(
     val globalScope: CoroutineScope,
-
     val blockingExecutor: Executor,
     val blockingDispatcher: CoroutineDispatcher,
-
     val backgroundExecutor: Executor,
     val backgroundDispatcher: CoroutineDispatcher,
-
     val lightweightExecutor: Executor,
     val lightweightDispatcher: CoroutineDispatcher,
-
     camera2Handler: () -> Handler,
     camera2Executor: () -> Executor
 ) {
@@ -49,4 +45,4 @@
         get() = _camera2Handler.value
     val camera2Executor: Executor
         get() = _camera2Executor.value
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Timestamps.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Timestamps.kt
index be44a90..a3c97b7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Timestamps.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Timestamps.kt
@@ -24,9 +24,7 @@
 import javax.inject.Inject
 import javax.inject.Singleton
 
-/**
- * A nanosecond timestamp
- */
+/** A nanosecond timestamp */
 @JvmInline
 public value class TimestampNs constructor(public val value: Long) {
     public inline operator fun minus(other: TimestampNs): DurationNs =
@@ -41,8 +39,7 @@
     public inline operator fun minus(other: DurationNs): DurationNs =
         DurationNs(value - other.value)
 
-    public inline operator fun plus(other: DurationNs): DurationNs =
-        DurationNs(value + other.value)
+    public inline operator fun plus(other: DurationNs): DurationNs = DurationNs(value + other.value)
 
     public inline operator fun plus(other: TimestampNs): TimestampNs =
         TimestampNs(value + other.value)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Token.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Token.kt
index aad27df..aee7384 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Token.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Token.kt
@@ -18,14 +18,12 @@
 
 import androidx.annotation.RequiresApi
 
-/**
- * A token is used to track access to underlying resources. Implementations must be thread-safe.
- */
+/** A token is used to track access to underlying resources. Implementations must be thread-safe. */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 internal interface Token {
     /**
-     * Release this token instance. Return true if this is the first time release has been called
-     * on this token.
+     * Release this token instance. Return true if this is the first time release has been called on
+     * this token.
      */
     fun release(): Boolean
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/TokenLock.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/TokenLock.kt
index 574cc10..a65fa0b 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/TokenLock.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/TokenLock.kt
@@ -22,7 +22,6 @@
 import androidx.annotation.GuardedBy
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.core.TokenLock.Token
-import java.io.Closeable
 import java.util.ArrayDeque
 import kotlin.coroutines.resume
 import kotlin.coroutines.resumeWithException
@@ -36,8 +35,8 @@
  * Provides fair access to a resources by acquiring and releasing variable sized [Token] objects.
  *
  * A [TokenLock] has a fixed maximum size that it will issue [Token] objects for. Additional
- * requests beyond the maximum capacity of the lock will wait until enough of the outstanding
- * tokens have been closed to fulfill the next request in the queue.
+ * requests beyond the maximum capacity of the lock will wait until enough of the outstanding tokens
+ * have been closed to fulfill the next request in the queue.
  *
  * This object behaves like a lock or mutex, which means that it's possible to deadlock if a
  * function or sequence attempts to acquire or hold multiple tokens. For this reason, it's
@@ -47,7 +46,7 @@
  * Access the methods and properties of the [TokenLock] are ThreadSafe, and closing this object
  * multiple times has no effect.
  */
-internal interface TokenLock : AutoCloseable, Closeable {
+internal interface TokenLock : AutoCloseable {
     val capacity: Long
     val available: Long
     val size: Long
@@ -75,7 +74,7 @@
      *
      * Closing this object multiple times has no effect.
      */
-    interface Token : AutoCloseable, Closeable {
+    interface Token : AutoCloseable {
         val value: Long
 
         /**
@@ -92,14 +91,10 @@
     this.acquire(value, value)
 
 /** Shorthand for "acquireOrNull(value, value)" */
-internal inline fun TokenLock.acquireOrNull(value: Long): TokenLock.Token? = this.acquireOrNull(
-    value,
-    value
-)
+internal inline fun TokenLock.acquireOrNull(value: Long): TokenLock.Token? =
+    this.acquireOrNull(value, value)
 
-/**
- * Executes the given action while holding a token.
- */
+/** Executes the given action while holding a token. */
 internal suspend inline fun <T> TokenLock.withToken(
     value: Long,
     crossinline action: (token: TokenLock.Token) -> T
@@ -109,9 +104,7 @@
     }
 }
 
-/**
- * Executes the given action while holding a token.
- */
+/** Executes the given action while holding a token. */
 internal suspend inline fun <T> TokenLock.withToken(
     min: Long,
     max: Long,
@@ -129,33 +122,32 @@
 
     private val pending = ArrayDeque<TokenRequest>()
 
-    @GuardedBy("pending")
-    private var closed = false
+    @GuardedBy("pending") private var closed = false
 
-    @GuardedBy("pending")
-    private var _available: Long = capacity
+    @GuardedBy("pending") private var _available: Long = capacity
 
     override val available: Long
-        get() = synchronized(pending) {
-            return if (closed || pending.isNotEmpty()) {
-                0
-            } else {
-                _available
+        get() =
+            synchronized(pending) {
+                return if (closed || pending.isNotEmpty()) {
+                    0
+                } else {
+                    _available
+                }
             }
-        }
 
     override val size: Long
-        get() = synchronized(pending) {
-            return if (closed || pending.isNotEmpty()) {
-                capacity
-            } else {
-                capacity - _available
+        get() =
+            synchronized(pending) {
+                return if (closed || pending.isNotEmpty()) {
+                    capacity
+                } else {
+                    capacity - _available
+                }
             }
-        }
 
     override fun acquireOrNull(min: Long, max: Long): TokenLock.Token? {
-        if (min > capacity)
-            throw IllegalArgumentException("Attempted to acquire $min / $capacity")
+        if (min > capacity) throw IllegalArgumentException("Attempted to acquire $min / $capacity")
 
         synchronized(pending) {
             if (closed) return null
@@ -175,8 +167,7 @@
         suspendCancellableCoroutine { continuation ->
             if (min > capacity) {
                 continuation.resumeWithException(
-                    IllegalArgumentException("Attempted to acquire $min / $capacity")
-                )
+                    IllegalArgumentException("Attempted to acquire $min / $capacity"))
                 return@suspendCancellableCoroutine
             }
 
@@ -209,15 +200,13 @@
         // Make sure all suspended functions that are waiting for a token are canceled, then clear
         // the list. This access is safe because all other interactions with the pending list occur
         // within a synchronized block that's guarded by a closed check.
-        pending.forEach {
-            it.continuation.cancel()
-        }
+        pending.forEach { it.continuation.cancel() }
         pending.clear()
     }
 
     /**
      * WARNING: This is an internal function to avoid creating synthetic accessors but it should
-     *  ONLY be called by TokenImpl.close()
+     * ONLY be called by TokenImpl.close()
      */
     internal fun release(qty: Long) {
         var requestsToComplete: List<TokenRequest>? = null
@@ -263,9 +252,7 @@
             }
         }
 
-        requestsToComplete?.forEach {
-            it.continuation.resume(it.token!!)
-        }
+        requestsToComplete?.forEach { it.continuation.resume(it.token!!) }
     }
 
     private class TokenRequest(
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/WakeLock.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/WakeLock.kt
index b6e08bf..225d157 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/WakeLock.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/WakeLock.kt
@@ -38,18 +38,22 @@
 internal class WakeLock(
     private val scope: CoroutineScope,
     private val timeout: Long = 0,
-    private val callback: () -> Unit
+    private val startTimeoutOnCreation: Boolean = false,
+    private val callback: () -> Unit,
 ) {
     private val lock = Any()
 
-    @GuardedBy("lock")
-    private var count = 0
+    @GuardedBy("lock") private var count = 0
 
-    @GuardedBy("lock")
-    private var timeoutJob: Job? = null
+    @GuardedBy("lock") private var timeoutJob: Job? = null
 
-    @GuardedBy("lock")
-    private var closed = false
+    @GuardedBy("lock") private var closed = false
+
+    init {
+        if (startTimeoutOnCreation) {
+            synchronized(lock) { startTimeout() }
+        }
+    }
 
     private inner class WakeLockToken : Token {
         private val closed = atomic(false)
@@ -99,21 +103,27 @@
         synchronized(lock) {
             count -= 1
             if (count == 0 && !closed) {
-                timeoutJob = scope.launch {
-                    delay(timeout)
-
-                    synchronized(lock) {
-                        if (closed || count != 0) {
-                            return@launch
-                        }
-                        timeoutJob = null
-                        closed = true
-                    }
-
-                    // Execute the callback
-                    callback()
-                }
+                startTimeout()
             }
         }
     }
-}
\ No newline at end of file
+
+    @GuardedBy("lock")
+    private fun startTimeout() {
+        timeoutJob =
+            scope.launch {
+                delay(timeout)
+
+                synchronized(lock) {
+                    if (closed || count != 0) {
+                        return@launch
+                    }
+                    timeoutJob = null
+                    closed = true
+                }
+
+                // Execute the callback
+                callback()
+            }
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt
index 3b7b43f..366f78c 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt
@@ -39,7 +39,9 @@
 
 @RequiresApi(21)
 @CameraGraphScope
-internal class CameraGraphImpl @Inject constructor(
+internal class CameraGraphImpl
+@Inject
+constructor(
     graphConfig: CameraGraph.Config,
     metadata: CameraMetadata,
     private val graphProcessor: GraphProcessor,
@@ -59,29 +61,35 @@
 
     init {
         // Log out the configuration of the camera graph when it is created.
-        Log.info {
-            Debug.formatCameraGraphProperties(metadata, graphConfig, this)
-        }
+        Log.info { Debug.formatCameraGraphProperties(metadata, graphConfig, this) }
 
         // Enforce preview and video stream use cases for high speed sessions
         if (graphConfig.sessionMode == CameraGraph.OperatingMode.HIGH_SPEED) {
             require(streamGraph.outputs.isNotEmpty()) {
-                "Cannot create a HIGH_SPEED CameraGraph without outputs." }
+                "Cannot create a HIGH_SPEED CameraGraph without outputs."
+            }
             require(streamGraph.outputs.size <= 2) {
                 "Cannot create a HIGH_SPEED CameraGraph with more than two outputs. " +
-                    "Configured outputs are ${streamGraph.outputs}" }
-            val containsPreviewStream = this.streamGraph.outputs.any {
-                it.streamUseCase == OutputStream.StreamUseCase.PREVIEW }
-            val containsVideoStream = this.streamGraph.outputs.any {
-                it.streamUseCase == OutputStream.StreamUseCase.VIDEO_RECORD }
+                    "Configured outputs are ${streamGraph.outputs}"
+            }
+            val containsPreviewStream =
+                this.streamGraph.outputs.any {
+                    it.streamUseCase == OutputStream.StreamUseCase.PREVIEW
+                }
+            val containsVideoStream =
+                this.streamGraph.outputs.any {
+                    it.streamUseCase == OutputStream.StreamUseCase.VIDEO_RECORD
+                }
             if (streamGraph.outputs.size == 2) {
                 require(containsPreviewStream) {
                     "Cannot create a HIGH_SPEED CameraGraph without setting the Preview " +
-                        "Video stream. Configured outputs are ${streamGraph.outputs}" }
+                        "Video stream. Configured outputs are ${streamGraph.outputs}"
+                }
             } else {
                 require(containsPreviewStream || containsVideoStream) {
                     "Cannot create a HIGH_SPEED CameraGraph without having a Preview or Video " +
-                        "stream. Configured outputs are ${streamGraph.outputs}" }
+                        "stream. Configured outputs are ${streamGraph.outputs}"
+                }
             }
         }
     }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
index c1744cc..91a28eb 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
@@ -89,8 +89,7 @@
             awbMode = awbMode,
             aeRegions = aeRegions,
             afRegions = afRegions,
-            awbRegions = awbRegions
-        )
+            awbRegions = awbRegions)
     }
 
     override suspend fun submit3A(
@@ -139,8 +138,7 @@
             awbLockBehavior,
             afTriggerStartAeMode,
             frameLimit,
-            timeLimitNs
-        )
+            timeLimitNs)
     }
 
     override suspend fun unlock3A(ae: Boolean?, af: Boolean?, awb: Boolean?): Deferred<Result3A> {
@@ -148,10 +146,7 @@
         return controller3A.unlock3A(ae, af, awb)
     }
 
-    override suspend fun lock3AForCapture(
-        frameLimit: Int,
-        timeLimitNs: Long
-    ): Deferred<Result3A> {
+    override suspend fun lock3AForCapture(frameLimit: Int, timeLimitNs: Long): Deferred<Result3A> {
         check(!closed.value) { "Cannot call lock3AForCapture on $this after close." }
         return controller3A.lock3AForCapture(frameLimit, timeLimitNs)
     }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
index 8f2fed9..7ed15a8 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
@@ -47,9 +47,7 @@
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.cancel
 
-/**
- * This class implements the 3A methods of [CameraGraphSessionImpl].
- */
+/** This class implements the 3A methods of [CameraGraphSessionImpl]. */
 internal class Controller3A(
     private val graphProcessor: GraphProcessor,
     private val metadata: CameraMetadata,
@@ -57,80 +55,76 @@
     private val graphListener3A: Listener3A
 ) {
     companion object {
-        private val aeConvergedStateList = listOf(
-            CaptureResult.CONTROL_AE_STATE_CONVERGED,
-            CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED,
-            CaptureResult.CONTROL_AE_STATE_LOCKED
-        )
+        private val aeConvergedStateList =
+            listOf(
+                CaptureResult.CONTROL_AE_STATE_CONVERGED,
+                CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED,
+                CaptureResult.CONTROL_AE_STATE_LOCKED)
 
-        private val awbConvergedStateList = listOf(
-            CaptureResult.CONTROL_AWB_STATE_CONVERGED,
-            CaptureResult.CONTROL_AWB_STATE_LOCKED
-        )
+        private val awbConvergedStateList =
+            listOf(
+                CaptureResult.CONTROL_AWB_STATE_CONVERGED, CaptureResult.CONTROL_AWB_STATE_LOCKED)
 
-        private val afConvergedStateList = listOf(
-            CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
-            CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED,
-            CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
-            CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED
-        )
+        private val afConvergedStateList =
+            listOf(
+                CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+                CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED,
+                CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED)
 
         private val aeLockedStateList = listOf(CaptureResult.CONTROL_AE_STATE_LOCKED)
 
         private val awbLockedStateList = listOf(CaptureResult.CONTROL_AWB_STATE_LOCKED)
 
-        private val afLockedStateList = listOf(
-            CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
-            CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED
-        )
+        private val afLockedStateList =
+            listOf(
+                CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED)
 
-        private val aePostPrecaptureStateList = listOf(
-            CaptureResult.CONTROL_AE_STATE_CONVERGED,
-            CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED,
-            CaptureResult.CONTROL_AE_STATE_LOCKED
-        )
+        private val aePostPrecaptureStateList =
+            listOf(
+                CaptureResult.CONTROL_AE_STATE_CONVERGED,
+                CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED,
+                CaptureResult.CONTROL_AE_STATE_LOCKED)
 
-        val parameterForAfTriggerStart = mapOf<CaptureRequest.Key<*>, Any>(
-            CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_START
-        )
+        val parameterForAfTriggerStart =
+            mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_START)
 
-        val parameterForAfTriggerCancel = mapOf<CaptureRequest.Key<*>, Any>(
-            CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_CANCEL
-        )
+        val parameterForAfTriggerCancel =
+            mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_CANCEL)
 
-        private val parametersForAePrecaptureAndAfTrigger = mapOf<CaptureRequest.Key<*>, Any>(
-            CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_START,
-            CONTROL_AE_PRECAPTURE_TRIGGER to CONTROL_AE_PRECAPTURE_TRIGGER_START
-        )
+        private val parametersForAePrecaptureAndAfTrigger =
+            mapOf<CaptureRequest.Key<*>, Any>(
+                CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_START,
+                CONTROL_AE_PRECAPTURE_TRIGGER to CONTROL_AE_PRECAPTURE_TRIGGER_START)
 
         private val result3ASubmitFailed = Result3A(Status.SUBMIT_FAILED)
 
-        private val aeUnlockedStateList = listOf(
-            CaptureResult.CONTROL_AE_STATE_INACTIVE,
-            CaptureResult.CONTROL_AE_STATE_SEARCHING,
-            CaptureResult.CONTROL_AE_STATE_CONVERGED,
-            CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED
-        )
+        private val aeUnlockedStateList =
+            listOf(
+                CaptureResult.CONTROL_AE_STATE_INACTIVE,
+                CaptureResult.CONTROL_AE_STATE_SEARCHING,
+                CaptureResult.CONTROL_AE_STATE_CONVERGED,
+                CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED)
 
-        private val afUnlockedStateList = listOf(
-            CaptureResult.CONTROL_AF_STATE_INACTIVE,
-            CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN,
-            CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
-            CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
-            CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED
-        )
+        private val afUnlockedStateList =
+            listOf(
+                CaptureResult.CONTROL_AF_STATE_INACTIVE,
+                CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN,
+                CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+                CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED)
 
-        private val awbUnlockedStateList = listOf(
-            CaptureResult.CONTROL_AWB_STATE_INACTIVE,
-            CaptureResult.CONTROL_AWB_STATE_SEARCHING,
-            CaptureResult.CONTROL_AWB_STATE_CONVERGED
-        )
+        private val awbUnlockedStateList =
+            listOf(
+                CaptureResult.CONTROL_AWB_STATE_INACTIVE,
+                CaptureResult.CONTROL_AWB_STATE_SEARCHING,
+                CaptureResult.CONTROL_AWB_STATE_CONVERGED)
     }
 
     // Keep track of the result associated with latest call to update3A. If update3A is called again
     // and the current result is not complete, we will cancel the current result.
-    @GuardedBy("this")
-    private var lastUpdate3AResult: Deferred<Result3A>? = null
+    @GuardedBy("this") private var lastUpdate3AResult: Deferred<Result3A>? = null
 
     fun update3A(
         aeMode: AeMode? = null,
@@ -179,24 +173,9 @@
         aeMode?.let { extra3AParams.put(CaptureRequest.CONTROL_AE_MODE, it.value) }
         afMode?.let { extra3AParams.put(CaptureRequest.CONTROL_AF_MODE, it.value) }
         awbMode?.let { extra3AParams.put(CaptureRequest.CONTROL_AWB_MODE, it.value) }
-        aeRegions?.let {
-            extra3AParams.put(
-                CaptureRequest.CONTROL_AE_REGIONS,
-                it.toTypedArray()
-            )
-        }
-        afRegions?.let {
-            extra3AParams.put(
-                CaptureRequest.CONTROL_AF_REGIONS,
-                it.toTypedArray()
-            )
-        }
-        awbRegions?.let {
-            extra3AParams.put(
-                CaptureRequest.CONTROL_AWB_REGIONS,
-                it.toTypedArray()
-            )
-        }
+        aeRegions?.let { extra3AParams.put(CaptureRequest.CONTROL_AE_REGIONS, it.toTypedArray()) }
+        afRegions?.let { extra3AParams.put(CaptureRequest.CONTROL_AF_REGIONS, it.toTypedArray()) }
+        awbRegions?.let { extra3AParams.put(CaptureRequest.CONTROL_AWB_REGIONS, it.toTypedArray()) }
 
         if (!graphProcessor.submit(extra3AParams)) {
             graphListener3A.removeListener(listener)
@@ -206,17 +185,17 @@
     }
 
     /**
-     * Given the desired metering regions and lock behaviors for ae, af and awb, this method
-     * updates the metering regions and then, (a) unlocks ae, af, awb and wait for them to converge,
-     * and then (b) locks them.
+     * Given the desired metering regions and lock behaviors for ae, af and awb, this method updates
+     * the metering regions and then, (a) unlocks ae, af, awb and wait for them to converge, and
+     * then (b) locks them.
      *
      * (a) In this step, as needed, we first send a single request with 'af trigger = cancel' to
      * unlock af, and then a repeating request to unlock ae and awb. We suspend till we receive a
-     * response from the camera that each of the ae, af awb are converged.
-     * (b) In this step, as needed, we submit a repeating request to lock ae and awb, and then a
-     * single request to lock af by setting 'af trigger = start'. Once these requests are submitted
-     * we don't wait further and immediately return a Deferred<Result3A> which gets completed when
-     * the capture result with correct lock states for ae, af and awb is received.
+     * response from the camera that each of the ae, af awb are converged. (b) In this step, as
+     * needed, we submit a repeating request to lock ae and awb, and then a single request to lock
+     * af by setting 'af trigger = start'. Once these requests are submitted we don't wait further
+     * and immediately return a Deferred<Result3A> which gets completed when the capture result with
+     * correct lock states for ae, af and awb is received.
      *
      * If we received an error when submitting any of the above requests or if waiting for the
      * desired 3A state times out then we return early with the appropriate status code.
@@ -264,18 +243,14 @@
         // As needed unlock ae, awb and wait for ae, af and awb to converge.
         if (aeLockBehavior.shouldWaitForAeToConverge() ||
             afLockBehaviorSanitized.shouldWaitForAfToConverge() ||
-            awbLockBehavior.shouldWaitForAwbToConverge()
-        ) {
-            val converged3AExitConditions = createConverged3AExitConditions(
-                aeLockBehavior.shouldWaitForAeToConverge(),
-                afLockBehaviorSanitized.shouldWaitForAfToConverge(),
-                awbLockBehavior.shouldWaitForAwbToConverge()
-            )
-            val listener = Result3AStateListenerImpl(
-                converged3AExitConditions,
-                frameLimit,
-                timeLimitNs
-            )
+            awbLockBehavior.shouldWaitForAwbToConverge()) {
+            val converged3AExitConditions =
+                createConverged3AExitConditions(
+                    aeLockBehavior.shouldWaitForAeToConverge(),
+                    afLockBehaviorSanitized.shouldWaitForAfToConverge(),
+                    awbLockBehavior.shouldWaitForAwbToConverge())
+            val listener =
+                Result3AStateListenerImpl(converged3AExitConditions, frameLimit, timeLimitNs)
             graphListener3A.addListener(listener)
 
             // If we have to explicitly unlock ae, awb, then update the 3A state of the camera
@@ -287,10 +262,7 @@
             val awbLockValue = if (awbLockBehavior.shouldUnlockAwb()) false else null
             if (aeLockValue != null || awbLockValue != null) {
                 debug { "lock3A - setting aeLock=$aeLockValue, awbLock=$awbLockValue" }
-                graphState3A.update(
-                    aeLock = aeLockValue,
-                    awbLock = awbLockValue
-                )
+                graphState3A.update(aeLock = aeLockValue, awbLock = awbLockValue)
             }
             graphProcessor.invalidate()
 
@@ -319,8 +291,7 @@
             awbLockBehavior,
             afTriggerStartAeMode,
             frameLimit,
-            timeLimitNs
-        )
+            timeLimitNs)
     }
 
     /**
@@ -354,11 +325,8 @@
         }
 
         // As needed unlock ae, awb and wait for ae, af and awb to converge.
-        val unlocked3AExitConditions = createUnLocked3AExitConditions(
-            ae == true,
-            afSanitized == true,
-            awb == true
-        )
+        val unlocked3AExitConditions =
+            createUnLocked3AExitConditions(ae == true, afSanitized == true, awb == true)
         val listener = Result3AStateListenerImpl(unlocked3AExitConditions)
         graphListener3A.addListener(listener)
 
@@ -368,10 +336,7 @@
         val awbLockValue = if (awb == true) false else null
         if (aeLockValue != null || awbLockValue != null) {
             debug { "unlock3A - updating graph state, aeLock=$aeLockValue, awbLock=$awbLockValue" }
-            graphState3A.update(
-                aeLock = aeLockValue,
-                awbLock = awbLockValue
-            )
+            graphState3A.update(aeLock = aeLockValue, awbLock = awbLockValue)
         }
         graphProcessor.invalidate()
         return listener.result
@@ -381,14 +346,13 @@
         frameLimit: Int = DEFAULT_FRAME_LIMIT,
         timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS
     ): Deferred<Result3A> {
-        val listener = Result3AStateListenerImpl(
-            mapOf<CaptureResult.Key<*>, List<Any>>(
-                CaptureResult.CONTROL_AE_STATE to aePostPrecaptureStateList,
-                CaptureResult.CONTROL_AF_STATE to afLockedStateList
-            ),
-            frameLimit,
-            timeLimitNs
-        )
+        val listener =
+            Result3AStateListenerImpl(
+                mapOf<CaptureResult.Key<*>, List<Any>>(
+                    CaptureResult.CONTROL_AE_STATE to aePostPrecaptureStateList,
+                    CaptureResult.CONTROL_AF_STATE to afLockedStateList),
+                frameLimit,
+                timeLimitNs)
         graphListener3A.addListener(listener)
 
         debug { "lock3AForCapture - sending a request to trigger ae precapture metering and af." }
@@ -413,20 +377,15 @@
     }
 
     /**
-     * For api level below 23, to resume the normal scan of ae after precapture metering
-     * sequence, we have to first send a request with ae lock = true and then a request with ae
-     * lock = false. REF :
+     * For api level below 23, to resume the normal scan of ae after precapture metering sequence,
+     * we have to first send a request with ae lock = true and then a request with ae lock = false.
+     * REF :
      * https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER
      */
     private suspend fun unlock3APostCaptureAndroidLAndBelow(): Deferred<Result3A> {
         debug { "unlock3AForCapture - sending a request to cancel af and turn on ae." }
         if (!graphProcessor.submit(
-                mapOf(
-                        CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_CANCEL,
-                        CONTROL_AE_LOCK to true
-                    )
-            )
-        ) {
+            mapOf(CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_CANCEL, CONTROL_AE_LOCK to true))) {
             debug { "unlock3AForCapture - request to cancel af and lock ae as failed." }
             return CompletableDeferred(result3ASubmitFailed)
         }
@@ -447,19 +406,18 @@
     }
 
     /**
-     * For API level 23 or newer versions, the sending a request with
-     * CONTROL_AE_PRECAPTURE_TRIGGER = CANCEL can be used to unlock the camera device's
-     * internally locked AE. REF :
+     * For API level 23 or newer versions, the sending a request with CONTROL_AE_PRECAPTURE_TRIGGER
+     * = CANCEL can be used to unlock the camera device's internally locked AE. REF :
      * https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER
      */
     @RequiresApi(23)
     private suspend fun unlock3APostCaptureAndroidMAndAbove(): Deferred<Result3A> {
         debug { "unlock3APostCapture - sending a request to reset af and ae precapture metering." }
-        val parametersForAePrecaptureAndAfCancel = mapOf<CaptureRequest.Key<*>, Any>(
-            CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_CANCEL,
-            CONTROL_AE_PRECAPTURE_TRIGGER to
-                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL
-        )
+        val parametersForAePrecaptureAndAfCancel =
+            mapOf<CaptureRequest.Key<*>, Any>(
+                CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_CANCEL,
+                CONTROL_AE_PRECAPTURE_TRIGGER to
+                    CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL)
         if (!graphProcessor.submit(parametersForAePrecaptureAndAfCancel)) {
             debug {
                 "unlock3APostCapture - request to reset af and ae precapture metering failed, " +
@@ -472,11 +430,10 @@
         // on the ae state, so we don't need to listen for a specific state. As long as the request
         // successfully reaches the camera device and the capture request corresponding to that
         // request arrives back, it should suffice.
-        val listener = Result3AStateListenerImpl(
-            mapOf<CaptureResult.Key<*>, List<Any>>(
-                CaptureResult.CONTROL_AF_STATE to afUnlockedStateList
-            )
-        )
+        val listener =
+            Result3AStateListenerImpl(
+                mapOf<CaptureResult.Key<*>, List<Any>>(
+                    CaptureResult.CONTROL_AF_STATE to afUnlockedStateList))
         graphListener3A.addListener(listener)
         graphProcessor.invalidate()
         return listener.result
@@ -487,8 +444,8 @@
         val flashMode = if (torchState == TorchState.ON) FlashMode.TORCH else FlashMode.OFF
         // To use the flash control, AE mode must be set to ON or OFF.
         val currAeMode = graphState3A.aeMode
-        val desiredAeMode = if (currAeMode == AeMode.ON || currAeMode == AeMode.OFF) null else
-            AeMode.ON
+        val desiredAeMode =
+            if (currAeMode == AeMode.ON || currAeMode == AeMode.OFF) null else AeMode.ON
         return update3A(aeMode = desiredAeMode, flashMode = flashMode)
     }
 
@@ -502,19 +459,14 @@
     ): Deferred<Result3A> {
         val finalAeLockValue = if (aeLockBehavior == null) null else true
         val finalAwbLockValue = if (awbLockBehavior == null) null else true
-        val locked3AExitConditions = createLocked3AExitConditions(
-            finalAeLockValue != null,
-            afLockBehavior != null,
-            finalAwbLockValue != null
-        )
+        val locked3AExitConditions =
+            createLocked3AExitConditions(
+                finalAeLockValue != null, afLockBehavior != null, finalAwbLockValue != null)
 
         var resultForLocked: Deferred<Result3A>? = null
         if (locked3AExitConditions.isNotEmpty()) {
-            val listener = Result3AStateListenerImpl(
-                locked3AExitConditions,
-                frameLimit,
-                timeLimitNs
-            )
+            val listener =
+                Result3AStateListenerImpl(locked3AExitConditions, frameLimit, timeLimitNs)
             graphListener3A.addListener(listener)
             graphState3A.update(aeLock = finalAeLockValue, awbLock = finalAwbLockValue)
             debug {
@@ -561,9 +513,7 @@
         waitForAfToConverge: Boolean,
         waitForAwbToConverge: Boolean
     ): Map<CaptureResult.Key<*>, List<Any>> {
-        if (
-            !waitForAeToConverge && !waitForAfToConverge && !waitForAwbToConverge
-        ) {
+        if (!waitForAeToConverge && !waitForAfToConverge && !waitForAwbToConverge) {
             return mapOf()
         }
         val exitConditionMapForConverged = mutableMapOf<CaptureResult.Key<*>, List<Any>>()
@@ -643,14 +593,11 @@
     }
 }
 
-internal fun Lock3ABehavior?.shouldUnlockAe(): Boolean =
-    this == Lock3ABehavior.AFTER_NEW_SCAN
+internal fun Lock3ABehavior?.shouldUnlockAe(): Boolean = this == Lock3ABehavior.AFTER_NEW_SCAN
 
-internal fun Lock3ABehavior?.shouldUnlockAf(): Boolean =
-    this == Lock3ABehavior.AFTER_NEW_SCAN
+internal fun Lock3ABehavior?.shouldUnlockAf(): Boolean = this == Lock3ABehavior.AFTER_NEW_SCAN
 
-internal fun Lock3ABehavior?.shouldUnlockAwb(): Boolean =
-    this == Lock3ABehavior.AFTER_NEW_SCAN
+internal fun Lock3ABehavior?.shouldUnlockAwb(): Boolean = this == Lock3ABehavior.AFTER_NEW_SCAN
 
 // For ae and awb if we set the lock = true in the capture request the camera device
 // locks them immediately. So when we want to wait for ae to converge we have to explicitly
@@ -671,6 +618,7 @@
 // convergence, we can send the trigger right away, but if the mode is CONTINUOUS_VIDEO then
 // sending a request to start a trigger locks the auto focus immediately, so if we want af to
 // converge first then we have to explicitly wait for it.
-// Ref: https://developer.android.com/reference/android/hardware/camera2/CaptureResult#CONTROL_AF_STATE
+// Ref:
+// https://developer.android.com/reference/android/hardware/camera2/CaptureResult#CONTROL_AF_STATE
 internal fun Lock3ABehavior?.shouldWaitForAfToConverge(): Boolean =
     this != null && this != Lock3ABehavior.IMMEDIATE
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphListener.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphListener.kt
index d801141..7e8ed2d 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphListener.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphListener.kt
@@ -52,8 +52,6 @@
      */
     fun onGraphModified(requestProcessor: GraphRequestProcessor)
 
-    /**
-     * Used to indicate that the graph has encountered an error.
-     */
+    /** Used to indicate that the graph has encountered an error. */
     fun onGraphError(graphStateError: GraphStateError)
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
index d439ebe..132f5bf 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
@@ -61,8 +61,8 @@
     fun stopRepeating()
 
     /**
-     * Indicates that internal parameters may have changed, and that the repeating request should
-     * be updated as soon as possible.
+     * Indicates that internal parameters may have changed, and that the repeating request should be
+     * updated as soon as possible.
      */
     fun invalidate()
 
@@ -79,11 +79,11 @@
     fun close()
 }
 
-/**
- * The graph processor handles *cross-session* state, such as the most recent repeating request.
- */
+/** The graph processor handles *cross-session* state, such as the most recent repeating request. */
 @CameraGraphScope
-internal class GraphProcessorImpl @Inject constructor(
+internal class GraphProcessorImpl
+@Inject
+constructor(
     private val threads: Threads,
     private val cameraGraphConfig: CameraGraph.Config,
     private val graphState3A: GraphState3A,
@@ -92,26 +92,19 @@
 ) : GraphProcessor, GraphListener {
     private val lock = Any()
 
-    @GuardedBy("lock")
-    private val submitQueue: MutableList<List<Request>> = ArrayList()
+    @GuardedBy("lock") private val submitQueue: MutableList<List<Request>> = ArrayList()
 
-    @GuardedBy("lock")
-    private var currentRepeatingRequest: Request? = null
+    @GuardedBy("lock") private var currentRepeatingRequest: Request? = null
 
-    @GuardedBy("lock")
-    private var nextRepeatingRequest: Request? = null
+    @GuardedBy("lock") private var nextRepeatingRequest: Request? = null
 
-    @GuardedBy("lock")
-    private var _requestProcessor: GraphRequestProcessor? = null
+    @GuardedBy("lock") private var _requestProcessor: GraphRequestProcessor? = null
 
-    @GuardedBy("lock")
-    private var submitting = false
+    @GuardedBy("lock") private var submitting = false
 
-    @GuardedBy("lock")
-    private var dirty = false
+    @GuardedBy("lock") private var dirty = false
 
-    @GuardedBy("lock")
-    private var closed = false
+    @GuardedBy("lock") private var closed = false
 
     private val _graphState = MutableStateFlow<GraphState>(GraphStateStopped)
 
@@ -141,9 +134,7 @@
 
         val processorToClose = old
         if (processorToClose != null) {
-            synchronized(processorToClose) {
-                processorToClose.close()
-            }
+            synchronized(processorToClose) { processorToClose.close() }
         }
         resubmit()
     }
@@ -175,9 +166,7 @@
 
         val processorToClose = old
         if (processorToClose != null) {
-            synchronized(processorToClose) {
-                processorToClose.close()
-            }
+            synchronized(processorToClose) { processorToClose.close() }
         }
     }
 
@@ -210,9 +199,7 @@
             debug { "startRepeating with ${request.formatForLogs()}" }
         }
 
-        graphScope.launch {
-            tryStartRepeating()
-        }
+        graphScope.launch { tryStartRepeating() }
     }
 
     override fun stopRepeating() {
@@ -228,9 +215,7 @@
             Debug.traceStart { "$this#stopRepeating" }
             // Start with requests that have already been submitted
             if (processor != null) {
-                synchronized(processor) {
-                    processor.stopRepeating()
-                }
+                synchronized(processor) { processor.stopRepeating() }
             }
             Debug.traceStop()
         }
@@ -243,22 +228,16 @@
     override fun submit(requests: List<Request>) {
         synchronized(lock) {
             if (closed) {
-                graphScope.launch(threads.lightweightDispatcher) {
-                    abortBurst(requests)
-                }
+                graphScope.launch(threads.lightweightDispatcher) { abortBurst(requests) }
                 return
             }
             submitQueue.add(requests)
         }
 
-        graphScope.launch(threads.lightweightDispatcher) {
-            submitLoop()
-        }
+        graphScope.launch(threads.lightweightDispatcher) { submitLoop() }
     }
 
-    /**
-     * Submit a request to the camera using only the current repeating request.
-     */
+    /** Submit a request to the camera using only the current repeating request. */
     override suspend fun submit(parameters: Map<*, Any?>): Boolean =
         withContext(threads.lightweightDispatcher) {
             val processor: GraphRequestProcessor?
@@ -277,22 +256,20 @@
 
             return@withContext when {
                 processor == null || request == null -> false
-                else -> processor.submit(
-                    isRepeating = false,
-                    requests = listOf(request),
-                    defaultParameters = cameraGraphConfig.defaultParameters,
-                    requiredParameters = requiredParameters,
-                    listeners = graphListeners
-                )
+                else ->
+                    processor.submit(
+                        isRepeating = false,
+                        requests = listOf(request),
+                        defaultParameters = cameraGraphConfig.defaultParameters,
+                        requiredParameters = requiredParameters,
+                        listeners = graphListeners)
             }
         }
 
     override fun invalidate() {
         // Invalidate is only used for updates to internal state (listeners, parameters, etc) and
         // should not (currently) attempt to resubmit the normal request queue.
-        graphScope.launch(threads.lightweightDispatcher) {
-            tryStartRepeating()
-        }
+        graphScope.launch(threads.lightweightDispatcher) { tryStartRepeating() }
     }
 
     override fun abort() {
@@ -309,9 +286,7 @@
             Debug.traceStart { "$this#abort" }
             // Start with requests that have already been submitted
             if (processor != null) {
-                synchronized(processor) {
-                    processor.abortCaptures()
-                }
+                synchronized(processor) { processor.abortCaptures() }
             }
 
             // Then abort requests that have not been submitted
@@ -397,20 +372,20 @@
                 requiredParameters.putAllMetadata(cameraGraphConfig.requiredParameters)
 
                 if (processor.submit(
-                        isRepeating = true,
-                        requests = listOf(request),
-                        defaultParameters = cameraGraphConfig.defaultParameters,
-                        requiredParameters = requiredParameters,
-                        listeners = graphListeners
-                    )
-                ) {
+                    isRepeating = true,
+                    requests = listOf(request),
+                    defaultParameters = cameraGraphConfig.defaultParameters,
+                    requiredParameters = requiredParameters,
+                    listeners = graphListeners)) {
                     // ONLY update the current repeating request if the update succeeds
                     synchronized(lock) {
                         if (processor === _requestProcessor) {
                             currentRepeatingRequest = request
 
-                            // There is a race condition where the nextRepeating request might be changed
-                            // while trying to update the current repeating request. If this happens, do no
+                            // There is a race condition where the nextRepeating request might be
+                            // changed
+                            // while trying to update the current repeating request. If this
+                            // happens, do no
                             // overwrite the pending request.
                             if (nextRepeatingRequest == request) {
                                 nextRepeatingRequest = null
@@ -451,19 +426,19 @@
             var submitted = false
             Debug.traceStart { "$this#submit" }
             try {
-                submitted = synchronized(processor) {
-                    val requiredParameters = mutableMapOf<Any, Any?>()
-                    graphState3A.writeTo(requiredParameters)
-                    requiredParameters.putAllMetadata(cameraGraphConfig.requiredParameters)
+                submitted =
+                    synchronized(processor) {
+                        val requiredParameters = mutableMapOf<Any, Any?>()
+                        graphState3A.writeTo(requiredParameters)
+                        requiredParameters.putAllMetadata(cameraGraphConfig.requiredParameters)
 
-                    processor.submit(
-                        isRepeating = false,
-                        requests = burst,
-                        defaultParameters = cameraGraphConfig.defaultParameters,
-                        requiredParameters = requiredParameters,
-                        listeners = graphListeners
-                    )
-                }
+                        processor.submit(
+                            isRepeating = false,
+                            requests = burst,
+                            defaultParameters = cameraGraphConfig.defaultParameters,
+                            requiredParameters = requiredParameters,
+                            listeners = graphListeners)
+                    }
             } finally {
                 Debug.traceStop()
                 synchronized(lock) {
@@ -504,4 +479,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphRequestProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphRequestProcessor.kt
index 2548f69..8b2d269 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphRequestProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphRequestProcessor.kt
@@ -21,10 +21,10 @@
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CaptureSequence
 import androidx.camera.camera2.pipe.CaptureSequenceProcessor
+import androidx.camera.camera2.pipe.CaptureSequences.invokeOnRequests
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.compat.ObjectUnavailableException
 import androidx.camera.camera2.pipe.core.Log
-import androidx.camera.camera2.pipe.CaptureSequences.invokeOnRequests
 import kotlinx.atomicfu.atomic
 
 internal val graphRequestProcessorIds = atomic(0)
@@ -37,7 +37,8 @@
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 @Suppress("NOTHING_TO_INLINE")
-public class GraphRequestProcessor private constructor(
+public class GraphRequestProcessor
+private constructor(
     private val captureSequenceProcessor: CaptureSequenceProcessor<Any, CaptureSequence<Any>>
 ) {
     companion object {
@@ -45,8 +46,7 @@
         fun from(captureSequenceProcessor: CaptureSequenceProcessor<*, *>): GraphRequestProcessor {
             @Suppress("UNCHECKED_CAST")
             return GraphRequestProcessor(
-                captureSequenceProcessor as CaptureSequenceProcessor<Any, CaptureSequence<Any>>
-            )
+                captureSequenceProcessor as CaptureSequenceProcessor<Any, CaptureSequence<Any>>)
         }
     }
 
@@ -54,18 +54,21 @@
     private val closed = atomic(false)
     @GuardedBy("activeCaptureSequences")
     private val activeCaptureSequences = mutableListOf<CaptureSequence<*>>()
-    private val activeBurstListener = object : CaptureSequence.CaptureSequenceListener {
-        override fun onCaptureSequenceComplete(captureSequence: CaptureSequence<*>) {
-            // Listen to the completion of active capture sequences and remove them from the list
-            // of currently active capture sequences. Since repeating requests are not required to
-            // execute, only non-repeating capture sequences are tracked.
-            if (!captureSequence.repeating) {
-                synchronized(activeCaptureSequences) {
-                    activeCaptureSequences.remove(captureSequence)
+    private val activeBurstListener =
+        object : CaptureSequence.CaptureSequenceListener {
+            override fun onCaptureSequenceComplete(captureSequence: CaptureSequence<*>) {
+                // Listen to the completion of active capture sequences and remove them from the
+                // list
+                // of currently active capture sequences. Since repeating requests are not required
+                // to
+                // execute, only non-repeating capture sequences are tracked.
+                if (!captureSequence.repeating) {
+                    synchronized(activeCaptureSequences) {
+                        activeCaptureSequences.remove(captureSequence)
+                    }
                 }
             }
         }
-    }
 
     internal fun abortCaptures() {
         // Note: abortCaptures is not affected by active state.
@@ -79,11 +82,12 @@
         // Create a copy of the list of non-repeating capture sequences (thread safe), clear the
         // list, then invoke the onAborted listeners for all capture sequences that were in progress
         // at the time abort was invoked.
-        val requestsToAbort = synchronized(activeCaptureSequences) {
-            val copy = activeCaptureSequences.toList()
-            activeCaptureSequences.clear()
-            copy
-        }
+        val requestsToAbort =
+            synchronized(activeCaptureSequences) {
+                val copy = activeCaptureSequences.toList()
+                activeCaptureSequences.clear()
+                copy
+            }
 
         // Invoke onAbort to indicate that the actual abort is about to happen.
         for (sequence in requestsToAbort) {
@@ -120,14 +124,14 @@
         }
 
         // This can fail for various reasons and may throw exceptions.
-        val captureSequence = captureSequenceProcessor.build(
-            isRepeating,
-            requests,
-            defaultParameters,
-            requiredParameters,
-            listeners,
-            activeBurstListener
-        )
+        val captureSequence =
+            captureSequenceProcessor.build(
+                isRepeating,
+                requests,
+                defaultParameters,
+                requiredParameters,
+                listeners,
+                activeBurstListener)
 
         // Reject incoming requests if this instance has been stopped or closed.
         if (captureSequence == null) {
@@ -152,9 +156,7 @@
 
         // Non-repeating requests must always be aware of abort calls.
         if (!captureSequence.repeating) {
-            synchronized(activeCaptureSequences) {
-                activeCaptureSequences.add(captureSequence)
-            }
+            synchronized(activeCaptureSequences) { activeCaptureSequences.add(captureSequence) }
         }
 
         var captured = false
@@ -167,16 +169,17 @@
             // invoked before this method call returns and sequenceNumber has been set on the
             // callback. Both this call and the synchronized behavior on the captureSequence have
             // been designed to minimize the number of synchronized calls.
-            val result = synchronized(lock = captureSequence) {
-                // Check closed state right before submitting.
-                if (closed.value) {
-                    Log.warn { "Did not submit $captureSequence, $this was closed!" }
-                    return false
+            val result =
+                synchronized(lock = captureSequence) {
+                    // Check closed state right before submitting.
+                    if (closed.value) {
+                        Log.warn { "Did not submit $captureSequence, $this was closed!" }
+                        return false
+                    }
+                    val sequenceNumber = captureSequenceProcessor.submit(captureSequence)
+                    captureSequence.sequenceNumber = sequenceNumber
+                    sequenceNumber
                 }
-                val sequenceNumber = captureSequenceProcessor.submit(captureSequence)
-                captureSequence.sequenceNumber = sequenceNumber
-                sequenceNumber
-            }
 
             if (result != -1) {
                 captureSequence.invokeOnRequestSequenceSubmitted()
@@ -210,20 +213,14 @@
      * abort was called.
      */
     private inline fun <T> CaptureSequence<T>.invokeOnAborted() {
-        invokeOnRequests { request, _, listener ->
-            listener.onAborted(request.request)
-        }
+        invokeOnRequests { request, _, listener -> listener.onAborted(request.request) }
     }
 
     private inline fun <T> CaptureSequence<T>.invokeOnRequestSequenceCreated() {
-        invokeOnRequests { request, _, listener ->
-            listener.onRequestSequenceCreated(request)
-        }
+        invokeOnRequests { request, _, listener -> listener.onRequestSequenceCreated(request) }
     }
 
     private inline fun <T> CaptureSequence<T>.invokeOnRequestSequenceSubmitted() {
-        invokeOnRequests { request, _, listener ->
-            listener.onRequestSequenceSubmitted(request)
-        }
+        invokeOnRequests { request, _, listener -> listener.onRequestSequenceSubmitted(request) }
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphState3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphState3A.kt
index 101070e..c7a4604 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphState3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphState3A.kt
@@ -122,4 +122,4 @@
             awbLock?.let { map[CaptureRequest.CONTROL_AWB_LOCK] = it }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Listener3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Listener3A.kt
index 988be7e..c145ab2 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Listener3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Listener3A.kt
@@ -30,8 +30,8 @@
 /**
  * A [Request.Listener] to receive partial and final metadata for each request sent to the camera
  * device. It maintains a list of [Result3AStateListener] to which it broadcasts the updates and
- * removes them as they are completed. This listener is useful for implementing 3A methods to
- * look for desired 3A state changes.
+ * removes them as they are completed. This listener is useful for implementing 3A methods to look
+ * for desired 3A state changes.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 @CameraGraphScope
@@ -75,4 +75,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt
index 2c8dd83..2048cdf 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt
@@ -56,13 +56,10 @@
     val result: Deferred<Result3A>
         get() = _result
 
-    @Volatile
-    private var frameNumberOfFirstUpdate: FrameNumber? = null
-    @Volatile
-    private var timestampOfFirstUpdateNs: Long? = null
+    @Volatile private var frameNumberOfFirstUpdate: FrameNumber? = null
+    @Volatile private var timestampOfFirstUpdateNs: Long? = null
 
-    @GuardedBy("this")
-    private var initialRequestNumber: RequestNumber? = null
+    @GuardedBy("this") private var initialRequestNumber: RequestNumber? = null
 
     override fun onRequestSequenceCreated(requestNumber: RequestNumber) {
         synchronized(this) {
@@ -97,8 +94,7 @@
         if (timeLimitNs != null &&
             timestampOfFirstUpdateNs != null &&
             currentTimestampNs != null &&
-            currentTimestampNs - timestampOfFirstUpdateNs > timeLimitNs
-        ) {
+            currentTimestampNs - timestampOfFirstUpdateNs > timeLimitNs) {
             _result.complete(Result3A(Result3A.Status.TIME_LIMIT_REACHED, frameMetadata))
             return true
         }
@@ -108,9 +104,9 @@
         }
 
         val frameNumberOfFirstUpdate = frameNumberOfFirstUpdate
-        if (frameNumberOfFirstUpdate != null && frameLimit != null &&
-            currentFrameNumber.value - frameNumberOfFirstUpdate.value > frameLimit
-        ) {
+        if (frameNumberOfFirstUpdate != null &&
+            frameLimit != null &&
+            currentFrameNumber.value - frameNumberOfFirstUpdate.value > frameLimit) {
             _result.complete(Result3A(Result3A.Status.FRAME_LIMIT_REACHED, frameMetadata))
             return true
         }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/StreamGraphImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/StreamGraphImpl.kt
index d03ed3b..34d9b26 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/StreamGraphImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/StreamGraphImpl.kt
@@ -43,24 +43,30 @@
 import kotlinx.atomicfu.atomic
 
 private val streamIds = atomic(0)
+
 internal fun nextStreamId(): StreamId = StreamId(streamIds.incrementAndGet())
 
 private val outputIds = atomic(0)
+
 internal fun nextOutputId(): OutputId = OutputId(outputIds.incrementAndGet())
 
 private val configIds = atomic(0)
+
 internal fun nextConfigId(): OutputConfigId = OutputConfigId(configIds.incrementAndGet())
 
 private val groupIds = atomic(0)
+
 internal fun nextGroupId(): Int = groupIds.incrementAndGet()
 
 /**
- * This object keeps track of which surfaces have been configured for each stream. In addition,
- * it will keep track of which surfaces have changed or replaced so that the CaptureSession can be
+ * This object keeps track of which surfaces have been configured for each stream. In addition, it
+ * will keep track of which surfaces have changed or replaced so that the CaptureSession can be
  * reconfigured if the configured surfaces change.
  */
 @CameraGraphScope
-internal class StreamGraphImpl @Inject constructor(
+internal class StreamGraphImpl
+@Inject
+constructor(
     cameraMetadata: CameraMetadata,
     graphConfig: CameraGraph.Config,
 ) : StreamGraph {
@@ -83,10 +89,8 @@
         val streamListBuilder = mutableListOf<CameraStream>()
         val streamMapBuilder = mutableMapOf<CameraStream.Config, CameraStream>()
 
-        val deferredOutputsAllowed = computeIfDeferredStreamsAreSupported(
-            cameraMetadata,
-            graphConfig
-        )
+        val deferredOutputsAllowed =
+            computeIfDeferredStreamsAreSupported(cameraMetadata, graphConfig)
 
         // Compute groupNumbers for buffer sharing.
         val groupNumbers = mutableMapOf<CameraStream.Config, Int>()
@@ -108,24 +112,25 @@
                 }
 
                 @SuppressWarnings("SyntheticAccessor")
-                val outputConfig = OutputConfig(
-                    nextConfigId(),
-                    output.size,
-                    output.format,
-                    output.camera ?: graphConfig.camera,
-                    groupNumber = groupNumbers[streamConfig],
-                    deferredOutputType = if (deferredOutputsAllowed) {
-                        (output as? OutputStream.Config.LazyOutputConfig)?.outputType
-                    } else {
-                        null
-                    },
-                    mirrorMode = output.mirrorMode,
-                    timestampBase = output.timestampBase,
-                    dynamicRangeProfile = output.dynamicRangeProfile,
-                    streamUseCase = output.streamUseCase,
-                    externalOutputConfig =
-                    (output as? OutputStream.Config.ExternalOutputConfig)?.output
-                )
+                val outputConfig =
+                    OutputConfig(
+                        nextConfigId(),
+                        output.size,
+                        output.format,
+                        output.camera ?: graphConfig.camera,
+                        groupNumber = groupNumbers[streamConfig],
+                        deferredOutputType =
+                            if (deferredOutputsAllowed) {
+                                (output as? OutputStream.Config.LazyOutputConfig)?.outputType
+                            } else {
+                                null
+                            },
+                        mirrorMode = output.mirrorMode,
+                        timestampBase = output.timestampBase,
+                        dynamicRangeProfile = output.dynamicRangeProfile,
+                        streamUseCase = output.streamUseCase,
+                        externalOutputConfig =
+                            (output as? OutputStream.Config.ExternalOutputConfig)?.output)
                 outputConfigMap[output] = outputConfig
                 outputConfigListBuilder.add(outputConfig)
             }
@@ -135,22 +140,24 @@
         for (streamConfigIdx in graphConfig.streams.indices) {
             val streamConfig = graphConfig.streams[streamConfigIdx]
 
-            val outputs = streamConfig.outputs.map {
-                val outputConfig = outputConfigMap[it]!!
+            val outputs =
+                streamConfig.outputs.map {
+                    val outputConfig = outputConfigMap[it]!!
 
-                @SuppressWarnings("SyntheticAccessor")
-                val outputStream = OutputStreamImpl(
-                    nextOutputId(),
-                    outputConfig.size,
-                    outputConfig.format,
-                    outputConfig.camera,
-                    outputConfig.mirrorMode,
-                    outputConfig.timestampBase,
-                    outputConfig.dynamicRangeProfile,
-                    outputConfig.streamUseCase,
-                )
-                outputStream
-            }
+                    @SuppressWarnings("SyntheticAccessor")
+                    val outputStream =
+                        OutputStreamImpl(
+                            nextOutputId(),
+                            outputConfig.size,
+                            outputConfig.format,
+                            outputConfig.camera,
+                            outputConfig.mirrorMode,
+                            outputConfig.timestampBase,
+                            outputConfig.dynamicRangeProfile,
+                            outputConfig.streamUseCase,
+                        )
+                    outputStream
+                }
 
             val stream = CameraStream(nextStreamId(), outputs)
             streamMapBuilder[streamConfig] = stream
@@ -255,10 +262,8 @@
             graphConfig.sessionMode == CameraGraph.OperatingMode.NORMAL &&
             hardwareLevel != INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY &&
             hardwareLevel != INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED &&
-            (
-                Build.VERSION.SDK_INT < Build.VERSION_CODES.P ||
-                    hardwareLevel != INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL
-                )
+            (Build.VERSION.SDK_INT < Build.VERSION_CODES.P ||
+                hardwareLevel != INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL)
     }
 
     override fun toString(): String {
@@ -269,4 +274,4 @@
 @JvmInline
 internal value class OutputConfigId(val value: Int) {
     override fun toString(): String = "OutputConfig-$value"
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/SurfaceGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/SurfaceGraph.kt
index 6f1b8e7..093483f 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/SurfaceGraph.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/SurfaceGraph.kt
@@ -25,7 +25,6 @@
 import androidx.camera.camera2.pipe.StreamId
 import androidx.camera.camera2.pipe.config.CameraGraphScope
 import androidx.camera.camera2.pipe.core.Log
-import java.io.Closeable
 import javax.inject.Inject
 
 /**
@@ -36,75 +35,78 @@
  */
 @RequiresApi(21)
 @CameraGraphScope
-internal class SurfaceGraph @Inject constructor(
+internal class SurfaceGraph
+@Inject
+constructor(
     private val streamGraph: StreamGraphImpl,
     private val cameraController: CameraController,
     private val surfaceManager: CameraSurfaceManager
 ) {
     private val lock = Any()
 
-    @GuardedBy("lock")
-    private val surfaceMap: MutableMap<StreamId, Surface> = mutableMapOf()
+    @GuardedBy("lock") private val surfaceMap: MutableMap<StreamId, Surface> = mutableMapOf()
 
     @GuardedBy("lock")
-    private val surfaceUsageMap: MutableMap<Surface, Closeable> = mutableMapOf()
+    private val surfaceUsageMap: MutableMap<Surface, AutoCloseable> = mutableMapOf()
 
-    @GuardedBy("lock")
-    private val closed: Boolean = false
+    @GuardedBy("lock") private val closed: Boolean = false
 
     operator fun set(streamId: StreamId, surface: Surface?) {
-        val closeable = synchronized(lock) {
-            if (closed) {
-                Log.warn { "Attempted to set $streamId to $surface after close!" }
-                return
-            }
-
-            Log.info {
-                if (surface != null) {
-                    "Configured $streamId to use $surface"
-                } else {
-                    "Removed surface for $streamId"
+        val closeable =
+            synchronized(lock) {
+                if (closed) {
+                    Log.warn { "Attempted to set $streamId to $surface after close!" }
+                    return
                 }
-            }
-            var oldSurfaceToken: Closeable? = null
 
-            if (surface == null) {
-                // TODO: Tell the graph processor that it should resubmit the repeating request or
-                //  reconfigure the camera2 captureSession
-                val oldSurface = surfaceMap.remove(streamId)
-                if (oldSurface != null) {
-                    oldSurfaceToken = surfaceUsageMap.remove(oldSurface)
-                }
-            } else {
-                val oldSurface = surfaceMap[streamId]
-                surfaceMap[streamId] = surface
-
-                if (oldSurface != surface) {
-                    check(!surfaceUsageMap.containsKey(surface)) {
-                        "Surface ($surface) is already in use!"
+                Log.info {
+                    if (surface != null) {
+                        "Configured $streamId to use $surface"
+                    } else {
+                        "Removed surface for $streamId"
                     }
-                    oldSurfaceToken = surfaceUsageMap.remove(oldSurface)
-                    val newToken = surfaceManager.registerSurface(surface)
-                    surfaceUsageMap[surface] = newToken
                 }
-            }
+                var oldSurfaceToken: AutoCloseable? = null
 
-            return@synchronized oldSurfaceToken
-        }
+                if (surface == null) {
+                    // TODO: Tell the graph processor that it should resubmit the repeating request
+                    // or
+                    //  reconfigure the camera2 captureSession
+                    val oldSurface = surfaceMap.remove(streamId)
+                    if (oldSurface != null) {
+                        oldSurfaceToken = surfaceUsageMap.remove(oldSurface)
+                    }
+                } else {
+                    val oldSurface = surfaceMap[streamId]
+                    surfaceMap[streamId] = surface
+
+                    if (oldSurface != surface) {
+                        check(!surfaceUsageMap.containsKey(surface)) {
+                            "Surface ($surface) is already in use!"
+                        }
+                        oldSurfaceToken = surfaceUsageMap.remove(oldSurface)
+                        val newToken = surfaceManager.registerSurface(surface)
+                        surfaceUsageMap[surface] = newToken
+                    }
+                }
+
+                return@synchronized oldSurfaceToken
+            }
         maybeUpdateSurfaces()
         closeable?.close()
     }
 
     fun close() {
-        val closeables = synchronized(lock) {
-            if (closed) {
-                return
+        val closeables =
+            synchronized(lock) {
+                if (closed) {
+                    return
+                }
+                surfaceMap.clear()
+                val tokensToClose = surfaceUsageMap.values.toList()
+                surfaceUsageMap.clear()
+                tokensToClose
             }
-            surfaceMap.clear()
-            val tokensToClose = surfaceUsageMap.values.toList()
-            surfaceUsageMap.clear()
-            tokensToClose
-        }
 
         for (closeable in closeables) {
             closeable.close()
@@ -123,22 +125,24 @@
         cameraController.updateSurfaceMap(surfaces)
     }
 
-    private fun buildSurfaceMap(): Map<StreamId, Surface> = synchronized(lock) {
-        val surfaces = mutableMapOf<StreamId, Surface>()
-        for (outputConfig in streamGraph.outputConfigs) {
-            for (stream in outputConfig.streamBuilder) {
-                val surface = surfaceMap[stream.id]
-                if (surface == null) {
-                    if (!outputConfig.deferrable) {
-                        // If output is non-deferrable, a surface must be available or the config
-                        // is not yet valid. Exit now with an empty map.
-                        return emptyMap()
+    private fun buildSurfaceMap(): Map<StreamId, Surface> =
+        synchronized(lock) {
+            val surfaces = mutableMapOf<StreamId, Surface>()
+            for (outputConfig in streamGraph.outputConfigs) {
+                for (stream in outputConfig.streamBuilder) {
+                    val surface = surfaceMap[stream.id]
+                    if (surface == null) {
+                        if (!outputConfig.deferrable) {
+                            // If output is non-deferrable, a surface must be available or the
+                            // config
+                            // is not yet valid. Exit now with an empty map.
+                            return emptyMap()
+                        }
+                    } else {
+                        surfaces[stream.id] = surface
                     }
-                } else {
-                    surfaces[stream.id] = surface
                 }
             }
+            return surfaces
         }
-        return surfaces
-    }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/CameraBackendsImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/CameraBackendsImpl.kt
index e7ed8fc..8c56d28 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/CameraBackendsImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/CameraBackendsImpl.kt
@@ -23,15 +23,14 @@
 import androidx.camera.camera2.pipe.CameraBackendId
 import androidx.camera.camera2.pipe.CameraBackends
 import androidx.camera.camera2.pipe.CameraContext
+import androidx.camera.camera2.pipe.config.CameraPipeContext
 import androidx.camera.camera2.pipe.core.Threads
 
-/**
- * Provides an implementation for interacting with CameraBackends.
- */
+/** Provides an implementation for interacting with CameraBackends. */
 internal class CameraBackendsImpl(
     private val defaultBackendId: CameraBackendId,
     private val cameraBackends: Map<CameraBackendId, CameraBackendFactory>,
-    private val appContext: Context,
+    @CameraPipeContext private val cameraPipeContext: Context,
     private val threads: Threads
 ) : CameraBackends {
     private val lock = Any()
@@ -39,10 +38,11 @@
     @GuardedBy("lock")
     private val activeCameraBackends = mutableMapOf<CameraBackendId, CameraBackend>()
 
-    override val default: CameraBackend = checkNotNull(get(defaultBackendId)) {
-        "Failed to load the default backend for $defaultBackendId! Available backends are " +
-            "${cameraBackends.keys}"
-    }
+    override val default: CameraBackend =
+        checkNotNull(get(defaultBackendId)) {
+            "Failed to load the default backend for $defaultBackendId! Available backends are " +
+                "${cameraBackends.keys}"
+        }
 
     override val allIds: Set<CameraBackendId>
         get() = cameraBackends.keys
@@ -54,9 +54,9 @@
             val existing = activeCameraBackends[backendId]
             if (existing != null) return existing
 
-            val backend = cameraBackends[backendId]?.create(
-                CameraBackendContext(appContext, threads, this)
-            )
+            val backend =
+                cameraBackends[backendId]?.create(
+                    CameraBackendContext(cameraPipeContext, threads, this))
             if (backend != null) {
                 check(backendId == backend.id) {
                     "Unexpected backend id! Expected $backendId but it was actually ${backend.id}"
@@ -72,4 +72,4 @@
         override val threads: Threads,
         override val cameraBackends: CameraBackends
     ) : CameraContext
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/CameraDevicesImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/CameraDevicesImpl.kt
new file mode 100644
index 0000000..26a313b
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/CameraDevicesImpl.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.internal
+
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraBackend
+import androidx.camera.camera2.pipe.CameraBackendId
+import androidx.camera.camera2.pipe.CameraBackends
+import androidx.camera.camera2.pipe.CameraDevices
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.Log
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Provides utilities for querying cameras and accessing metadata about those cameras. */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@Singleton
+internal class CameraDevicesImpl @Inject constructor(private val cameraBackends: CameraBackends) :
+    CameraDevices {
+
+    @Deprecated(
+        "findAll() is not able to specify a specific CameraBackendId to query.",
+        replaceWith = ReplaceWith("awaitCameraIds"),
+        level = DeprecationLevel.WARNING)
+    override fun findAll(): List<CameraId> = awaitCameraIds() ?: emptyList()
+
+    @Deprecated(
+        "ids() is not able to specify a specific CameraBackendId to query.",
+        replaceWith = ReplaceWith("getCameraIds"),
+        level = DeprecationLevel.WARNING)
+    override suspend fun ids(): List<CameraId> = getCameraIds() ?: emptyList()
+
+    @Deprecated(
+        "getMetadata() is not able to specify a specific CameraBackendId to query.",
+        replaceWith = ReplaceWith("getCameraMetadata"),
+        level = DeprecationLevel.WARNING)
+    override suspend fun getMetadata(camera: CameraId): CameraMetadata =
+        checkNotNull(getCameraMetadata(camera))
+
+    @Deprecated(
+        "awaitMetadata() is not able to specify a specific CameraBackendId to query.",
+        replaceWith = ReplaceWith("awaitCameraMetadata"),
+        level = DeprecationLevel.WARNING)
+    override fun awaitMetadata(camera: CameraId): CameraMetadata =
+        checkNotNull(awaitCameraMetadata(camera))
+
+    override suspend fun getCameraIds(cameraBackendId: CameraBackendId?): List<CameraId>? {
+        val cameraBackend = getCameraBackend(cameraBackendId)
+        val cameraIds = cameraBackend.getCameraIds()
+        if (cameraIds == null) {
+            Log.warn { "Failed to load cameraIds from ${cameraBackend.id}" }
+        }
+        return cameraIds
+    }
+
+    override fun awaitCameraIds(cameraBackendId: CameraBackendId?): List<CameraId>? {
+        val cameraBackend = getCameraBackend(cameraBackendId)
+        val cameraIds = cameraBackend.awaitCameraIds()
+        if (cameraIds == null) {
+            Log.warn { "Failed to load cameraIds from ${cameraBackend.id}" }
+        }
+        return cameraIds
+    }
+
+    override suspend fun getCameraMetadata(
+        cameraId: CameraId,
+        cameraBackendId: CameraBackendId?
+    ): CameraMetadata? {
+        val cameraBackend = getCameraBackend(cameraBackendId)
+        val metadata = cameraBackend.getCameraMetadata(cameraId)
+        if (metadata == null) {
+            Log.warn { "Failed to load metadata for $cameraId from ${cameraBackend.id}" }
+        }
+        return metadata
+    }
+
+    override fun awaitCameraMetadata(
+        cameraId: CameraId,
+        cameraBackendId: CameraBackendId?
+    ): CameraMetadata? {
+        val cameraBackend = getCameraBackend(cameraBackendId)
+        val metadata = cameraBackend.awaitCameraMetadata(cameraId)
+        if (metadata == null) {
+            Log.warn { "Failed to load metadata for $cameraId from ${cameraBackend.id}" }
+        }
+        return metadata
+    }
+
+    private fun getCameraBackend(cameraBackendId: CameraBackendId?): CameraBackend =
+        Debug.trace("getCameraBackend") {
+            val actualBackendId = cameraBackendId ?: cameraBackends.default.id
+            val backend = cameraBackends[actualBackendId]
+            checkNotNull(backend) { "Failed to load CameraBackend $actualBackendId" }
+        }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt
index 14026d0..ef64bab 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt
@@ -47,13 +47,12 @@
         val fakeCameraId = RobolectricCameras.create()
         val context = ApplicationProvider.getApplicationContext() as Context
         val cameraPipe = CameraPipe(CameraPipe.Config(context))
-        val cameraGraph = cameraPipe.create(
-            CameraGraph.Config(
-                camera = fakeCameraId,
-                streams = listOf(),
-                defaultTemplate = RequestTemplate(0)
-            )
-        )
+        val cameraGraph =
+            cameraPipe.create(
+                CameraGraph.Config(
+                    camera = fakeCameraId,
+                    streams = listOf(),
+                    defaultTemplate = RequestTemplate(0)))
         assertThat(cameraGraph).isNotNull()
     }
 
@@ -63,10 +62,10 @@
         val context = ApplicationProvider.getApplicationContext() as Context
         val cameraPipe = CameraPipe(CameraPipe.Config(context))
         val cameras = cameraPipe.cameras()
-        val cameraList = cameras.ids()
+        val cameraList = cameras.getCameraIds()
 
         assertThat(cameraList).isNotNull()
-        assertThat(cameraList.size).isEqualTo(1)
+        assertThat(cameraList!!.size).isEqualTo(1)
         assertThat(cameraList).contains(fakeCameraId)
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraSurfaceManagerTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraSurfaceManagerTest.kt
index 5891557..f8353e9 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraSurfaceManagerTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraSurfaceManagerTest.kt
@@ -34,15 +34,16 @@
     private val surfaceManager = CameraSurfaceManager()
     private var activeInvokeCount = 0
     private var inactiveInvokeCount = 0
-    private val listener = object : CameraSurfaceManager.SurfaceListener {
-        override fun onSurfaceActive(surface: Surface) {
-            activeInvokeCount++
-        }
+    private val listener =
+        object : CameraSurfaceManager.SurfaceListener {
+            override fun onSurfaceActive(surface: Surface) {
+                activeInvokeCount++
+            }
 
-        override fun onSurfaceInactive(surface: Surface) {
-            inactiveInvokeCount++
+            override fun onSurfaceInactive(surface: Surface) {
+                inactiveInvokeCount++
+            }
         }
-    }
     private val imageReader1 = ImageReader.newInstance(1280, 720, ImageFormat.YUV_420_888, 4)
     private val imageReader2 = ImageReader.newInstance(1920, 1080, ImageFormat.YUV_420_888, 4)
 
@@ -84,15 +85,16 @@
 
     @Test
     fun testRemoveListenerDuringSurfaceListener() {
-        surfaceManager.addListener(object : CameraSurfaceManager.SurfaceListener {
-            override fun onSurfaceActive(surface: Surface) {
-                // No-op
-            }
+        surfaceManager.addListener(
+            object : CameraSurfaceManager.SurfaceListener {
+                override fun onSurfaceActive(surface: Surface) {
+                    // No-op
+                }
 
-            override fun onSurfaceInactive(surface: Surface) {
-                surfaceManager.removeListener(this)
-            }
-        })
+                override fun onSurfaceInactive(surface: Surface) {
+                    surfaceManager.removeListener(this)
+                }
+            })
         surfaceManager.registerSurface(imageReader1.surface).close()
     }
 
@@ -167,4 +169,4 @@
         assertThat(activeInvokeCount).isEqualTo(1)
         assertThat(inactiveInvokeCount).isEqualTo(1)
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/ModeEnum3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/ModeEnum3ATest.kt
index 9f78329..9af5a51 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/ModeEnum3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/ModeEnum3ATest.kt
@@ -41,4 +41,4 @@
     fun iterateAwbModes() {
         assertThat(AwbMode.values().all { awbMode -> awbMode.value >= 0 }).isTrue()
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt
index a45add3..4f56868 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt
@@ -43,13 +43,12 @@
 
     @Test
     fun canReadCaptureParameters() {
-        val request = Request(
-            listOf(StreamId(1)),
-            parameters = mapOf(
-                CaptureRequest.EDGE_MODE to CaptureRequest.EDGE_MODE_HIGH_QUALITY
-            ),
-            extras = mapOf(FakeMetadata.TEST_KEY to 42)
-        )
+        val request =
+            Request(
+                listOf(StreamId(1)),
+                parameters =
+                    mapOf(CaptureRequest.EDGE_MODE to CaptureRequest.EDGE_MODE_HIGH_QUALITY),
+                extras = mapOf(FakeMetadata.TEST_KEY to 42))
 
         // Check with a valid test key
         assertThat(request[FakeMetadata.TEST_KEY]).isEqualTo(42)
@@ -68,9 +67,8 @@
         // Check with an invalid test key
         assertThat(request.get(CaptureRequest.CONTROL_AE_MODE)).isNull()
         assertThat(
-            request.getOrDefault(
-                CaptureRequest.CONTROL_AE_MODE, default = CaptureRequest.CONTROL_AE_MODE_ON
-            )
-        ).isEqualTo(CaptureRequest.CONTROL_AE_MODE_ON)
+                request.getOrDefault(
+                    CaptureRequest.CONTROL_AE_MODE, default = CaptureRequest.CONTROL_AE_MODE_ON))
+            .isEqualTo(CaptureRequest.CONTROL_AE_MODE_ON)
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamFormatTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamFormatTest.kt
index 87e9bf1..a0483ef 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamFormatTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamFormatTest.kt
@@ -47,4 +47,4 @@
         assertThat(StreamFormat(0x23).bitsPerPixel).isEqualTo(12)
         assertThat(StreamFormat.RAW10.bitsPerPixel).isEqualTo(10)
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamTest.kt
index 8216861..42fe60d 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamTest.kt
@@ -27,20 +27,14 @@
 @RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class StreamTest {
-    private val streamConfig1 = CameraStream.Config.create(
-        size = Size(640, 480),
-        format = StreamFormat.YUV_420_888
-    )
+    private val streamConfig1 =
+        CameraStream.Config.create(size = Size(640, 480), format = StreamFormat.YUV_420_888)
 
-    private val streamConfig2 = CameraStream.Config.create(
-        size = Size(640, 480),
-        format = StreamFormat.YUV_420_888
-    )
+    private val streamConfig2 =
+        CameraStream.Config.create(size = Size(640, 480), format = StreamFormat.YUV_420_888)
 
-    private val streamConfig3 = CameraStream.Config.create(
-        size = Size(640, 480),
-        format = StreamFormat.JPEG
-    )
+    private val streamConfig3 =
+        CameraStream.Config.create(size = Size(640, 480), format = StreamFormat.JPEG)
 
     @Test
     fun differentStreamConfigsAreNotEqual() {
@@ -63,10 +57,8 @@
 
     @Test
     fun sharedOutputsAreShared() {
-        val outputConfig = OutputStream.Config.create(
-            size = Size(640, 480),
-            format = StreamFormat.YUV_420_888
-        )
+        val outputConfig =
+            OutputStream.Config.create(size = Size(640, 480), format = StreamFormat.YUV_420_888)
         val sharedConfig1 = CameraStream.Config.create(outputConfig)
         val sharedConfig2 = CameraStream.Config.create(outputConfig)
         assertThat(sharedConfig1).isNotEqualTo(sharedConfig2)
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/AndroidCaptureSessionStateCallbackTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/AndroidCaptureSessionStateCallbackTest.kt
index 9e8006d..4e1e57f 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/AndroidCaptureSessionStateCallbackTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/AndroidCaptureSessionStateCallbackTest.kt
@@ -35,11 +35,12 @@
     private val stateCallback: CameraCaptureSessionWrapper.StateCallback = mock()
     private val previousStateCallback: CameraCaptureSessionWrapper.StateCallback = mock()
     private val captureSession: CameraCaptureSession = mock()
-    private val androidStateCallback = AndroidCaptureSessionStateCallback(
-        device = camera,
-        stateCallback = stateCallback,
-        lastStateCallback = previousStateCallback,
-    )
+    private val androidStateCallback =
+        AndroidCaptureSessionStateCallback(
+            device = camera,
+            stateCallback = stateCallback,
+            lastStateCallback = previousStateCallback,
+        )
 
     @Test
     fun normalMethodsAreForwardedToStateCallback() {
@@ -137,4 +138,4 @@
         verify(previousStateCallback, never()).onClosed(any())
         verify(previousStateCallback, times(1)).onSessionFinalized()
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessorTest.kt
index 6b45ae5..715854c 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessorTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessorTest.kt
@@ -57,81 +57,71 @@
     //  version of robolectric is dropped into AndroidX.
 
     private val mainLooper = Shadows.shadowOf(Looper.getMainLooper())
-    private val cameraId = RobolectricCameras.create(
-        mapOf(INFO_SUPPORTED_HARDWARE_LEVEL to INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
-    )
+    private val cameraId =
+        RobolectricCameras.create(
+            mapOf(INFO_SUPPORTED_HARDWARE_LEVEL to INFO_SUPPORTED_HARDWARE_LEVEL_FULL))
     private val testCamera = RobolectricCameras.open(cameraId)
 
-    private val stream1Config = CameraStream.Config.create(
-        Size(640, 480),
-        StreamFormat.YUV_420_888
-    )
+    private val stream1Config = CameraStream.Config.create(Size(640, 480), StreamFormat.YUV_420_888)
 
-    private val stream2Config = CameraStream.Config.create(
-        Size(1024, 768),
-        StreamFormat.RAW_PRIVATE
-    )
+    private val stream2Config =
+        CameraStream.Config.create(Size(1024, 768), StreamFormat.RAW_PRIVATE)
 
-    private val stream3Config = CameraStream.Config.create(
-        Size(1024, 768),
-        StreamFormat.PRIVATE,
-        streamUseCase = OutputStream.StreamUseCase.PREVIEW
-    )
+    private val stream3Config =
+        CameraStream.Config.create(
+            Size(1024, 768),
+            StreamFormat.PRIVATE,
+            streamUseCase = OutputStream.StreamUseCase.PREVIEW)
 
-    private val stream4Config = CameraStream.Config.create(
-        Size(1024, 768),
-        StreamFormat.PRIVATE,
-        streamUseCase = OutputStream.StreamUseCase.VIDEO_RECORD
-    )
+    private val stream4Config =
+        CameraStream.Config.create(
+            Size(1024, 768),
+            StreamFormat.PRIVATE,
+            streamUseCase = OutputStream.StreamUseCase.VIDEO_RECORD)
 
-    private val graphConfig = CameraGraph.Config(
-        camera = testCamera.cameraId,
-        streams = listOf(stream1Config, stream2Config),
-    )
+    private val graphConfig =
+        CameraGraph.Config(
+            camera = testCamera.cameraId,
+            streams = listOf(stream1Config, stream2Config),
+        )
 
-    private val highSpeedGraphConfig = CameraGraph.Config(
-        camera = testCamera.cameraId,
-        streams = listOf(stream3Config, stream4Config),
-        sessionMode = CameraGraph.OperatingMode.HIGH_SPEED
-    )
+    private val highSpeedGraphConfig =
+        CameraGraph.Config(
+            camera = testCamera.cameraId,
+            streams = listOf(stream3Config, stream4Config),
+            sessionMode = CameraGraph.OperatingMode.HIGH_SPEED)
 
-    private val streamGraph = StreamGraphImpl(
-        testCamera.metadata,
-        graphConfig
-    )
+    private val streamGraph = StreamGraphImpl(testCamera.metadata, graphConfig)
 
-    private val highSpeedStreamGraph = StreamGraphImpl(
-        testCamera.metadata,
-        highSpeedGraphConfig
-    )
+    private val highSpeedStreamGraph = StreamGraphImpl(testCamera.metadata, highSpeedGraphConfig)
 
-    private val surface1 = Surface(
-        SurfaceTexture(0).also {
-            val output = stream1Config.outputs.first()
-            it.setDefaultBufferSize(output.size.width, output.size.height)
-        }
-    )
+    private val surface1 =
+        Surface(
+            SurfaceTexture(0).also {
+                val output = stream1Config.outputs.first()
+                it.setDefaultBufferSize(output.size.width, output.size.height)
+            })
 
-    private val surface2 = Surface(
-        SurfaceTexture(0).also {
-            val output = stream2Config.outputs.first()
-            it.setDefaultBufferSize(output.size.width, output.size.height)
-        }
-    )
+    private val surface2 =
+        Surface(
+            SurfaceTexture(0).also {
+                val output = stream2Config.outputs.first()
+                it.setDefaultBufferSize(output.size.width, output.size.height)
+            })
 
-    private val surface3 = Surface(
-        SurfaceTexture(0).also {
-            val output = stream3Config.outputs.first()
-            it.setDefaultBufferSize(output.size.width, output.size.height)
-        }
-    )
+    private val surface3 =
+        Surface(
+            SurfaceTexture(0).also {
+                val output = stream3Config.outputs.first()
+                it.setDefaultBufferSize(output.size.width, output.size.height)
+            })
 
-    private val surface4 = Surface(
-        SurfaceTexture(0).also {
-            val output = stream4Config.outputs.first()
-            it.setDefaultBufferSize(output.size.width, output.size.height)
-        }
-    )
+    private val surface4 =
+        Surface(
+            SurfaceTexture(0).also {
+                val output = stream4Config.outputs.first()
+                it.setDefaultBufferSize(output.size.width, output.size.height)
+            })
 
     private val stream1 = streamGraph[stream1Config]!!
     private val stream2 = streamGraph[stream2Config]!!
@@ -139,8 +129,7 @@
     private val stream4 = highSpeedStreamGraph[stream4Config]!!
 
     private val fakeCameraDevice = FakeCameraDeviceWrapper(testCamera)
-    private val fakeCaptureSessionWrapper =
-        fakeCameraDevice.createFakeCaptureSession(null)
+    private val fakeCaptureSessionWrapper = fakeCameraDevice.createFakeCaptureSession(null)
 
     @After
     fun teardown() {
@@ -153,14 +142,11 @@
         val requestBuilder = fakeCameraDevice.fakeCamera.cameraDevice.createCaptureRequest(1)
         // Parameters
         requestBuilder.set(
-            CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH
-        )
+            CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH)
         requestBuilder.set(
-            CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
-        )
+            CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
         requestBuilder.set(
-            CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO
-        )
+            CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
 
         // Surfaces
         requestBuilder.addTarget(surface1)
@@ -178,30 +164,30 @@
 
     @Test
     fun requestIsCreatedAndSubmitted() = runTest {
-        val captureSequenceProcessor = Camera2CaptureSequenceProcessor(
-            fakeCaptureSessionWrapper,
-            FakeThreads.fromTestScope(this),
-            RequestTemplate(1),
-            mapOf(
-                stream1.id to surface1,
-                stream2.id to surface2
-            ),
-            streamGraph
-        )
+        val captureSequenceProcessor =
+            Camera2CaptureSequenceProcessor(
+                fakeCaptureSessionWrapper,
+                FakeThreads.fromTestScope(this),
+                RequestTemplate(1),
+                mapOf(stream1.id to surface1, stream2.id to surface2),
+                streamGraph)
 
-        val sequence = captureSequenceProcessor.build(
-            isRepeating = false,
-            requests = listOf(Request(listOf(stream1.id, stream2.id))),
-            defaultParameters = mapOf<Any, Any?>(
-                CaptureRequest.CONTROL_AE_MODE to CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH,
-                CaptureRequest.CONTROL_AF_MODE to CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
-            ),
-            requiredParameters = mapOf<Any, Any?>(
-                CaptureRequest.CONTROL_AF_MODE to CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO
-            ),
-            listeners = listOf(),
-            sequenceListener = FakeCaptureSequenceListener()
-        )
+        val sequence =
+            captureSequenceProcessor.build(
+                isRepeating = false,
+                requests = listOf(Request(listOf(stream1.id, stream2.id))),
+                defaultParameters =
+                    mapOf<Any, Any?>(
+                        CaptureRequest.CONTROL_AE_MODE to
+                            CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH,
+                        CaptureRequest.CONTROL_AF_MODE to
+                            CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE),
+                requiredParameters =
+                    mapOf<Any, Any?>(
+                        CaptureRequest.CONTROL_AF_MODE to
+                            CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO),
+                listeners = listOf(),
+                sequenceListener = FakeCaptureSequenceListener())
 
         val result = captureSequenceProcessor.submit(sequence!!)
 
@@ -214,23 +200,21 @@
 
     @Test
     fun requestIsSubmittedWithPartialSurfaces() = runTest {
-        val captureSequenceProcessor = Camera2CaptureSequenceProcessor(
-            fakeCaptureSessionWrapper,
-            FakeThreads.fromTestScope(this),
-            RequestTemplate(1),
-            mapOf(
-                stream1.id to surface1
-            ),
-            streamGraph
-        )
-        val captureSequence = captureSequenceProcessor.build(
-            isRepeating = false,
-            requests = listOf(Request(listOf(stream1.id, stream2.id))),
-            defaultParameters = mapOf<Any, Any?>(),
-            requiredParameters = mapOf<Any, Any?>(),
-            listeners = emptyList(),
-            sequenceListener = FakeCaptureSequenceListener()
-        )
+        val captureSequenceProcessor =
+            Camera2CaptureSequenceProcessor(
+                fakeCaptureSessionWrapper,
+                FakeThreads.fromTestScope(this),
+                RequestTemplate(1),
+                mapOf(stream1.id to surface1),
+                streamGraph)
+        val captureSequence =
+            captureSequenceProcessor.build(
+                isRepeating = false,
+                requests = listOf(Request(listOf(stream1.id, stream2.id))),
+                defaultParameters = mapOf<Any, Any?>(),
+                requiredParameters = mapOf<Any, Any?>(),
+                listeners = emptyList(),
+                sequenceListener = FakeCaptureSequenceListener())
         assertThat(captureSequence).isNotNull()
 
         val result = captureSequenceProcessor.submit(captureSequence!!)
@@ -239,48 +223,44 @@
 
     @Test
     fun requestIsNotSubmittedWithEmptySurfaceList() = runTest {
-        val captureSequenceProcessor = Camera2CaptureSequenceProcessor(
-            fakeCaptureSessionWrapper,
-            FakeThreads.fromTestScope(this),
-            RequestTemplate(1),
-            mapOf(
-                stream1.id to surface1
-            ),
-            streamGraph
-        )
+        val captureSequenceProcessor =
+            Camera2CaptureSequenceProcessor(
+                fakeCaptureSessionWrapper,
+                FakeThreads.fromTestScope(this),
+                RequestTemplate(1),
+                mapOf(stream1.id to surface1),
+                streamGraph)
 
         // Key part is that only stream1 has a surface, but stream2 is requested.
-        val captureSequence = captureSequenceProcessor.build(
-            isRepeating = false,
-            requests = listOf(Request(listOf(stream2.id))),
-            defaultParameters = mapOf<Any, Any?>(),
-            requiredParameters = mapOf<Any, Any?>(),
-            listeners = emptyList(),
-            sequenceListener = FakeCaptureSequenceListener()
-        )
+        val captureSequence =
+            captureSequenceProcessor.build(
+                isRepeating = false,
+                requests = listOf(Request(listOf(stream2.id))),
+                defaultParameters = mapOf<Any, Any?>(),
+                requiredParameters = mapOf<Any, Any?>(),
+                listeners = emptyList(),
+                sequenceListener = FakeCaptureSequenceListener())
 
         assertThat(captureSequence).isNull()
     }
 
     @Test
     fun requestMetaDataUnwrapsAsCameraCaptureSession() = runTest {
-        val captureSequenceProcessor = Camera2CaptureSequenceProcessor(
-            fakeCaptureSessionWrapper,
-            FakeThreads.fromTestScope(this),
-            RequestTemplate(1),
-            mapOf(
-                stream1.id to surface1
-            ),
-            streamGraph
-        )
-        val captureSequence = captureSequenceProcessor.build(
-            isRepeating = false,
-            requests = listOf(Request(listOf(stream1.id, stream2.id))),
-            defaultParameters = mapOf<Any, Any?>(),
-            requiredParameters = mapOf<Any, Any?>(),
-            listeners = emptyList(),
-            sequenceListener = FakeCaptureSequenceListener()
-        )
+        val captureSequenceProcessor =
+            Camera2CaptureSequenceProcessor(
+                fakeCaptureSessionWrapper,
+                FakeThreads.fromTestScope(this),
+                RequestTemplate(1),
+                mapOf(stream1.id to surface1),
+                streamGraph)
+        val captureSequence =
+            captureSequenceProcessor.build(
+                isRepeating = false,
+                requests = listOf(Request(listOf(stream1.id, stream2.id))),
+                defaultParameters = mapOf<Any, Any?>(),
+                requiredParameters = mapOf<Any, Any?>(),
+                listeners = emptyList(),
+                sequenceListener = FakeCaptureSequenceListener())
 
         assertThat(captureSequence).isNotNull()
         assertThat(captureSequence!!.captureMetadataList).isNotEmpty()
@@ -292,30 +272,30 @@
     }
 
     fun highSpeedRequestIsCreatedAndSubmitted() = runTest {
-        val captureSequenceProcessor = Camera2CaptureSequenceProcessor(
-            fakeCaptureSessionWrapper,
-            FakeThreads.fromTestScope(this),
-            RequestTemplate(1),
-            mapOf(
-                stream3.id to surface3,
-                stream4.id to surface4
-            ),
-            highSpeedStreamGraph
-        )
+        val captureSequenceProcessor =
+            Camera2CaptureSequenceProcessor(
+                fakeCaptureSessionWrapper,
+                FakeThreads.fromTestScope(this),
+                RequestTemplate(1),
+                mapOf(stream3.id to surface3, stream4.id to surface4),
+                highSpeedStreamGraph)
 
-        val sequence = captureSequenceProcessor.build(
-            isRepeating = false,
-            requests = listOf(Request(listOf(stream3.id, stream4.id))),
-            defaultParameters = mapOf<Any, Any?>(
-                CaptureRequest.CONTROL_AE_MODE to CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH,
-                CaptureRequest.CONTROL_AF_MODE to CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
-            ),
-            requiredParameters = mapOf<Any, Any?>(
-                CaptureRequest.CONTROL_AF_MODE to CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO
-            ),
-            listeners = listOf(),
-            sequenceListener = FakeCaptureSequenceListener()
-        )
+        val sequence =
+            captureSequenceProcessor.build(
+                isRepeating = false,
+                requests = listOf(Request(listOf(stream3.id, stream4.id))),
+                defaultParameters =
+                    mapOf<Any, Any?>(
+                        CaptureRequest.CONTROL_AE_MODE to
+                            CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH,
+                        CaptureRequest.CONTROL_AF_MODE to
+                            CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE),
+                requiredParameters =
+                    mapOf<Any, Any?>(
+                        CaptureRequest.CONTROL_AF_MODE to
+                            CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO),
+                listeners = listOf(),
+                sequenceListener = FakeCaptureSequenceListener())
 
         val result = captureSequenceProcessor.submit(sequence!!)
 
@@ -325,4 +305,4 @@
 
         // TODO: Add support for checking parameters when robolectric supports it.
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCacheTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCacheTest.kt
index c6345f1..ba53292 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCacheTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCacheTest.kt
@@ -39,36 +39,34 @@
 internal class Camera2MetadataCacheTest {
     @Test
     fun metadataIsCachedAndShimmed() = runTest {
-        val camera0 = RobolectricCameras.create(
-            mapOf(
-                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL to CameraCharacteristics
-                    .INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
-                CameraCharacteristics.SENSOR_ORIENTATION to 90,
-                CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK,
-                CameraCharacteristics.FLASH_INFO_AVAILABLE to true
-            )
-        )
+        val camera0 =
+            RobolectricCameras.create(
+                mapOf(
+                    CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL to
+                        CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+                    CameraCharacteristics.SENSOR_ORIENTATION to 90,
+                    CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK,
+                    CameraCharacteristics.FLASH_INFO_AVAILABLE to true))
 
-        val camera1 = RobolectricCameras.create(
-            mapOf(
-                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL to CameraCharacteristics
-                    .INFO_SUPPORTED_HARDWARE_LEVEL_3,
-                CameraCharacteristics.SENSOR_ORIENTATION to 0,
-                CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT,
-                CameraCharacteristics.FLASH_INFO_AVAILABLE to false
-            )
-        )
+        val camera1 =
+            RobolectricCameras.create(
+                mapOf(
+                    CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL to
+                        CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3,
+                    CameraCharacteristics.SENSOR_ORIENTATION to 0,
+                    CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT,
+                    CameraCharacteristics.FLASH_INFO_AVAILABLE to false))
 
-        val cache = Camera2MetadataCache(
-            RobolectricCameras.application,
-            FakeThreads.fromTestScope(this),
-            Permissions(RobolectricCameras.application),
-            CameraPipe.CameraMetadataConfig(),
-            SystemTimeSource()
-        )
+        val cache =
+            Camera2MetadataCache(
+                RobolectricCameras.application,
+                FakeThreads.fromTestScope(this),
+                Permissions(RobolectricCameras.application),
+                CameraPipe.CameraMetadataConfig(),
+                SystemTimeSource())
 
-        val metadata0 = cache.awaitMetadata(camera0)
-        val metadata1 = cache.awaitMetadata(camera1)
+        val metadata0 = cache.awaitCameraMetadata(camera0)
+        val metadata1 = cache.awaitCameraMetadata(camera1)
 
         // Check to make sure that metadata is not null, and that various properties do not crash
         // on older OS versions when accessed.
@@ -94,4 +92,4 @@
         assertThat(metadata1.physicalRequestKeys).isNotNull()
         assertThat(metadata1[CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL]).isEqualTo(3)
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactoryTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactoryTest.kt
index 2c8baea..c2bcc17 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactoryTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactoryTest.kt
@@ -23,6 +23,8 @@
 import android.util.Size
 import android.view.Surface
 import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
 import androidx.camera.camera2.pipe.CameraPipe
 import androidx.camera.camera2.pipe.CameraStream
 import androidx.camera.camera2.pipe.CameraSurfaceManager
@@ -98,34 +100,31 @@
         val stream1Output = stream1.outputs.first()
 
         val surfaceTexture = SurfaceTexture(0)
-        surfaceTexture.setDefaultBufferSize(
-            stream1Output.size.width,
-            stream1Output.size.height
-        )
+        surfaceTexture.setDefaultBufferSize(stream1Output.size.width, stream1Output.size.height)
         val surface = Surface(surfaceTexture)
 
-        val pendingOutputs = sessionFactory.create(
-            AndroidCameraDevice(
-                testCamera.metadata,
-                testCamera.cameraDevice,
-                testCamera.cameraId
-            ),
-            mapOf(stream1.id to surface),
-            captureSessionState = CaptureSessionState(
-                FakeGraphProcessor(),
-                sessionFactory,
-                object : Camera2CaptureSequenceProcessorFactory {
-                    override fun create(
-                        session: CameraCaptureSessionWrapper,
-                        surfaceMap: Map<StreamId, Surface>
-                    ): CaptureSequenceProcessor<Request, FakeCaptureSequence> =
-                        FakeCaptureSequenceProcessor()
-                },
-                CameraSurfaceManager(),
-                SystemTimeSource(),
-                this
+        val pendingOutputs =
+            sessionFactory.create(
+                AndroidCameraDevice(
+                    testCamera.metadata, testCamera.cameraDevice, testCamera.cameraId
+                ),
+                mapOf(stream1.id to surface),
+                captureSessionState =
+                CaptureSessionState(
+                    FakeGraphProcessor(),
+                    sessionFactory,
+                    object : Camera2CaptureSequenceProcessorFactory {
+                        override fun create(
+                            session: CameraCaptureSessionWrapper,
+                            surfaceMap: Map<StreamId, Surface>
+                        ): CaptureSequenceProcessor<Request, FakeCaptureSequence> =
+                            FakeCaptureSequenceProcessor()
+                    },
+                    CameraSurfaceManager(),
+                    SystemTimeSource(),
+                    this
+                )
             )
-        )
 
         assertThat(pendingOutputs).isNotNull()
         assertThat(pendingOutputs).isEmpty()
@@ -137,11 +136,12 @@
 @CameraGraphScope
 @Camera2ControllerScope
 @Component(
-    modules = [
+    modules =
+    [
         FakeCameraGraphModule::class,
         FakeCameraPipeModule::class,
-        Camera2CaptureSessionsModule::class
-    ]
+        Camera2CaptureSessionsModule::class,
+        FakeCamera2Module::class]
 )
 internal interface Camera2CaptureSessionTestComponent {
     fun graphConfig(): CameraGraph.Config
@@ -149,9 +149,7 @@
     fun streamMap(): StreamGraphImpl
 }
 
-/**
- * Utility module for testing the Dagger generated graph with a a reasonable default config.
- */
+/** Utility module for testing the Dagger generated graph with a a reasonable default config. */
 @Module(includes = [ThreadConfigModule::class, CameraPipeModules::class])
 class FakeCameraPipeModule(
     private val context: Context,
@@ -174,13 +172,27 @@
     @Provides
     @CameraGraphScope
     fun provideFakeGraphConfig(fakeCamera: RobolectricCameras.FakeCamera): CameraGraph.Config {
-        val stream = CameraStream.Config.create(
-            Size(640, 480),
-            StreamFormat.YUV_420_888
-        )
+        val stream = CameraStream.Config.create(Size(640, 480), StreamFormat.YUV_420_888)
         return CameraGraph.Config(
             camera = fakeCamera.cameraId,
             streams = listOf(stream),
         )
     }
+}
+
+@Module
+class FakeCamera2Module {
+    @Provides
+    @Singleton
+    internal fun provideFakeCamera2MetadataProvider(
+        fakeCamera: RobolectricCameras.FakeCamera
+    ): Camera2MetadataProvider = object : Camera2MetadataProvider {
+        override suspend fun getCameraMetadata(cameraId: CameraId): CameraMetadata {
+            return fakeCamera.metadata
+        }
+
+        override fun awaitCameraMetadata(cameraId: CameraId): CameraMetadata {
+            return fakeCamera.metadata
+        }
+    }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionStateTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionStateTest.kt
index a9f7c5b..578a55a 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionStateTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionStateTest.kt
@@ -49,17 +49,16 @@
 class CaptureSessionStateTest {
     private val fakeGraphListener: GraphListener = mock()
     private val fakeSurfaceListener: CameraSurfaceManager.SurfaceListener = mock()
-    private val cameraSurfaceManager = CameraSurfaceManager().also {
-        it.addListener(fakeSurfaceListener)
-    }
+    private val cameraSurfaceManager =
+        CameraSurfaceManager().also { it.addListener(fakeSurfaceListener) }
     private val fakeCaptureSequenceProcessor = FakeCaptureSequenceProcessor()
-    private val captureSequenceProcessorFactory = object : Camera2CaptureSequenceProcessorFactory {
-        override fun create(
-            session: CameraCaptureSessionWrapper,
-            surfaceMap: Map<StreamId, Surface>
-        ): CaptureSequenceProcessor<Request, FakeCaptureSequence> =
-            fakeCaptureSequenceProcessor
-    }
+    private val captureSequenceProcessorFactory =
+        object : Camera2CaptureSequenceProcessorFactory {
+            override fun create(
+                session: CameraCaptureSessionWrapper,
+                surfaceMap: Map<StreamId, Surface>
+            ): CaptureSequenceProcessor<Request, FakeCaptureSequence> = fakeCaptureSequenceProcessor
+        }
     private val timeSource = SystemTimeSource()
 
     private val surface1: Surface = Surface(SurfaceTexture(1))
@@ -68,10 +67,9 @@
     private val stream2: StreamId = StreamId(2)
     private val stream3Deferred: StreamId = StreamId(3)
 
-    private val captureSessionFactory = FakeCaptureSessionFactory(
-        requiredStreams = setOf(stream1, stream2),
-        deferrableStreams = setOf(stream3Deferred)
-    )
+    private val captureSessionFactory =
+        FakeCaptureSessionFactory(
+            requiredStreams = setOf(stream1, stream2), deferrableStreams = setOf(stream3Deferred))
 
     private val fakeCameraDevice: CameraDeviceWrapper = mock()
     private val fakeCaptureSession: CameraCaptureSessionWrapper = mock()
@@ -84,14 +82,14 @@
 
     @Test
     fun disconnectBeforeCameraDoesNotAcceptCamera() = runTest {
-        val state = CaptureSessionState(
-            fakeGraphListener,
-            captureSessionFactory,
-            captureSequenceProcessorFactory,
-            cameraSurfaceManager,
-            timeSource,
-            this
-        )
+        val state =
+            CaptureSessionState(
+                fakeGraphListener,
+                captureSessionFactory,
+                captureSequenceProcessorFactory,
+                cameraSurfaceManager,
+                timeSource,
+                this)
         // When disconnect is called first
         state.disconnect()
 
@@ -105,22 +103,17 @@
 
     @Test
     fun disconnectBeforeCameraCallsSurfaceListener() = runTest {
-        val state = CaptureSessionState(
-            fakeGraphListener,
-            captureSessionFactory,
-            captureSequenceProcessorFactory,
-            cameraSurfaceManager,
-            timeSource,
-            this
-        )
+        val state =
+            CaptureSessionState(
+                fakeGraphListener,
+                captureSessionFactory,
+                captureSequenceProcessorFactory,
+                cameraSurfaceManager,
+                timeSource,
+                this)
 
         // When surfaces are configured
-        state.configureSurfaceMap(
-            mapOf(
-                stream1 to surface1,
-                stream2 to surface2
-            )
-        )
+        state.configureSurfaceMap(mapOf(stream1 to surface1, stream2 to surface2))
         verify(fakeSurfaceListener, times(1)).onSurfaceActive(eq(surface1))
         verify(fakeSurfaceListener, times(1)).onSurfaceActive(eq(surface2))
 
@@ -136,22 +129,17 @@
 
     @Test
     fun disconnectAfterCameraSetDoesNotCallOnSurfaceInactive() = runTest {
-        val state = CaptureSessionState(
-            fakeGraphListener,
-            captureSessionFactory,
-            captureSequenceProcessorFactory,
-            cameraSurfaceManager,
-            timeSource,
-            this
-        )
+        val state =
+            CaptureSessionState(
+                fakeGraphListener,
+                captureSessionFactory,
+                captureSequenceProcessorFactory,
+                cameraSurfaceManager,
+                timeSource,
+                this)
 
         // When surfaces are configured
-        state.configureSurfaceMap(
-            mapOf(
-                stream1 to surface1,
-                stream2 to surface2
-            )
-        )
+        state.configureSurfaceMap(mapOf(stream1 to surface1, stream2 to surface2))
         verify(fakeSurfaceListener, times(1)).onSurfaceActive(eq(surface1))
         verify(fakeSurfaceListener, times(1)).onSurfaceActive(eq(surface2))
 
@@ -169,21 +157,16 @@
 
     @Test
     fun onSessionFinalizeCallsSurfaceListener() = runTest {
-        val state = CaptureSessionState(
-            fakeGraphListener,
-            captureSessionFactory,
-            captureSequenceProcessorFactory,
-            cameraSurfaceManager,
-            timeSource,
-            this
-        )
+        val state =
+            CaptureSessionState(
+                fakeGraphListener,
+                captureSessionFactory,
+                captureSequenceProcessorFactory,
+                cameraSurfaceManager,
+                timeSource,
+                this)
         // When surfaces are configured
-        state.configureSurfaceMap(
-            mapOf(
-                stream1 to surface1,
-                stream2 to surface2
-            )
-        )
+        state.configureSurfaceMap(mapOf(stream1 to surface1, stream2 to surface2))
         // And session is finalized
         state.onSessionFinalized()
 
@@ -196,21 +179,16 @@
 
     @Test
     fun onConfigureFailedCallsSurfaceListener() = runTest {
-        val state = CaptureSessionState(
-            fakeGraphListener,
-            captureSessionFactory,
-            captureSequenceProcessorFactory,
-            cameraSurfaceManager,
-            timeSource,
-            this
-        )
+        val state =
+            CaptureSessionState(
+                fakeGraphListener,
+                captureSessionFactory,
+                captureSequenceProcessorFactory,
+                cameraSurfaceManager,
+                timeSource,
+                this)
         // When surfaces are configured
-        state.configureSurfaceMap(
-            mapOf(
-                stream1 to surface1,
-                stream2 to surface2
-            )
-        )
+        state.configureSurfaceMap(mapOf(stream1 to surface1, stream2 to surface2))
         // And configuration fails
         state.onConfigureFailed(fakeCaptureSession)
 
@@ -223,21 +201,16 @@
 
     @Test
     fun onClosedCallsSurfaceListener() = runTest {
-        val state = CaptureSessionState(
-            fakeGraphListener,
-            captureSessionFactory,
-            captureSequenceProcessorFactory,
-            cameraSurfaceManager,
-            timeSource,
-            this
-        )
+        val state =
+            CaptureSessionState(
+                fakeGraphListener,
+                captureSessionFactory,
+                captureSequenceProcessorFactory,
+                cameraSurfaceManager,
+                timeSource,
+                this)
         // When surfaces are configured
-        state.configureSurfaceMap(
-            mapOf(
-                stream1 to surface1,
-                stream2 to surface2
-            )
-        )
+        state.configureSurfaceMap(mapOf(stream1 to surface1, stream2 to surface2))
         // And the capture session is closed
         state.onClosed(fakeCaptureSession)
 
@@ -247,4 +220,4 @@
         verify(fakeSurfaceListener, times(1)).onSurfaceInactive(eq(surface1))
         verify(fakeSurfaceListener, times(1)).onSurfaceInactive(eq(surface2))
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpenerTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpenerTest.kt
index 4f63c02..1bd2604 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpenerTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpenerTest.kt
@@ -55,70 +55,65 @@
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 class RetryingCameraStateOpenerTest {
     private val cameraId0 = CameraId("0")
-    private val cameraMetadataProvider = object : CameraMetadataProvider {
-        override suspend fun getMetadata(cameraId: CameraId): CameraMetadata =
-            FakeCameraMetadata(cameraId = cameraId)
+    private val camera2MetadataProvider =
+        object : Camera2MetadataProvider {
+            override suspend fun getCameraMetadata(cameraId: CameraId): CameraMetadata =
+                FakeCameraMetadata(cameraId = cameraId)
 
-        override fun awaitMetadata(cameraId: CameraId): CameraMetadata =
-            FakeCameraMetadata(cameraId = cameraId)
-    }
+            override fun awaitCameraMetadata(cameraId: CameraId): CameraMetadata =
+                FakeCameraMetadata(cameraId = cameraId)
+        }
 
     // TODO(lnishan): Consider mocking this object when Mockito works well with value classes.
-    private val cameraOpener = object : CameraOpener {
-        var toThrow: Throwable? = null
-        var numberOfOpens = 0
+    private val cameraOpener =
+        object : CameraOpener {
+            var toThrow: Throwable? = null
+            var numberOfOpens = 0
 
-        override fun openCamera(cameraId: CameraId, stateCallback: CameraDevice.StateCallback) {
-            numberOfOpens++
-            toThrow?.let {
-                throw it
+            override fun openCamera(cameraId: CameraId, stateCallback: CameraDevice.StateCallback) {
+                numberOfOpens++
+                toThrow?.let { throw it }
             }
         }
-    }
 
     private val fakeTimeSource = FakeTimeSource()
 
     private val cameraStateOpener =
-        CameraStateOpener(cameraOpener, cameraMetadataProvider, fakeTimeSource, null)
+        CameraStateOpener(cameraOpener, camera2MetadataProvider, fakeTimeSource, null)
 
-    private val cameraAvailabilityMonitor = object : CameraAvailabilityMonitor {
-        override suspend fun awaitAvailableCamera(
-            cameraId: CameraId,
-            timeoutMillis: Long
-        ): Boolean {
-            delay(timeoutMillis)
-            fakeTimeSource.currentTimestamp += DurationNs.fromMs(timeoutMillis)
-            return true
+    private val cameraAvailabilityMonitor =
+        object : CameraAvailabilityMonitor {
+            override suspend fun awaitAvailableCamera(
+                cameraId: CameraId,
+                timeoutMillis: Long
+            ): Boolean {
+                delay(timeoutMillis)
+                fakeTimeSource.currentTimestamp += DurationNs.fromMs(timeoutMillis)
+                return true
+            }
         }
-    }
 
     private val fakeDevicePolicyManager: DevicePolicyManagerWrapper = mock()
 
     private val retryingCameraStateOpener =
         RetryingCameraStateOpener(
-            cameraStateOpener,
-            cameraAvailabilityMonitor,
-            fakeTimeSource,
-            fakeDevicePolicyManager
-        )
+            cameraStateOpener, cameraAvailabilityMonitor, fakeTimeSource, fakeDevicePolicyManager)
 
     // TODO(lnishan): Consider mocking this object when Mockito works well with value classes.
-    private val fakeGraphListener = object : GraphListener {
-        var numberOfErrorCalls = 0
+    private val fakeGraphListener =
+        object : GraphListener {
+            var numberOfErrorCalls = 0
 
-        override fun onGraphStarted(requestProcessor: GraphRequestProcessor) {
-        }
+            override fun onGraphStarted(requestProcessor: GraphRequestProcessor) {}
 
-        override fun onGraphStopped(requestProcessor: GraphRequestProcessor) {
-        }
+            override fun onGraphStopped(requestProcessor: GraphRequestProcessor) {}
 
-        override fun onGraphModified(requestProcessor: GraphRequestProcessor) {
-        }
+            override fun onGraphModified(requestProcessor: GraphRequestProcessor) {}
 
-        override fun onGraphError(graphStateError: GraphStateError) {
-            numberOfErrorCalls++
+            override fun onGraphError(graphStateError: GraphStateError) {
+                numberOfErrorCalls++
+            }
         }
-    }
 
     @Test
     fun testShouldRetryReturnsTrueWithinTimeout() {
@@ -126,14 +121,13 @@
         fakeTimeSource.currentTimestamp = TimestampNs(1_000_000_000L) // 1 second
 
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_CAMERA_IN_USE,
-                1,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                false
-            )
-        ).isTrue()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_CAMERA_IN_USE,
+                    1,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    false))
+            .isTrue()
     }
 
     @Test
@@ -142,14 +136,13 @@
         fakeTimeSource.currentTimestamp = TimestampNs(30_000_000_000L) // 30 seconds
 
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_CAMERA_IN_USE,
-                1,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                false
-            )
-        ).isFalse()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_CAMERA_IN_USE,
+                    1,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    false))
+            .isFalse()
     }
 
     @Test
@@ -158,14 +151,13 @@
         fakeTimeSource.currentTimestamp = TimestampNs(1_000_000_000L) // 1 second
 
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_UNDETERMINED,
-                1,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                false
-            )
-        ).isFalse()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_UNDETERMINED,
+                    1,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    false))
+            .isFalse()
     }
 
     @Test
@@ -174,23 +166,18 @@
         fakeTimeSource.currentTimestamp = TimestampNs(1_000_000_000L) // 1 second
 
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_CAMERA_IN_USE,
-                1,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                false
-            )
-        ).isTrue()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_CAMERA_IN_USE,
+                    1,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    false))
+            .isTrue()
 
         // The second retry attempt should fail if SDK version < S, and succeed otherwise.
-        val secondRetry = RetryingCameraStateOpener.shouldRetry(
-            CameraError.ERROR_CAMERA_IN_USE,
-            2,
-            firstAttemptTimestamp,
-            fakeTimeSource,
-            false
-        )
+        val secondRetry =
+            RetryingCameraStateOpener.shouldRetry(
+                CameraError.ERROR_CAMERA_IN_USE, 2, firstAttemptTimestamp, fakeTimeSource, false)
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
             assertThat(secondRetry).isFalse()
         } else {
@@ -204,25 +191,23 @@
         fakeTimeSource.currentTimestamp = TimestampNs(1_000_000_000L) // 1 second
 
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_CAMERA_LIMIT_EXCEEDED,
-                1,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                false
-            )
-        ).isTrue()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_CAMERA_LIMIT_EXCEEDED,
+                    1,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    false))
+            .isTrue()
 
         // Second attempt should succeed as well.
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_CAMERA_LIMIT_EXCEEDED,
-                2,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                false
-            )
-        ).isTrue()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_CAMERA_LIMIT_EXCEEDED,
+                    2,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    false))
+            .isTrue()
     }
 
     @Test
@@ -231,25 +216,23 @@
         fakeTimeSource.currentTimestamp = TimestampNs(1_000_000_000L) // 1 second
 
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_CAMERA_DISABLED,
-                1,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                camerasDisabledByDevicePolicy = true
-            )
-        ).isTrue()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_CAMERA_DISABLED,
+                    1,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    camerasDisabledByDevicePolicy = true))
+            .isTrue()
 
         // Second attempt should fail if camera is disabled.
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_CAMERA_DISABLED,
-                2,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                camerasDisabledByDevicePolicy = true
-            )
-        ).isFalse()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_CAMERA_DISABLED,
+                    2,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    camerasDisabledByDevicePolicy = true))
+            .isFalse()
     }
 
     @Test
@@ -258,25 +241,23 @@
         fakeTimeSource.currentTimestamp = TimestampNs(1_000_000_000L) // 1 second
 
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_CAMERA_DISABLED,
-                1,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                camerasDisabledByDevicePolicy = false
-            )
-        ).isTrue()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_CAMERA_DISABLED,
+                    1,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    camerasDisabledByDevicePolicy = false))
+            .isTrue()
 
         // Second attempt should success if camera is not disabled.
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_CAMERA_DISABLED,
-                2,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                camerasDisabledByDevicePolicy = false
-            )
-        ).isTrue()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_CAMERA_DISABLED,
+                    2,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    camerasDisabledByDevicePolicy = false))
+            .isTrue()
     }
 
     @Test
@@ -285,25 +266,23 @@
         fakeTimeSource.currentTimestamp = TimestampNs(1_000_000_000L) // 1 second
 
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_CAMERA_DEVICE,
-                1,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                false
-            )
-        ).isTrue()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_CAMERA_DEVICE,
+                    1,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    false))
+            .isTrue()
 
         // Second attempt should succeed as well.
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_CAMERA_DEVICE,
-                2,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                false
-            )
-        ).isTrue()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_CAMERA_DEVICE,
+                    2,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    false))
+            .isTrue()
     }
 
     @Test
@@ -312,25 +291,23 @@
         fakeTimeSource.currentTimestamp = TimestampNs(1_000_000_000L) // 1 second
 
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_CAMERA_SERVICE,
-                1,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                false
-            )
-        ).isTrue()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_CAMERA_SERVICE,
+                    1,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    false))
+            .isTrue()
 
         // Second attempt should succeed as well.
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_CAMERA_SERVICE,
-                2,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                false
-            )
-        ).isTrue()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_CAMERA_SERVICE,
+                    2,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    false))
+            .isTrue()
     }
 
     @Test
@@ -339,25 +316,23 @@
         fakeTimeSource.currentTimestamp = TimestampNs(1_000_000_000L) // 1 second
 
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_CAMERA_DISCONNECTED,
-                1,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                false
-            )
-        ).isTrue()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_CAMERA_DISCONNECTED,
+                    1,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    false))
+            .isTrue()
 
         // Second attempt should succeed as well.
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_CAMERA_DISCONNECTED,
-                2,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                false
-            )
-        ).isTrue()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_CAMERA_DISCONNECTED,
+                    2,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    false))
+            .isTrue()
     }
 
     @Test
@@ -366,25 +341,23 @@
         fakeTimeSource.currentTimestamp = TimestampNs(1_000_000_000L) // 1 second
 
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_ILLEGAL_ARGUMENT_EXCEPTION,
-                1,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                false
-            )
-        ).isTrue()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_ILLEGAL_ARGUMENT_EXCEPTION,
+                    1,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    false))
+            .isTrue()
 
         // Second attempt should succeed as well.
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_ILLEGAL_ARGUMENT_EXCEPTION,
-                2,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                false
-            )
-        ).isTrue()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_ILLEGAL_ARGUMENT_EXCEPTION,
+                    2,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    false))
+            .isTrue()
     }
 
     @Test
@@ -393,32 +366,29 @@
         fakeTimeSource.currentTimestamp = TimestampNs(1_000_000_000L) // 1 second
 
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_SECURITY_EXCEPTION,
-                1,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                false
-            )
-        ).isTrue()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_SECURITY_EXCEPTION,
+                    1,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    false))
+            .isTrue()
 
         // Second attempt should fail.
         assertThat(
-            RetryingCameraStateOpener.shouldRetry(
-                CameraError.ERROR_SECURITY_EXCEPTION,
-                2,
-                firstAttemptTimestamp,
-                fakeTimeSource,
-                false
-            )
-        ).isFalse()
+                RetryingCameraStateOpener.shouldRetry(
+                    CameraError.ERROR_SECURITY_EXCEPTION,
+                    2,
+                    firstAttemptTimestamp,
+                    fakeTimeSource,
+                    false))
+            .isFalse()
     }
 
     @Test
     fun cameraStateOpenerReturnsCorrectError() = runTest {
         cameraOpener.toThrow = CameraAccessException(CameraAccessException.CAMERA_IN_USE)
-        val result =
-            cameraStateOpener.tryOpenCamera(cameraId0, 1, Timestamps.now(fakeTimeSource))
+        val result = cameraStateOpener.tryOpenCamera(cameraId0, 1, Timestamps.now(fakeTimeSource))
 
         assertThat(result.errorCode).isEqualTo(ERROR_CAMERA_IN_USE)
     }
@@ -575,4 +545,4 @@
         // exactly 1.
         assertThat(fakeGraphListener.numberOfErrorCalls).isEqualTo(1)
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/VirtualCameraTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/VirtualCameraTest.kt
index f92e78d..063d350 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/VirtualCameraTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/VirtualCameraTest.kt
@@ -85,23 +85,18 @@
         // changes that it receives those changes and can be subsequently disconnected, which stops
         // additional events from being passed to the virtual camera instance.
         val virtualCamera = VirtualCameraState(cameraId)
-        val cameraState = flowOf(
-            CameraStateOpen(
-                AndroidCameraDevice(
-                    testCamera.metadata,
-                    testCamera.cameraDevice,
-                    testCamera.cameraId
-                )
-            )
-        )
+        val cameraState =
+            flowOf(
+                CameraStateOpen(
+                    AndroidCameraDevice(
+                        testCamera.metadata, testCamera.cameraDevice, testCamera.cameraId)))
         virtualCamera.connect(
             cameraState,
             object : Token {
                 override fun release(): Boolean {
                     return true
                 }
-            }
-        )
+            })
 
         virtualCamera.state.first { it !is CameraStateUnopened }
 
@@ -119,28 +114,19 @@
         // This tests that a listener attached to the virtualCamera.state property will receive all
         // of the events, starting from CameraStateUnopened.
         val virtualCamera = VirtualCameraState(cameraId)
-        val states = listOf(
-            CameraStateOpen(
-                AndroidCameraDevice(
-                    testCamera.metadata,
-                    testCamera.cameraDevice,
-                    testCamera.cameraId
-                )
-            ),
-            CameraStateClosing(),
-            CameraStateClosed(
-                cameraId,
-                ClosedReason.CAMERA2_ERROR,
-                cameraErrorCode = CameraError.ERROR_CAMERA_SERVICE
-            )
-        )
+        val states =
+            listOf(
+                CameraStateOpen(
+                    AndroidCameraDevice(
+                        testCamera.metadata, testCamera.cameraDevice, testCamera.cameraId)),
+                CameraStateClosing(),
+                CameraStateClosed(
+                    cameraId,
+                    ClosedReason.CAMERA2_ERROR,
+                    cameraErrorCode = CameraError.ERROR_CAMERA_SERVICE))
 
         val events = mutableListOf<CameraState>()
-        val job = launch {
-            virtualCamera.state.collect {
-                events.add(it)
-            }
-        }
+        val job = launch { virtualCamera.state.collect { events.add(it) } }
 
         virtualCamera.connect(
             states.asFlow(),
@@ -148,8 +134,7 @@
                 override fun release(): Boolean {
                     return true
                 }
-            }
-        )
+            })
 
         advanceUntilIdle()
         job.cancelAndJoin()
@@ -176,13 +161,13 @@
     @Test
     fun cameraOpensAndGeneratesStats() {
         mainLooper.idleFor(200, TimeUnit.MILLISECONDS)
-        val listener = AndroidCameraState(
-            testCamera.cameraId,
-            testCamera.metadata,
-            attemptNumber = 1,
-            attemptTimestampNanos = now,
-            timeSource
-        )
+        val listener =
+            AndroidCameraState(
+                testCamera.cameraId,
+                testCamera.metadata,
+                attemptNumber = 1,
+                attemptTimestampNanos = now,
+                timeSource)
 
         assertThat(listener.state.value).isInstanceOf(CameraStateUnopened.javaClass)
 
@@ -192,10 +177,9 @@
 
         assertThat(listener.state.value).isInstanceOf(CameraStateOpen::class.java)
         assertThat(
-            (listener.state.value as CameraStateOpen)
-                .cameraDevice
-                .unwrapAs(CameraDevice::class)
-        )
+                (listener.state.value as CameraStateOpen)
+                    .cameraDevice
+                    .unwrapAs(CameraDevice::class))
             .isSameInstanceAs(testCamera.cameraDevice)
 
         mainLooper.idleFor(1000, TimeUnit.MILLISECONDS)
@@ -220,13 +204,13 @@
 
     @Test
     fun multipleCloseEventsReportFirstEvent() {
-        val listener = AndroidCameraState(
-            testCamera.cameraId,
-            testCamera.metadata,
-            attemptNumber = 1,
-            attemptTimestampNanos = now,
-            timeSource
-        )
+        val listener =
+            AndroidCameraState(
+                testCamera.cameraId,
+                testCamera.metadata,
+                attemptNumber = 1,
+                attemptTimestampNanos = now,
+                timeSource)
 
         listener.onDisconnected(testCamera.cameraDevice)
         listener.onError(testCamera.cameraDevice, CameraDevice.StateCallback.ERROR_CAMERA_SERVICE)
@@ -240,13 +224,13 @@
 
     @Test
     fun closingStateReportsAppClose() {
-        val listener = AndroidCameraState(
-            testCamera.cameraId,
-            testCamera.metadata,
-            attemptNumber = 1,
-            attemptTimestampNanos = now,
-            timeSource
-        )
+        val listener =
+            AndroidCameraState(
+                testCamera.cameraId,
+                testCamera.metadata,
+                attemptNumber = 1,
+                attemptTimestampNanos = now,
+                timeSource)
 
         listener.close()
         mainLooper.idle()
@@ -257,13 +241,13 @@
 
     @Test
     fun closingWithExceptionIsReported() {
-        val listener = AndroidCameraState(
-            testCamera.cameraId,
-            testCamera.metadata,
-            attemptNumber = 1,
-            attemptTimestampNanos = now,
-            timeSource
-        )
+        val listener =
+            AndroidCameraState(
+                testCamera.cameraId,
+                testCamera.metadata,
+                attemptNumber = 1,
+                attemptTimestampNanos = now,
+                timeSource)
 
         listener.closeWith(IllegalArgumentException("Test Exception"))
         mainLooper.idle()
@@ -274,13 +258,13 @@
 
     @Test
     fun errorCodesAreReported() {
-        val listener = AndroidCameraState(
-            testCamera.cameraId,
-            testCamera.metadata,
-            attemptNumber = 1,
-            attemptTimestampNanos = now,
-            timeSource
-        )
+        val listener =
+            AndroidCameraState(
+                testCamera.cameraId,
+                testCamera.metadata,
+                attemptNumber = 1,
+                attemptTimestampNanos = now,
+                timeSource)
 
         listener.onError(testCamera.cameraDevice, CameraDevice.StateCallback.ERROR_CAMERA_SERVICE)
         mainLooper.idle()
@@ -290,4 +274,4 @@
         assertThat(closedState.cameraErrorCode).isEqualTo(CameraError.ERROR_CAMERA_SERVICE)
         assertThat(closedState.cameraException).isNull()
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/config/CameraPipeComponentTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/config/CameraPipeComponentTest.kt
index d343347..6e4969e 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/config/CameraPipeComponentTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/config/CameraPipeComponentTest.kt
@@ -50,16 +50,18 @@
     @Test
     fun createCameraGraphComponent() {
         val context = ApplicationProvider.getApplicationContext() as Context
-        val component = DaggerCameraPipeComponent.builder()
-            .cameraPipeConfigModule(CameraPipeConfigModule(CameraPipe.Config(context)))
-            .threadConfigModule(ThreadConfigModule(CameraPipe.ThreadConfig()))
-            .build()
+        val component =
+            DaggerCameraPipeComponent.builder()
+                .cameraPipeConfigModule(CameraPipeConfigModule(CameraPipe.Config(context)))
+                .threadConfigModule(ThreadConfigModule(CameraPipe.ThreadConfig()))
+                .build()
 
         val cameraId = fakeCameraId
-        val config = CameraGraph.Config(
-            camera = cameraId,
-            streams = listOf(),
-        )
+        val config =
+            CameraGraph.Config(
+                camera = cameraId,
+                streams = listOf(),
+            )
         val module = CameraGraphConfigModule(config)
         val builder = component.cameraGraphComponentBuilder()
         builder.cameraGraphConfigModule(module)
@@ -70,23 +72,24 @@
     @Test
     fun createCameraGraph() {
         val context = ApplicationProvider.getApplicationContext() as Context
-        val component = DaggerCameraPipeComponent.builder()
-            .cameraPipeConfigModule(CameraPipeConfigModule(CameraPipe.Config(context)))
-            .threadConfigModule(ThreadConfigModule(CameraPipe.ThreadConfig()))
-            .build()
+        val component =
+            DaggerCameraPipeComponent.builder()
+                .cameraPipeConfigModule(CameraPipeConfigModule(CameraPipe.Config(context)))
+                .threadConfigModule(ThreadConfigModule(CameraPipe.ThreadConfig()))
+                .build()
 
-        val graphComponent = component.cameraGraphComponentBuilder()
-            .cameraGraphConfigModule(
-                CameraGraphConfigModule(
-                    CameraGraph.Config(
-                        camera = fakeCameraId,
-                        streams = listOf(),
-                    )
-                )
-            )
-            .build()
+        val graphComponent =
+            component
+                .cameraGraphComponentBuilder()
+                .cameraGraphConfigModule(
+                    CameraGraphConfigModule(
+                        CameraGraph.Config(
+                            camera = fakeCameraId,
+                            streams = listOf(),
+                        )))
+                .build()
 
         val graph = graphComponent.cameraGraph()
         assertThat(graph).isNotNull()
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/TimestampsTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/TimestampsTest.kt
index a12e56c..3e2c15f 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/TimestampsTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/TimestampsTest.kt
@@ -43,4 +43,4 @@
         val duration = DurationNs.fromMs(500L)
         assertThat(duration.value).isEqualTo(500_000_000L)
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/TokenLockTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/TokenLockTest.kt
index 20d3057..51dadf2 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/TokenLockTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/TokenLockTest.kt
@@ -79,9 +79,7 @@
         val token1 = tokenLock.acquire(1)
 
         // This should suspend, and then cancel the request for the token.
-        val token2: TokenLock.Token? = withTimeoutOrNull(10) {
-            tokenLock.acquire(2)
-        }
+        val token2: TokenLock.Token? = withTimeoutOrNull(10) { tokenLock.acquire(2) }
 
         assertThat(token2).isNull()
         assertThat(tokenLock.available).isEqualTo(1)
@@ -252,9 +250,7 @@
     fun tokensAreClosedWithUseKeyword() = runBlocking {
         val tokenLock = TokenLockImpl(1)
 
-        tokenLock.acquire(1).use {
-            assertThat(tokenLock.size).isEqualTo(1)
-        }
+        tokenLock.acquire(1).use { assertThat(tokenLock.size).isEqualTo(1) }
         assertThat(tokenLock.size).isEqualTo(0)
     }
 
@@ -262,9 +258,7 @@
     fun testWithTokenExtension() = runBlocking {
         val tokenLock = TokenLockImpl(1)
 
-        tokenLock.withToken(1) {
-            assertThat(tokenLock.size).isEqualTo(1)
-        }
+        tokenLock.withToken(1) { assertThat(tokenLock.size).isEqualTo(1) }
         assertThat(tokenLock.size).isEqualTo(0)
     }
 
@@ -321,4 +315,4 @@
         assertThat(tokenLock.size).isEqualTo(2)
         assertThat(tokenLock.available).isEqualTo(0)
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/WakeLockTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/WakeLockTest.kt
index 993d7a1..97b8ee5 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/WakeLockTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/WakeLockTest.kt
@@ -33,9 +33,7 @@
     fun testWakeLockInvokesCallbackAfterTokenIsReleased() = runTest {
         val result = CompletableDeferred<Boolean>()
 
-        val wakelock = WakeLock(this) {
-            result.complete(true)
-        }
+        val wakelock = WakeLock(this) { result.complete(true) }
 
         wakelock.acquire()!!.release()
         assertThat(result.await()).isTrue()
@@ -45,9 +43,7 @@
     fun testWakelockDoesNotCompleteUntilAllTokensAreReleased() = runTest {
         val result = CompletableDeferred<Boolean>()
 
-        val wakelock = WakeLock(this) {
-            result.complete(true)
-        }
+        val wakelock = WakeLock(this) { result.complete(true) }
 
         val token1 = wakelock.acquire()!!
         val token2 = wakelock.acquire()!!
@@ -64,10 +60,15 @@
     @Test
     fun testClosingWakelockInvokesCallback() = runTest {
         val result = CompletableDeferred<Boolean>()
-        val wakelock = WakeLock(this, 100) {
-            result.complete(true)
-        }
+        val wakelock = WakeLock(this, 100) { result.complete(true) }
         wakelock.release()
         assertThat(result.await()).isTrue()
     }
-}
\ No newline at end of file
+
+    @Test
+    fun testWakeLockCompletesWhenStartTimeoutOnCreation() = runTest {
+        val result = CompletableDeferred<Boolean>()
+        WakeLock(this, 100, startTimeoutOnCreation = true) { result.complete(true) }
+        assertThat(result.await()).isTrue()
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt
index 5b1200b..6c4489a 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt
@@ -57,81 +57,65 @@
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class CameraGraphImplTest {
     private val context = ApplicationProvider.getApplicationContext() as Context
-    private val metadata = FakeCameraMetadata(
-        mapOf(INFO_SUPPORTED_HARDWARE_LEVEL to INFO_SUPPORTED_HARDWARE_LEVEL_FULL),
-    )
+    private val metadata =
+        FakeCameraMetadata(
+            mapOf(INFO_SUPPORTED_HARDWARE_LEVEL to INFO_SUPPORTED_HARDWARE_LEVEL_FULL),
+        )
     private val fakeGraphProcessor = FakeGraphProcessor()
     private val imageReader1 = ImageReader.newInstance(1280, 720, ImageFormat.YUV_420_888, 4)
     private val imageReader2 = ImageReader.newInstance(1920, 1080, ImageFormat.YUV_420_888, 4)
     private val fakeSurfaceListener: CameraSurfaceManager.SurfaceListener = mock()
     private val cameraSurfaceManager = CameraSurfaceManager()
 
-    private val stream1Config = CameraStream.Config.create(
-        Size(1280, 720),
-        StreamFormat.YUV_420_888
-    )
-    private val stream2Config = CameraStream.Config.create(
-        Size(1920, 1080),
-        StreamFormat.YUV_420_888
-    )
+    private val stream1Config =
+        CameraStream.Config.create(Size(1280, 720), StreamFormat.YUV_420_888)
+    private val stream2Config =
+        CameraStream.Config.create(Size(1920, 1080), StreamFormat.YUV_420_888)
 
     private lateinit var cameraController: CameraControllerSimulator
     private lateinit var stream1: CameraStream
     private lateinit var stream2: CameraStream
 
     private fun initializeCameraGraphImpl(scope: TestScope): CameraGraphImpl {
-        val graphConfig = CameraGraph.Config(
-            camera = metadata.camera,
-            streams = listOf(
-                stream1Config,
-                stream2Config
-            ),
-        )
+        val graphConfig =
+            CameraGraph.Config(
+                camera = metadata.camera,
+                streams = listOf(stream1Config, stream2Config),
+            )
         val threads = FakeThreads.fromTestScope(scope)
-        val backend = FakeCameraBackend(
-            fakeCameras = mapOf(metadata.camera to metadata)
-        )
-        val backends = CameraBackendsImpl(
-            defaultBackendId = backend.id,
-            cameraBackends = mapOf(backend.id to CameraBackendFactory { backend }),
-            context,
-            threads
-        )
-        val cameraContext = CameraBackendsImpl.CameraBackendContext(
-            context,
-            threads,
-            backends
-        )
-        val streamGraph = StreamGraphImpl(
-            metadata,
-            graphConfig
-        )
-        cameraController = CameraControllerSimulator(
-            cameraContext,
-            graphConfig,
-            fakeGraphProcessor,
-            streamGraph
-        )
+        val backend = FakeCameraBackend(fakeCameras = mapOf(metadata.camera to metadata))
+        val backends =
+            CameraBackendsImpl(
+                defaultBackendId = backend.id,
+                cameraBackends = mapOf(backend.id to CameraBackendFactory { backend }),
+                context,
+                threads)
+        val cameraContext = CameraBackendsImpl.CameraBackendContext(context, threads, backends)
+        val streamGraph = StreamGraphImpl(metadata, graphConfig)
+        cameraController =
+            CameraControllerSimulator(cameraContext, graphConfig, fakeGraphProcessor, streamGraph)
         cameraSurfaceManager.addListener(fakeSurfaceListener)
         val surfaceGraph = SurfaceGraph(streamGraph, cameraController, cameraSurfaceManager)
-        val graph = CameraGraphImpl(
-            graphConfig,
-            metadata,
-            fakeGraphProcessor,
-            fakeGraphProcessor,
-            streamGraph,
-            surfaceGraph,
-            cameraController,
-            GraphState3A(),
-            Listener3A()
-        )
-        stream1 = checkNotNull(graph.streams[stream1Config]) {
-            "Failed to find stream for $stream1Config!"
-        }
+        val graph =
+            CameraGraphImpl(
+                graphConfig,
+                metadata,
+                fakeGraphProcessor,
+                fakeGraphProcessor,
+                streamGraph,
+                surfaceGraph,
+                cameraController,
+                GraphState3A(),
+                Listener3A())
+        stream1 =
+            checkNotNull(graph.streams[stream1Config]) {
+                "Failed to find stream for $stream1Config!"
+            }
 
-        stream2 = checkNotNull(graph.streams[stream2Config]) {
-            "Failed to find stream for $stream2Config!"
-        }
+        stream2 =
+            checkNotNull(graph.streams[stream2Config]) {
+                "Failed to find stream for $stream2Config!"
+            }
         return graph
     }
 
@@ -253,4 +237,4 @@
         verify(fakeSurfaceListener, times(1)).onSurfaceInactive(eq(imageReader1.surface))
         verify(fakeSurfaceListener, times(1)).onSurfaceInactive(eq(imageReader1.surface))
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImplTest.kt
index f4a0ff3..f2c065e 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImplTest.kt
@@ -38,18 +38,11 @@
     private val graphState3A = GraphState3A()
     private val graphProcessor = FakeGraphProcessor(graphState3A = graphState3A)
     private val listener3A = Listener3A()
-    private val controller3A = Controller3A(
-        graphProcessor,
-        FakeCameraMetadata(),
-        graphState3A,
-        listener3A
-    )
+    private val controller3A =
+        Controller3A(graphProcessor, FakeCameraMetadata(), graphState3A, listener3A)
 
-    private val session = CameraGraphSessionImpl(
-        tokenLock.acquireOrNull(1, 1)!!,
-        graphProcessor,
-        controller3A
-    )
+    private val session =
+        CameraGraphSessionImpl(tokenLock.acquireOrNull(1, 1)!!, graphProcessor, controller3A)
 
     @Test
     fun createCameraGraphSession() {
@@ -60,9 +53,7 @@
     fun sessionCannotBeUsedAfterClose() {
         session.close()
 
-        val result = assertThrows<IllegalStateException> {
-            session.submit(Request(listOf()))
-        }
+        val result = assertThrows<IllegalStateException> { session.submit(Request(listOf())) }
         result.hasMessageThat().contains("submit")
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
index c5dce6e..346f7d4 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
@@ -47,12 +47,8 @@
     private val captureSequenceProcessor = graphTestContext.captureSequenceProcessor
 
     private val listener3A = Listener3A()
-    private val controller3A = Controller3A(
-        graphProcessor,
-        FakeCameraMetadata(),
-        graphState3A,
-        listener3A
-    )
+    private val controller3A =
+        Controller3A(graphProcessor, FakeCameraMetadata(), graphState3A, listener3A)
 
     @After
     fun teardown() {
@@ -69,22 +65,18 @@
         // this response i.e cameraResponse1, AF is still scanning so the result won't be complete.
         val cameraResponse = async {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_STATE to CaptureResult
-                            .CONTROL_AF_STATE_PASSIVE_SCAN,
-                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_SEARCHING
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_SEARCHING)))
         }
 
         cameraResponse.await()
@@ -94,22 +86,18 @@
         // lock3AForCapture call will complete.
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_STATE to CaptureResult
-                            .CONTROL_AF_STATE_FOCUSED_LOCKED,
-                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_CONVERGED
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_CONVERGED)))
         }
 
         val result3A = result.await()
@@ -119,9 +107,8 @@
         // We now check if the correct sequence of requests were submitted by lock3AForCapture call.
         // There should be a request to trigger AF and AE precapture metering.
         val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
-            CaptureRequest.CONTROL_AF_TRIGGER_START
-        )
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
         assertThat(request1.requiredParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
             .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START)
     }
@@ -143,22 +130,18 @@
         // complete.
         val cameraResponse = async {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_STATE to CaptureResult
-                            .CONTROL_AF_STATE_FOCUSED_LOCKED,
-                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_CONVERGED
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_CONVERGED)))
         }
 
         cameraResponse.await()
@@ -169,22 +152,18 @@
         // capture result corresponding to the submitted request suffices.
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_STATE to CaptureResult
-                            .CONTROL_AF_STATE_PASSIVE_SCAN,
-                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_CONVERGED
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_CONVERGED)))
         }
 
         val result3A = result.await()
@@ -194,9 +173,8 @@
         // We now check if the correct sequence of requests were submitted by unlock3APostCapture
         // call. There should be a request to cancel AF and AE precapture metering.
         val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
-            CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
-        )
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
         assertThat(request1.requiredParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
             .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL)
     }
@@ -207,18 +185,11 @@
 
         val cameraResponse = async {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
-                FakeFrameMetadata(
-                    frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf()
-                )
-            )
+                FakeFrameMetadata(frameNumber = FrameNumber(101L), resultMetadata = mapOf()))
         }
 
         cameraResponse.await()
@@ -229,15 +200,12 @@
         // We now check if the correct sequence of requests were submitted by unlock3APostCapture
         // call. There should be a request to cancel AF and lock ae.
         val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
-            CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
-        )
-        assertThat(request1.requiredParameters[CaptureRequest.CONTROL_AE_LOCK])
-            .isEqualTo(true)
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+        assertThat(request1.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
 
         // Then another request to unlock ae.
         val request2 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK])
-            .isEqualTo(false)
+        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
     }
 }
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
index fa3e4e4..537dd6d 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
@@ -53,12 +53,12 @@
     private val captureSequenceProcessor = graphTestContext.captureSequenceProcessor
 
     private val listener3A = Listener3A()
-    private val fakeMetadata = FakeCameraMetadata(
-        mapOf(
-            CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES to
-                intArrayOf(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
-        ),
-    )
+    private val fakeMetadata =
+        FakeCameraMetadata(
+            mapOf(
+                CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES to
+                    intArrayOf(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)),
+        )
     private val controller3A = Controller3A(graphProcessor, fakeMetadata, graphState3A, listener3A)
 
     @After
@@ -68,10 +68,10 @@
 
     @Test
     fun testAfImmediateAeImmediate() = runTest {
-        val result = controller3A.lock3A(
-            afLockBehavior = Lock3ABehavior.IMMEDIATE,
-            aeLockBehavior = Lock3ABehavior.IMMEDIATE
-        )
+        val result =
+            controller3A.lock3A(
+                afLockBehavior = Lock3ABehavior.IMMEDIATE,
+                aeLockBehavior = Lock3ABehavior.IMMEDIATE)
         assertThat(result.isCompleted).isFalse()
 
         // Since requirement of to lock both AE and AF immediately, the requests to lock AE and AF
@@ -80,22 +80,18 @@
         // result won't be complete.
         val cameraResponse = async {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_STATE to CaptureResult
-                            .CONTROL_AF_STATE_PASSIVE_SCAN,
-                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_LOCKED)))
         }
 
         cameraResponse.await()
@@ -105,22 +101,18 @@
         // will complete.
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_STATE to CaptureResult
-                            .CONTROL_AF_STATE_FOCUSED_LOCKED,
-                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_LOCKED)))
         }
 
         val result3A = result.await()
@@ -130,52 +122,42 @@
         // We not check if the correct sequence of requests were submitted by lock3A call. The
         // request should be a repeating request to lock AE.
         val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
-            true
-        )
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
 
         // The second request should be a single request to lock AF.
         val request2 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
-            CaptureRequest.CONTROL_AF_TRIGGER_START
-        )
-        assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
-            true
-        )
+        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
+        assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
     }
 
     @Test
     fun testAfImmediateAeAfterCurrentScan() = runTest {
         val globalScope = CoroutineScope(UnconfinedTestDispatcher())
 
-        val lock3AAsyncTask = globalScope.async {
-            controller3A.lock3A(
-                afLockBehavior = Lock3ABehavior.IMMEDIATE,
-                aeLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN
-            )
-        }
+        val lock3AAsyncTask =
+            globalScope.async {
+                controller3A.lock3A(
+                    afLockBehavior = Lock3ABehavior.IMMEDIATE,
+                    aeLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN)
+            }
         assertThat(lock3AAsyncTask.isCompleted).isFalse()
         // Launch a task to repeatedly invoke a given capture result.
         globalScope.launch {
             while (true) {
                 listener3A.onRequestSequenceCreated(
-                    FakeRequestMetadata(
-                        requestNumber = RequestNumber(1)
-                    )
-                )
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)))
                 listener3A.onPartialCaptureResult(
                     FakeRequestMetadata(requestNumber = RequestNumber(1)),
                     FrameNumber(101L),
                     FakeFrameMetadata(
                         frameNumber = FrameNumber(101L),
-                        resultMetadata = mapOf(
-                            CaptureResult.CONTROL_AF_STATE to
-                                CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
-                            CaptureResult.CONTROL_AE_STATE to
-                                CaptureResult.CONTROL_AE_STATE_CONVERGED
-                        )
-                    )
-                )
+                        resultMetadata =
+                            mapOf(
+                                CaptureResult.CONTROL_AF_STATE to
+                                    CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                                CaptureResult.CONTROL_AE_STATE to
+                                    CaptureResult.CONTROL_AE_STATE_CONVERGED)))
                 delay(FRAME_RATE_MS)
             }
         }
@@ -189,28 +171,22 @@
         captureSequenceProcessor.nextEvent().requestSequence
         // Once AE is converged, another repeatingrequest is sent to lock AE.
         val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
-            true
-        )
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
 
         globalScope.launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_STATE to CaptureResult
-                            .CONTROL_AF_STATE_FOCUSED_LOCKED,
-                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_LOCKED)))
         }
 
         val result3A = result.await()
@@ -219,9 +195,8 @@
 
         // A single request to lock AF must have been used as well.
         val request2 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
-            CaptureRequest.CONTROL_AF_TRIGGER_START
-        )
+        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
         globalScope.cancel()
     }
 
@@ -229,34 +204,29 @@
     fun testAfImmediateAeAfterNewScan() = runTest {
         val globalScope = CoroutineScope(UnconfinedTestDispatcher())
 
-        val lock3AAsyncTask = globalScope.async {
-            controller3A.lock3A(
-                afLockBehavior = Lock3ABehavior.IMMEDIATE,
-                aeLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN
-            )
-        }
+        val lock3AAsyncTask =
+            globalScope.async {
+                controller3A.lock3A(
+                    afLockBehavior = Lock3ABehavior.IMMEDIATE,
+                    aeLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN)
+            }
         assertThat(lock3AAsyncTask.isCompleted).isFalse()
 
         globalScope.launch {
             while (true) {
                 listener3A.onRequestSequenceCreated(
-                    FakeRequestMetadata(
-                        requestNumber = RequestNumber(1)
-                    )
-                )
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)))
                 listener3A.onPartialCaptureResult(
                     FakeRequestMetadata(requestNumber = RequestNumber(1)),
                     FrameNumber(101L),
                     FakeFrameMetadata(
                         frameNumber = FrameNumber(101L),
-                        resultMetadata = mapOf(
-                            CaptureResult.CONTROL_AF_STATE to
-                                CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
-                            CaptureResult.CONTROL_AE_STATE to
-                                CaptureResult.CONTROL_AE_STATE_CONVERGED
-                        )
-                    )
-                )
+                        resultMetadata =
+                            mapOf(
+                                CaptureResult.CONTROL_AF_STATE to
+                                    CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                                CaptureResult.CONTROL_AE_STATE to
+                                    CaptureResult.CONTROL_AE_STATE_CONVERGED)))
                 delay(FRAME_RATE_MS)
             }
         }
@@ -267,28 +237,22 @@
         // For a new AE scan we first send a request to unlock AE just in case it was
         // previously or internally locked.
         val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
-            false
-        )
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
 
         globalScope.launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_STATE to CaptureResult
-                            .CONTROL_AF_STATE_FOCUSED_LOCKED,
-                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_LOCKED)))
         }
 
         val result3A = result.await()
@@ -297,18 +261,13 @@
 
         // There should be one more request to lock AE after new scan is done.
         val request2 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
-            true
-        )
+        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
 
         // And one request to lock AF.
         val request3 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
-            CaptureRequest.CONTROL_AF_TRIGGER_START
-        )
-        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
-            true
-        )
+        assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
+        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
 
         globalScope.cancel()
     }
@@ -317,33 +276,28 @@
     fun testAfAfterCurrentScanAeImmediate() = runTest {
         val globalScope = CoroutineScope(UnconfinedTestDispatcher())
 
-        val lock3AAsyncTask = globalScope.async {
-            controller3A.lock3A(
-                afLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN,
-                aeLockBehavior = Lock3ABehavior.IMMEDIATE
-            )
-        }
+        val lock3AAsyncTask =
+            globalScope.async {
+                controller3A.lock3A(
+                    afLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN,
+                    aeLockBehavior = Lock3ABehavior.IMMEDIATE)
+            }
         assertThat(lock3AAsyncTask.isCompleted).isFalse()
         globalScope.launch {
             while (true) {
                 listener3A.onRequestSequenceCreated(
-                    FakeRequestMetadata(
-                        requestNumber = RequestNumber(1)
-                    )
-                )
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)))
                 listener3A.onPartialCaptureResult(
                     FakeRequestMetadata(requestNumber = RequestNumber(1)),
                     FrameNumber(101L),
                     FakeFrameMetadata(
                         frameNumber = FrameNumber(101L),
-                        resultMetadata = mapOf(
-                            CaptureResult.CONTROL_AF_STATE to
-                                CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
-                            CaptureResult.CONTROL_AE_STATE to
-                                CaptureResult.CONTROL_AE_STATE_CONVERGED
-                        )
-                    )
-                )
+                        resultMetadata =
+                            mapOf(
+                                CaptureResult.CONTROL_AF_STATE to
+                                    CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+                                CaptureResult.CONTROL_AE_STATE to
+                                    CaptureResult.CONTROL_AE_STATE_CONVERGED)))
                 delay(FRAME_RATE_MS)
             }
         }
@@ -353,22 +307,18 @@
 
         globalScope.launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_STATE to CaptureResult
-                            .CONTROL_AF_STATE_FOCUSED_LOCKED,
-                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_LOCKED)))
         }
 
         val result3A = result.await()
@@ -379,18 +329,13 @@
         captureSequenceProcessor.nextEvent()
         // One request to lock AE
         val request2 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
-            true
-        )
+        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
 
         // And one request to lock AF.
         val request3 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
-            CaptureRequest.CONTROL_AF_TRIGGER_START
-        )
-        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
-            true
-        )
+        assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
+        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
         globalScope.cancel()
     }
 
@@ -398,33 +343,28 @@
     fun testAfAfterNewScanScanAeImmediate() = runTest {
         val globalScope = CoroutineScope(UnconfinedTestDispatcher())
 
-        val lock3AAsyncTask = globalScope.async {
-            controller3A.lock3A(
-                afLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN,
-                aeLockBehavior = Lock3ABehavior.IMMEDIATE
-            )
-        }
+        val lock3AAsyncTask =
+            globalScope.async {
+                controller3A.lock3A(
+                    afLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN,
+                    aeLockBehavior = Lock3ABehavior.IMMEDIATE)
+            }
         assertThat(lock3AAsyncTask.isCompleted).isFalse()
         globalScope.launch {
             while (true) {
                 listener3A.onRequestSequenceCreated(
-                    FakeRequestMetadata(
-                        requestNumber = RequestNumber(1)
-                    )
-                )
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)))
                 listener3A.onPartialCaptureResult(
                     FakeRequestMetadata(requestNumber = RequestNumber(1)),
                     FrameNumber(101L),
                     FakeFrameMetadata(
                         frameNumber = FrameNumber(101L),
-                        resultMetadata = mapOf(
-                            CaptureResult.CONTROL_AF_STATE to
-                                CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
-                            CaptureResult.CONTROL_AE_STATE to
-                                CaptureResult.CONTROL_AE_STATE_CONVERGED
-                        )
-                    )
-                )
+                        resultMetadata =
+                            mapOf(
+                                CaptureResult.CONTROL_AF_STATE to
+                                    CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+                                CaptureResult.CONTROL_AE_STATE to
+                                    CaptureResult.CONTROL_AE_STATE_CONVERGED)))
                 delay(FRAME_RATE_MS)
             }
         }
@@ -434,22 +374,18 @@
 
         globalScope.launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_STATE to CaptureResult
-                            .CONTROL_AF_STATE_FOCUSED_LOCKED,
-                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_LOCKED)))
         }
 
         val result3A = result.await()
@@ -458,26 +394,20 @@
 
         // One request to cancel AF to start a new scan.
         val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
-            CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
-        )
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
         // There should be one request to monitor AF to finish it's scan.
         captureSequenceProcessor.nextEvent()
 
         // There should be one request to monitor lock AE.
         val request2 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
-            true
-        )
+        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
 
         // And one request to lock AF.
         val request3 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
-            CaptureRequest.CONTROL_AF_TRIGGER_START
-        )
-        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
-            true
-        )
+        assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
+        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
         globalScope.cancel()
     }
 
@@ -485,33 +415,28 @@
     fun testAfAfterCurrentScanAeAfterCurrentScan() = runTest {
         val globalScope = CoroutineScope(UnconfinedTestDispatcher())
 
-        val lock3AAsyncTask = globalScope.async {
-            controller3A.lock3A(
-                afLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN,
-                aeLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN
-            )
-        }
+        val lock3AAsyncTask =
+            globalScope.async {
+                controller3A.lock3A(
+                    afLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN,
+                    aeLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN)
+            }
         assertThat(lock3AAsyncTask.isCompleted).isFalse()
         globalScope.launch {
             while (true) {
                 listener3A.onRequestSequenceCreated(
-                    FakeRequestMetadata(
-                        requestNumber = RequestNumber(1)
-                    )
-                )
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)))
                 listener3A.onPartialCaptureResult(
                     FakeRequestMetadata(requestNumber = RequestNumber(1)),
                     FrameNumber(101L),
                     FakeFrameMetadata(
                         frameNumber = FrameNumber(101L),
-                        resultMetadata = mapOf(
-                            CaptureResult.CONTROL_AF_STATE to
-                                CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
-                            CaptureResult.CONTROL_AE_STATE to
-                                CaptureResult.CONTROL_AE_STATE_CONVERGED
-                        )
-                    )
-                )
+                        resultMetadata =
+                            mapOf(
+                                CaptureResult.CONTROL_AF_STATE to
+                                    CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+                                CaptureResult.CONTROL_AE_STATE to
+                                    CaptureResult.CONTROL_AE_STATE_CONVERGED)))
                 delay(FRAME_RATE_MS)
             }
         }
@@ -521,22 +446,18 @@
 
         globalScope.launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_STATE to CaptureResult
-                            .CONTROL_AF_STATE_FOCUSED_LOCKED,
-                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_LOCKED)))
         }
 
         val result3A = result.await()
@@ -558,21 +479,16 @@
         val request2 = request2Event.requestSequence!!
         assertThat(request2).isNotNull()
         assertThat(request2.requiredParameters).isNotEmpty()
-        assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
-            true
-        )
+        assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
 
         // And one request to lock AF.
         val request3Event = captureSequenceProcessor.nextEvent()
         assertThat(request3Event.requestSequence!!.repeating).isFalse()
         assertThat(request3Event.submit).isTrue()
         val request3 = request3Event.requestSequence!!
-        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
-            CaptureRequest.CONTROL_AF_TRIGGER_START
-        )
-        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
-            true
-        )
+        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
+        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
 
         globalScope.cancel()
     }
@@ -580,33 +496,28 @@
     @Test
     fun testAfAfterNewScanScanAeAfterNewScan() = runTest {
         val globalScope = CoroutineScope(UnconfinedTestDispatcher())
-        val lock3AAsyncTask = globalScope.async {
-            controller3A.lock3A(
-                afLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN,
-                aeLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN
-            )
-        }
+        val lock3AAsyncTask =
+            globalScope.async {
+                controller3A.lock3A(
+                    afLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN,
+                    aeLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN)
+            }
         assertThat(lock3AAsyncTask.isCompleted).isFalse()
         globalScope.launch {
             while (true) {
                 listener3A.onRequestSequenceCreated(
-                    FakeRequestMetadata(
-                        requestNumber = RequestNumber(1)
-                    )
-                )
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)))
                 listener3A.onPartialCaptureResult(
                     FakeRequestMetadata(requestNumber = RequestNumber(1)),
                     FrameNumber(101L),
                     FakeFrameMetadata(
                         frameNumber = FrameNumber(101L),
-                        resultMetadata = mapOf(
-                            CaptureResult.CONTROL_AF_STATE to
-                                CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
-                            CaptureResult.CONTROL_AE_STATE to
-                                CaptureResult.CONTROL_AE_STATE_CONVERGED
-                        )
-                    )
-                )
+                        resultMetadata =
+                            mapOf(
+                                CaptureResult.CONTROL_AF_STATE to
+                                    CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+                                CaptureResult.CONTROL_AE_STATE to
+                                    CaptureResult.CONTROL_AE_STATE_CONVERGED)))
                 delay(FRAME_RATE_MS)
             }
         }
@@ -616,22 +527,18 @@
 
         globalScope.launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_STATE to CaptureResult
-                            .CONTROL_AF_STATE_FOCUSED_LOCKED,
-                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_LOCKED)))
         }
 
         val result3A = result.await()
@@ -640,29 +547,21 @@
 
         // One request to cancel AF to start a new scan.
         val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
-            CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
-        )
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
         // There should be one request to unlock AE and monitor the current AF scan to finish.
         val request2 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
-            false
-        )
+        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
 
         // There should be one request to monitor lock AE.
         val request3 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
-            true
-        )
+        assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
 
         // And one request to lock AF.
         val request4 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request4!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
-            CaptureRequest.CONTROL_AF_TRIGGER_START
-        )
-        assertThat(request4.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
-            true
-        )
+        assertThat(request4!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
+        assertThat(request4.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
         globalScope.cancel()
     }
 
@@ -670,12 +569,12 @@
     fun testLock3AWithRegions() = runTest {
         val afMeteringRegion = MeteringRectangle(1, 1, 100, 100, 2)
         val aeMeteringRegion = MeteringRectangle(10, 15, 140, 140, 3)
-        val result = controller3A.lock3A(
-            aeRegions = listOf(aeMeteringRegion),
-            afRegions = listOf(afMeteringRegion),
-            afLockBehavior = Lock3ABehavior.IMMEDIATE,
-            aeLockBehavior = Lock3ABehavior.IMMEDIATE
-        )
+        val result =
+            controller3A.lock3A(
+                aeRegions = listOf(aeMeteringRegion),
+                afRegions = listOf(afMeteringRegion),
+                afLockBehavior = Lock3ABehavior.IMMEDIATE,
+                aeLockBehavior = Lock3ABehavior.IMMEDIATE)
         assertThat(result.isCompleted).isFalse()
 
         // Since requirement of to lock both AE and AF immediately, the requests to lock AE and AF
@@ -684,22 +583,18 @@
         // result won't be complete.
         val cameraResponse = async {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_STATE to CaptureResult
-                            .CONTROL_AF_STATE_PASSIVE_SCAN,
-                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_LOCKED)))
         }
 
         cameraResponse.await()
@@ -709,22 +604,18 @@
         // will complete.
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_STATE to CaptureResult
-                            .CONTROL_AF_STATE_FOCUSED_LOCKED,
-                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_LOCKED)))
         }
 
         val result3A = result.await()
@@ -742,28 +633,23 @@
         // We not check if the correct sequence of requests were submitted by lock3A call. The
         // request should be a repeating request to lock AE.
         val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
-            true
-        )
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
 
         // The second request should be a single request to lock AF.
         val request2 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
-            CaptureRequest.CONTROL_AF_TRIGGER_START
-        )
-        assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
-            true
-        )
+        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
+        assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
     }
 
     @Test
     fun testLock3AWithUnsupportedAutoFocusTrigger() = runTest {
-        val fakeMetadata = FakeCameraMetadata(
-            mapOf(
-                CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES to
-                    intArrayOf(CaptureRequest.CONTROL_AF_MODE_OFF)
-            ),
-        )
+        val fakeMetadata =
+            FakeCameraMetadata(
+                mapOf(
+                    CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES to
+                        intArrayOf(CaptureRequest.CONTROL_AF_MODE_OFF)),
+            )
         val controller3A = Controller3A(graphProcessor, fakeMetadata, graphState3A, listener3A)
         val result = controller3A.lock3A(afLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN).await()
         assertThat(result.status).isEqualTo(Result3A.Status.OK)
@@ -774,4 +660,4 @@
         // The time duration in milliseconds between two frame results.
         private const val FRAME_RATE_MS = 33L
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt
index c0e441a..520954e 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt
@@ -45,12 +45,8 @@
     private val graphState3A = graphTestContext.graphProcessor.graphState3A
     private val graphProcessor = graphTestContext.graphProcessor
     private val listener3A = Listener3A()
-    private val controller3A = Controller3A(
-        graphProcessor,
-        FakeCameraMetadata(),
-        graphState3A,
-        listener3A
-    )
+    private val controller3A =
+        Controller3A(graphProcessor, FakeCameraMetadata(), graphState3A, listener3A)
 
     @After
     fun teardown() {
@@ -66,21 +62,16 @@
 
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_ON,
-                        CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_TORCH
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_ON,
+                            CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_TORCH)))
         }
         val result3A = result.await()
         assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
@@ -96,21 +87,16 @@
 
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_ON,
-                        CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_OFF
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_ON,
+                            CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_OFF)))
         }
         val result3A = result.await()
         assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
@@ -123,28 +109,21 @@
 
         val result = controller3A.setTorch(TorchState.ON)
         assertThat(graphState3A.aeMode!!.value).isEqualTo(CaptureRequest.CONTROL_AE_MODE_OFF)
-        assertThat(graphState3A.flashMode!!.value).isEqualTo(
-            CaptureRequest.FLASH_MODE_TORCH
-        )
+        assertThat(graphState3A.flashMode!!.value).isEqualTo(CaptureRequest.FLASH_MODE_TORCH)
         assertThat(result.isCompleted).isFalse()
 
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_OFF,
-                        CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_TORCH
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_OFF,
+                            CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_TORCH)))
         }
         val result3A = result.await()
         assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt
index c0a706a..ce86e0d 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt
@@ -47,12 +47,8 @@
     private val graphState3A = graphTestContext.graphProcessor.graphState3A
     private val graphProcessor = graphTestContext.graphProcessor
     private val listener3A = Listener3A()
-    private val controller3A = Controller3A(
-        graphProcessor,
-        FakeCameraMetadata(),
-        graphState3A,
-        listener3A
-    )
+    private val controller3A =
+        Controller3A(graphProcessor, FakeCameraMetadata(), graphState3A, listener3A)
 
     @After
     fun teardown() {
@@ -71,20 +67,14 @@
         val result = controller3A.submit3A(afMode = AfMode.OFF)
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_OFF
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_OFF)))
         }
         val result3A = result.await()
         assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
@@ -96,21 +86,16 @@
         val result = controller3A.submit3A(aeMode = AeMode.ON_ALWAYS_FLASH)
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AE_MODE to
-                            CaptureResult.CONTROL_AE_MODE_ON_ALWAYS_FLASH
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AE_MODE to
+                                CaptureResult.CONTROL_AE_MODE_ON_ALWAYS_FLASH)))
         }
         val result3A = result.await()
         assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
@@ -122,21 +107,16 @@
         val result = controller3A.submit3A(awbMode = AwbMode.CLOUDY_DAYLIGHT)
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AWB_MODE to
-                            CaptureResult.CONTROL_AWB_MODE_CLOUDY_DAYLIGHT
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AWB_MODE to
+                                CaptureResult.CONTROL_AWB_MODE_CLOUDY_DAYLIGHT)))
         }
         val result3A = result.await()
         assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
@@ -148,21 +128,16 @@
         val result = controller3A.submit3A(afRegions = listOf(MeteringRectangle(1, 1, 100, 100, 2)))
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_REGIONS to
-                            Array(1) { MeteringRectangle(1, 1, 99, 99, 2) }
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AF_REGIONS to
+                                Array(1) { MeteringRectangle(1, 1, 99, 99, 2) })))
         }
         val result3A = result.await()
         assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
@@ -174,21 +149,16 @@
         val result = controller3A.submit3A(aeRegions = listOf(MeteringRectangle(1, 1, 100, 100, 2)))
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AE_REGIONS to
-                            Array(1) { MeteringRectangle(1, 1, 99, 99, 2) }
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AE_REGIONS to
+                                Array(1) { MeteringRectangle(1, 1, 99, 99, 2) })))
         }
         val result3A = result.await()
         assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
@@ -197,29 +167,21 @@
 
     @Test
     fun testAwbRegionsSubmit() = runTest {
-        val result = controller3A.submit3A(
-            awbRegions = listOf(
-                MeteringRectangle(1, 1, 100, 100, 2)
-            )
-        )
+        val result =
+            controller3A.submit3A(awbRegions = listOf(MeteringRectangle(1, 1, 100, 100, 2)))
 
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AWB_REGIONS to
-                            Array(1) { MeteringRectangle(1, 1, 99, 99, 2) }
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AWB_REGIONS to
+                                Array(1) { MeteringRectangle(1, 1, 99, 99, 2) })))
         }
         val result3A = result.await()
         assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
@@ -237,4 +199,4 @@
         assertThat(result.frameMetadata).isNull()
         assertThat(result.status).isEqualTo(Result3A.Status.SUBMIT_FAILED)
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
index 7e87bcc..fcb0d26 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
@@ -47,18 +47,13 @@
     private val graphProcessor = graphTestContext.graphProcessor
     private val captureSequenceProcessor = graphTestContext.captureSequenceProcessor
     private val listener3A = Listener3A()
-    private val fakeMetadata = FakeCameraMetadata(
-        mapOf(
-            CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES to
-                intArrayOf(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
-        ),
-    )
-    private val controller3A = Controller3A(
-        graphProcessor,
-        fakeMetadata,
-        graphState3A,
-        listener3A
-    )
+    private val fakeMetadata =
+        FakeCameraMetadata(
+            mapOf(
+                CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES to
+                    intArrayOf(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)),
+        )
+    private val controller3A = Controller3A(graphProcessor, fakeMetadata, graphState3A, listener3A)
 
     @After
     fun teardown() {
@@ -67,29 +62,22 @@
 
     @Test
     fun testUnlockAe() = runTest {
-        val unLock3AAsyncTask = async {
-            controller3A.unlock3A(ae = true)
-        }
+        val unLock3AAsyncTask = async { controller3A.unlock3A(ae = true) }
 
         // Launch a task to repeatedly invoke a given capture result.
         val repeatingJob = launch {
             while (true) {
                 listener3A.onRequestSequenceCreated(
-                    FakeRequestMetadata(
-                        requestNumber = RequestNumber(1)
-                    )
-                )
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)))
                 listener3A.onPartialCaptureResult(
                     FakeRequestMetadata(requestNumber = RequestNumber(1)),
                     FrameNumber(101L),
                     FakeFrameMetadata(
                         frameNumber = FrameNumber(101L),
-                        resultMetadata = mapOf(
-                            CaptureResult.CONTROL_AE_STATE to
-                                CaptureResult.CONTROL_AE_STATE_LOCKED
-                        )
-                    )
-                )
+                        resultMetadata =
+                            mapOf(
+                                CaptureResult.CONTROL_AE_STATE to
+                                    CaptureResult.CONTROL_AE_STATE_LOCKED)))
                 delay(FRAME_RATE_MS)
             }
         }
@@ -100,28 +88,23 @@
 
         // There should be one request to lock AE.
         val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK])
-            .isEqualTo(false)
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
 
         repeatingJob.cancel()
         repeatingJob.join()
 
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_SEARCHING
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_SEARCHING)))
         }
 
         val result3A = result.await()
@@ -137,21 +120,16 @@
         val repeatingJob = launch {
             while (true) {
                 listener3A.onRequestSequenceCreated(
-                    FakeRequestMetadata(
-                        requestNumber = RequestNumber(1)
-                    )
-                )
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)))
                 listener3A.onPartialCaptureResult(
                     FakeRequestMetadata(requestNumber = RequestNumber(1)),
                     FrameNumber(101L),
                     FakeFrameMetadata(
                         frameNumber = FrameNumber(101L),
-                        resultMetadata = mapOf(
-                            CaptureResult.CONTROL_AF_STATE to
-                                CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
-                        )
-                    )
-                )
+                        resultMetadata =
+                            mapOf(
+                                CaptureResult.CONTROL_AF_STATE to
+                                    CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED)))
                 delay(FRAME_RATE_MS)
             }
         }
@@ -170,20 +148,16 @@
 
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_INACTIVE
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_INACTIVE)))
         }
 
         val result3A = result.await()
@@ -193,29 +167,22 @@
 
     @Test
     fun testUnlockAwb() = runTest {
-        val unLock3AAsyncTask = async {
-            controller3A.unlock3A(awb = true)
-        }
+        val unLock3AAsyncTask = async { controller3A.unlock3A(awb = true) }
 
         // Launch a task to repeatedly invoke a given capture result.
         val repeatingJob = launch {
             while (true) {
                 listener3A.onRequestSequenceCreated(
-                    FakeRequestMetadata(
-                        requestNumber = RequestNumber(1)
-                    )
-                )
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)))
                 listener3A.onPartialCaptureResult(
                     FakeRequestMetadata(requestNumber = RequestNumber(1)),
                     FrameNumber(101L),
                     FakeFrameMetadata(
                         frameNumber = FrameNumber(101L),
-                        resultMetadata = mapOf(
-                            CaptureResult.CONTROL_AWB_STATE to
-                                CaptureResult.CONTROL_AWB_STATE_LOCKED
-                        )
-                    )
-                )
+                        resultMetadata =
+                            mapOf(
+                                CaptureResult.CONTROL_AWB_STATE to
+                                    CaptureResult.CONTROL_AWB_STATE_LOCKED)))
                 delay(FRAME_RATE_MS)
             }
         }
@@ -226,28 +193,23 @@
 
         // There should be one request to lock AWB.
         val request1 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AWB_LOCK])
-            .isEqualTo(false)
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AWB_LOCK]).isEqualTo(false)
 
         repeatingJob.cancel()
         repeatingJob.join()
 
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AWB_STATE to CaptureResult.CONTROL_AWB_STATE_SEARCHING
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AWB_STATE to
+                                CaptureResult.CONTROL_AWB_STATE_SEARCHING)))
         }
 
         val result3A = result.await()
@@ -263,22 +225,18 @@
         val repeatingJob = launch {
             while (true) {
                 listener3A.onRequestSequenceCreated(
-                    FakeRequestMetadata(
-                        requestNumber = RequestNumber(1)
-                    )
-                )
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)))
                 listener3A.onPartialCaptureResult(
                     FakeRequestMetadata(requestNumber = RequestNumber(1)),
                     FrameNumber(101L),
                     FakeFrameMetadata(
                         frameNumber = FrameNumber(101L),
-                        resultMetadata = mapOf(
-                            CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED,
-                            CaptureResult.CONTROL_AF_STATE to
-                                CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
-                        )
-                    )
-                )
+                        resultMetadata =
+                            mapOf(
+                                CaptureResult.CONTROL_AE_STATE to
+                                    CaptureResult.CONTROL_AE_STATE_LOCKED,
+                                CaptureResult.CONTROL_AF_STATE to
+                                    CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED)))
                 delay(FRAME_RATE_MS)
             }
         }
@@ -293,29 +251,25 @@
             .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
         // Then request to unlock AE.
         val request2 = captureSequenceProcessor.nextEvent().requestSequence
-        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK])
-            .isEqualTo(false)
+        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
 
         repeatingJob.cancel()
         repeatingJob.join()
 
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_INACTIVE,
-                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_SEARCHING
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_INACTIVE,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_SEARCHING)))
         }
 
         val result3A = result.await()
@@ -325,12 +279,12 @@
 
     @Test
     fun testUnlockAfWhenAfNotSupported() = runTest {
-        val fakeMetadata = FakeCameraMetadata(
-            mapOf(
-                CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES to
-                    intArrayOf(CaptureRequest.CONTROL_AF_MODE_OFF)
-            ),
-        )
+        val fakeMetadata =
+            FakeCameraMetadata(
+                mapOf(
+                    CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES to
+                        intArrayOf(CaptureRequest.CONTROL_AF_MODE_OFF)),
+            )
         val controller3A = Controller3A(graphProcessor, fakeMetadata, graphState3A, listener3A)
         val result = controller3A.unlock3A(af = true).await()
         assertThat(result.status).isEqualTo(Result3A.Status.OK)
@@ -341,4 +295,4 @@
         // The time duration in milliseconds between two frame results.
         private const val FRAME_RATE_MS = 33L
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt
index 81b46ec..5682eb6 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt
@@ -29,10 +29,10 @@
 import androidx.camera.camera2.pipe.Result3A
 import androidx.camera.camera2.pipe.StreamId
 import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor
 import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
 import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
 import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
-import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor
 import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CancellationException
@@ -52,12 +52,8 @@
     private val fakeCaptureSequenceProcessor = FakeCaptureSequenceProcessor()
     private val fakeGraphRequestProcessor = GraphRequestProcessor.from(fakeCaptureSequenceProcessor)
     private val listener3A = Listener3A()
-    private val controller3A = Controller3A(
-        graphProcessor,
-        FakeCameraMetadata(),
-        graphState3A,
-        listener3A
-    )
+    private val controller3A =
+        Controller3A(graphProcessor, FakeCameraMetadata(), graphState3A, listener3A)
 
     @Test
     fun testUpdate3AUpdatesState3A() {
@@ -87,20 +83,14 @@
         val result = controller3A.update3A(afMode = AfMode.OFF)
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_OFF
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_OFF)))
         }
         val result3A = result.await()
         assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
@@ -114,21 +104,16 @@
         val result = controller3A.update3A(aeMode = AeMode.ON_ALWAYS_FLASH)
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AE_MODE to
-                            CaptureResult.CONTROL_AE_MODE_ON_ALWAYS_FLASH
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AE_MODE to
+                                CaptureResult.CONTROL_AE_MODE_ON_ALWAYS_FLASH)))
         }
         val result3A = result.await()
         assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
@@ -142,21 +127,16 @@
         val result = controller3A.update3A(awbMode = AwbMode.CLOUDY_DAYLIGHT)
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AWB_MODE to
-                            CaptureResult.CONTROL_AWB_MODE_CLOUDY_DAYLIGHT
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AWB_MODE to
+                                CaptureResult.CONTROL_AWB_MODE_CLOUDY_DAYLIGHT)))
         }
         val result3A = result.await()
         assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
@@ -170,21 +150,16 @@
         val result = controller3A.update3A(afRegions = listOf(MeteringRectangle(1, 1, 100, 100, 2)))
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AF_REGIONS to
-                            Array(1) { MeteringRectangle(1, 1, 99, 99, 2) }
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AF_REGIONS to
+                                Array(1) { MeteringRectangle(1, 1, 99, 99, 2) })))
         }
         val result3A = result.await()
         assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
@@ -198,21 +173,16 @@
         val result = controller3A.update3A(aeRegions = listOf(MeteringRectangle(1, 1, 100, 100, 2)))
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AE_REGIONS to
-                            Array(1) { MeteringRectangle(1, 1, 99, 99, 2) }
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AE_REGIONS to
+                                Array(1) { MeteringRectangle(1, 1, 99, 99, 2) })))
         }
         val result3A = result.await()
         assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
@@ -223,28 +193,20 @@
     fun testAwbRegionsUpdate() = runTest {
         initGraphProcessor()
 
-        val result = controller3A.update3A(
-            awbRegions = listOf(
-                MeteringRectangle(1, 1, 100, 100, 2)
-            )
-        )
+        val result =
+            controller3A.update3A(awbRegions = listOf(MeteringRectangle(1, 1, 100, 100, 2)))
         launch {
             listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(
-                    requestNumber = RequestNumber(1)
-                )
-            )
+                FakeRequestMetadata(requestNumber = RequestNumber(1)))
             listener3A.onPartialCaptureResult(
                 FakeRequestMetadata(requestNumber = RequestNumber(1)),
                 FrameNumber(101L),
                 FakeFrameMetadata(
                     frameNumber = FrameNumber(101L),
-                    resultMetadata = mapOf(
-                        CaptureResult.CONTROL_AWB_REGIONS to
-                            Array(1) { MeteringRectangle(1, 1, 99, 99, 2) }
-                    )
-                )
-            )
+                    resultMetadata =
+                        mapOf(
+                            CaptureResult.CONTROL_AWB_REGIONS to
+                                Array(1) { MeteringRectangle(1, 1, 99, 99, 2) })))
         }
         val result3A = result.await()
         assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
@@ -255,4 +217,4 @@
         graphProcessor.onGraphStarted(fakeGraphRequestProcessor)
         graphProcessor.startRepeating(Request(streams = listOf(StreamId(1))))
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CorrectedFrameMetadataTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CorrectedFrameMetadataTest.kt
index fdaebc3..73bd154 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CorrectedFrameMetadataTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CorrectedFrameMetadataTest.kt
@@ -32,21 +32,20 @@
 internal class CorrectedFrameMetadataTest {
     @Test
     fun canOverrideFrameMetadata() {
-        val metadata = FakeFrameMetadata(
-            mapOf(
-                CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_ON,
-                CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE
-            ),
-            mapOf(FakeMetadata.TEST_KEY to 42)
-        )
+        val metadata =
+            FakeFrameMetadata(
+                mapOf(
+                    CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_ON,
+                    CaptureResult.CONTROL_AF_MODE to
+                        CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE),
+                mapOf(FakeMetadata.TEST_KEY to 42))
 
-        val fixed = CorrectedFrameMetadata(
-            metadata,
-            mapOf(
-                CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_OFF,
-                CaptureResult.LENS_STATE to CaptureResult.LENS_STATE_STATIONARY
-            )
-        )
+        val fixed =
+            CorrectedFrameMetadata(
+                metadata,
+                mapOf(
+                    CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_OFF,
+                    CaptureResult.LENS_STATE to CaptureResult.LENS_STATE_STATIONARY))
 
         assertThat(fixed[CaptureResult.CONTROL_AE_MODE])
             .isEqualTo(CaptureResult.CONTROL_AE_MODE_OFF)
@@ -56,4 +55,4 @@
         assertThat(fixed[FakeMetadata.TEST_KEY]).isEqualTo(42)
         assertThat(fixed[FakeMetadata.TEST_KEY_ABSENT]).isNull()
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
index 633ef50..5b80a9d 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
@@ -54,12 +54,8 @@
     private val streamId = StreamId(0)
     private val surfaceMap = mapOf(streamId to Surface(SurfaceTexture(1)))
 
-    private val fakeProcessor1 = FakeCaptureSequenceProcessor().also {
-        it.surfaceMap = surfaceMap
-    }
-    private val fakeProcessor2 = FakeCaptureSequenceProcessor().also {
-        it.surfaceMap = surfaceMap
-    }
+    private val fakeProcessor1 = FakeCaptureSequenceProcessor().also { it.surfaceMap = surfaceMap }
+    private val fakeProcessor2 = FakeCaptureSequenceProcessor().also { it.surfaceMap = surfaceMap }
 
     private val graphRequestProcessor1 = GraphRequestProcessor.from(fakeProcessor1)
     private val graphRequestProcessor2 = GraphRequestProcessor.from(fakeProcessor2)
@@ -77,13 +73,13 @@
 
     @Test
     fun graphProcessorSubmitsRequests() = runTest {
-        val graphProcessor = GraphProcessorImpl(
-            FakeThreads.fromTestScope(this),
-            FakeGraphConfigs.graphConfig,
-            graphState3A,
-            this,
-            arrayListOf(globalListener)
-        )
+        val graphProcessor =
+            GraphProcessorImpl(
+                FakeThreads.fromTestScope(this),
+                FakeGraphConfigs.graphConfig,
+                graphState3A,
+                this,
+                arrayListOf(globalListener))
         graphProcessor.onGraphStarted(graphRequestProcessor1)
         graphProcessor.submit(request1)
         advanceUntilIdle()
@@ -91,20 +87,19 @@
         // Make sure the requests get submitted to the request processor
         val event = fakeProcessor1.nextEvent()
         assertThat(event.requestSequence!!.captureRequestList).containsExactly(request1)
-        assertThat(event.requestSequence!!.requiredParameters).containsEntry(
-            CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42
-        )
+        assertThat(event.requestSequence!!.requiredParameters)
+            .containsEntry(CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42)
     }
 
     @Test
     fun graphProcessorSubmitsRequestsToMostRecentProcessor() = runTest {
-        val graphProcessor = GraphProcessorImpl(
-            FakeThreads.fromTestScope(this),
-            FakeGraphConfigs.graphConfig,
-            graphState3A,
-            this,
-            arrayListOf(globalListener)
-        )
+        val graphProcessor =
+            GraphProcessorImpl(
+                FakeThreads.fromTestScope(this),
+                FakeGraphConfigs.graphConfig,
+                graphState3A,
+                this,
+                arrayListOf(globalListener))
 
         graphProcessor.onGraphStarted(graphRequestProcessor1)
         graphProcessor.onGraphStarted(graphRequestProcessor2)
@@ -120,13 +115,13 @@
 
     @Test
     fun graphProcessorSubmitsQueuedRequests() = runTest {
-        val graphProcessor = GraphProcessorImpl(
-            FakeThreads.fromTestScope(this),
-            FakeGraphConfigs.graphConfig,
-            graphState3A,
-            this,
-            arrayListOf(globalListener)
-        )
+        val graphProcessor =
+            GraphProcessorImpl(
+                FakeThreads.fromTestScope(this),
+                FakeGraphConfigs.graphConfig,
+                graphState3A,
+                this,
+                arrayListOf(globalListener))
 
         graphProcessor.submit(request1)
         graphProcessor.submit(request2)
@@ -146,13 +141,13 @@
 
     @Test
     fun graphProcessorSubmitsBurstsOfRequestsTogetherWithExtras() = runTest {
-        val graphProcessor = GraphProcessorImpl(
-            FakeThreads.fromTestScope(this),
-            FakeGraphConfigs.graphConfig,
-            graphState3A,
-            this,
-            arrayListOf(globalListener)
-        )
+        val graphProcessor =
+            GraphProcessorImpl(
+                FakeThreads.fromTestScope(this),
+                FakeGraphConfigs.graphConfig,
+                graphState3A,
+                this,
+                arrayListOf(globalListener))
 
         graphProcessor.submit(listOf(request1, request2))
         graphProcessor.onGraphStarted(graphRequestProcessor1)
@@ -164,13 +159,13 @@
 
     @Test
     fun graphProcessorDoesNotForgetRejectedRequests() = runTest {
-        val graphProcessor = GraphProcessorImpl(
-            FakeThreads.fromTestScope(this),
-            FakeGraphConfigs.graphConfig,
-            graphState3A,
-            this,
-            arrayListOf(globalListener)
-        )
+        val graphProcessor =
+            GraphProcessorImpl(
+                FakeThreads.fromTestScope(this),
+                FakeGraphConfigs.graphConfig,
+                graphState3A,
+                this,
+                arrayListOf(globalListener))
 
         fakeProcessor1.rejectRequests = true
         graphProcessor.onGraphStarted(graphRequestProcessor1)
@@ -194,13 +189,13 @@
 
     @Test
     fun graphProcessorContinuesSubmittingRequestsWhenFirstRequestIsRejected() = runTest {
-        val graphProcessor = GraphProcessorImpl(
-            FakeThreads.fromTestScope(this),
-            FakeGraphConfigs.graphConfig,
-            graphState3A,
-            this,
-            arrayListOf(globalListener)
-        )
+        val graphProcessor =
+            GraphProcessorImpl(
+                FakeThreads.fromTestScope(this),
+                FakeGraphConfigs.graphConfig,
+                graphState3A,
+                this,
+                arrayListOf(globalListener))
 
         // Note: setting the requestProcessor, and calling submit() can both trigger a call
         // to submit a request.
@@ -230,36 +225,36 @@
 
     @Test
     fun graphProcessorSetsRepeatingRequest() = runTest {
-        val graphProcessor = GraphProcessorImpl(
-            FakeThreads.fromTestScope(this),
-            FakeGraphConfigs.graphConfig,
-            graphState3A,
-            this,
-            arrayListOf(globalListener)
-        )
+        val graphProcessor =
+            GraphProcessorImpl(
+                FakeThreads.fromTestScope(this),
+                FakeGraphConfigs.graphConfig,
+                graphState3A,
+                this,
+                arrayListOf(globalListener))
 
         graphProcessor.onGraphStarted(graphRequestProcessor1)
         graphProcessor.startRepeating(request1)
         graphProcessor.startRepeating(request2)
         advanceUntilIdle()
 
-        val event = fakeProcessor1.awaitEvent(request = request2) {
-            it.submit && it.requestSequence?.repeating == true
-        }
-        assertThat(event.requestSequence!!.requiredParameters).containsEntry(
-            CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42
-        )
+        val event =
+            fakeProcessor1.awaitEvent(request = request2) {
+                it.submit && it.requestSequence?.repeating == true
+            }
+        assertThat(event.requestSequence!!.requiredParameters)
+            .containsEntry(CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42)
     }
 
     @Test
     fun graphProcessorTracksRepeatingRequest() = runTest {
-        val graphProcessor = GraphProcessorImpl(
-            FakeThreads.fromTestScope(this),
-            FakeGraphConfigs.graphConfig,
-            graphState3A,
-            this,
-            arrayListOf(globalListener)
-        )
+        val graphProcessor =
+            GraphProcessorImpl(
+                FakeThreads.fromTestScope(this),
+                FakeGraphConfigs.graphConfig,
+                graphState3A,
+                this,
+                arrayListOf(globalListener))
 
         graphProcessor.onGraphStarted(graphRequestProcessor1)
         graphProcessor.startRepeating(request1)
@@ -279,13 +274,13 @@
 
     @Test
     fun graphProcessorTracksRejectedRepeatingRequests() = runTest {
-        val graphProcessor = GraphProcessorImpl(
-            FakeThreads.fromTestScope(this),
-            FakeGraphConfigs.graphConfig,
-            graphState3A,
-            this,
-            arrayListOf(globalListener)
-        )
+        val graphProcessor =
+            GraphProcessorImpl(
+                FakeThreads.fromTestScope(this),
+                FakeGraphConfigs.graphConfig,
+                graphState3A,
+                this,
+                arrayListOf(globalListener))
 
         fakeProcessor1.rejectRequests = true
         graphProcessor.onGraphStarted(graphRequestProcessor1)
@@ -300,13 +295,13 @@
 
     @Test
     fun graphProcessorSubmitsRepeatingRequestAndQueuedRequests() = runTest {
-        val graphProcessor = GraphProcessorImpl(
-            FakeThreads.fromTestScope(this),
-            FakeGraphConfigs.graphConfig,
-            graphState3A,
-            this,
-            arrayListOf(globalListener)
-        )
+        val graphProcessor =
+            GraphProcessorImpl(
+                FakeThreads.fromTestScope(this),
+                FakeGraphConfigs.graphConfig,
+                graphState3A,
+                this,
+                arrayListOf(globalListener))
 
         graphProcessor.startRepeating(request1)
         graphProcessor.submit(request2)
@@ -319,25 +314,28 @@
 
         // Loop until we see at least one repeating request, and one submit event.
         launch {
-            while (!hasRequest1Event && !hasRequest2Event) {
-                val event = fakeProcessor1.nextEvent()
-                hasRequest1Event = hasRequest1Event ||
-                    event.requestSequence?.captureRequestList?.contains(request1) ?: false
-                hasRequest2Event = hasRequest2Event ||
-                    event.requestSequence?.captureRequestList?.contains(request2) ?: false
+                while (!hasRequest1Event && !hasRequest2Event) {
+                    val event = fakeProcessor1.nextEvent()
+                    hasRequest1Event =
+                        hasRequest1Event ||
+                            event.requestSequence?.captureRequestList?.contains(request1) ?: false
+                    hasRequest2Event =
+                        hasRequest2Event ||
+                            event.requestSequence?.captureRequestList?.contains(request2) ?: false
+                }
             }
-        }.join()
+            .join()
     }
 
     @Test
     fun graphProcessorAbortsQueuedRequests() = runTest {
-        val graphProcessor = GraphProcessorImpl(
-            FakeThreads.fromTestScope(this),
-            FakeGraphConfigs.graphConfig,
-            graphState3A,
-            this,
-            arrayListOf(globalListener)
-        )
+        val graphProcessor =
+            GraphProcessorImpl(
+                FakeThreads.fromTestScope(this),
+                FakeGraphConfigs.graphConfig,
+                graphState3A,
+                this,
+                arrayListOf(globalListener))
 
         graphProcessor.startRepeating(request1)
         graphProcessor.submit(request2)
@@ -346,9 +344,8 @@
         graphProcessor.abort()
         graphProcessor.onGraphStarted(graphRequestProcessor1)
 
-        val abortEvent1 = withTimeoutOrNull(timeMillis = 50L) {
-            requestListener1.onAbortedFlow.firstOrNull()
-        }
+        val abortEvent1 =
+            withTimeoutOrNull(timeMillis = 50L) { requestListener1.onAbortedFlow.firstOrNull() }
         val abortEvent2 = requestListener2.onAbortedFlow.first()
         val globalAbortEvent = globalListener.onAbortedFlow.first()
 
@@ -363,13 +360,13 @@
 
     @Test
     fun closingGraphProcessorAbortsSubsequentRequests() = runTest {
-        val graphProcessor = GraphProcessorImpl(
-            FakeThreads.fromTestScope(this),
-            FakeGraphConfigs.graphConfig,
-            graphState3A,
-            this,
-            arrayListOf(globalListener)
-        )
+        val graphProcessor =
+            GraphProcessorImpl(
+                FakeThreads.fromTestScope(this),
+                FakeGraphConfigs.graphConfig,
+                graphState3A,
+                this,
+                arrayListOf(globalListener))
         graphProcessor.close()
 
         // Abort queued and in-flight requests.
@@ -377,9 +374,8 @@
         graphProcessor.startRepeating(request1)
         graphProcessor.submit(request2)
 
-        val abortEvent1 = withTimeoutOrNull(timeMillis = 50L) {
-            requestListener1.onAbortedFlow.firstOrNull()
-        }
+        val abortEvent1 =
+            withTimeoutOrNull(timeMillis = 50L) { requestListener1.onAbortedFlow.firstOrNull() }
         val abortEvent2 = requestListener2.onAbortedFlow.first()
         assertThat(abortEvent1).isNull()
         assertThat(abortEvent2.request).isSameInstanceAs(request2)
@@ -389,42 +385,34 @@
 
     @Test
     fun graphProcessorChangesGraphStateOnError() = runTest {
-        val graphProcessor = GraphProcessorImpl(
-            FakeThreads.fromTestScope(this),
-            FakeGraphConfigs.graphConfig,
-            graphState3A,
-            this,
-            arrayListOf(globalListener)
-        )
+        val graphProcessor =
+            GraphProcessorImpl(
+                FakeThreads.fromTestScope(this),
+                FakeGraphConfigs.graphConfig,
+                graphState3A,
+                this,
+                arrayListOf(globalListener))
         assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
 
         graphProcessor.onGraphStarted(graphRequestProcessor1)
         graphProcessor.onGraphError(
-            GraphStateError(
-                CameraError.ERROR_CAMERA_DEVICE,
-                willAttemptRetry = true
-            )
-        )
+            GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true))
         assertThat(graphProcessor.graphState.value).isInstanceOf(GraphStateError::class.java)
     }
 
     @Test
     fun graphProcessorDropsStaleErrors() = runTest {
-        val graphProcessor = GraphProcessorImpl(
-            FakeThreads.fromTestScope(this),
-            FakeGraphConfigs.graphConfig,
-            graphState3A,
-            this,
-            arrayListOf(globalListener)
-        )
+        val graphProcessor =
+            GraphProcessorImpl(
+                FakeThreads.fromTestScope(this),
+                FakeGraphConfigs.graphConfig,
+                graphState3A,
+                this,
+                arrayListOf(globalListener))
         assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
 
         graphProcessor.onGraphError(
-            GraphStateError(
-                CameraError.ERROR_CAMERA_DEVICE,
-                willAttemptRetry = true
-            )
-        )
+            GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true))
         assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
 
         graphProcessor.onGraphStarting()
@@ -433,21 +421,13 @@
         // GraphProcessor should drop errors while the camera graph is stopping.
         graphProcessor.onGraphStopping()
         graphProcessor.onGraphError(
-            GraphStateError(
-                CameraError.ERROR_CAMERA_DEVICE,
-                willAttemptRetry = true
-            )
-        )
+            GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true))
         assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
 
         // GraphProcessor should also drop errors while the camera graph is stopped.
         graphProcessor.onGraphStopped(graphRequestProcessor1)
         graphProcessor.onGraphError(
-            GraphStateError(
-                CameraError.ERROR_CAMERA_DEVICE,
-                willAttemptRetry = true
-            )
-        )
+            GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true))
         assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphTestContext.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphTestContext.kt
index 5077416..93d4a6e 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphTestContext.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphTestContext.kt
@@ -22,9 +22,8 @@
 import androidx.camera.camera2.pipe.StreamId
 import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor
 import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
-import java.io.Closeable
 
-internal class GraphTestContext : Closeable {
+internal class GraphTestContext : AutoCloseable {
     val streamId = StreamId(0)
     val surfaceMap = mapOf(streamId to Surface(SurfaceTexture(1)))
     val captureSequenceProcessor = FakeCaptureSequenceProcessor()
@@ -40,4 +39,4 @@
     override fun close() {
         surfaceMap[streamId]?.release()
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Listener3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Listener3ATest.kt
index cf3ff6a..0bc44fd 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Listener3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Listener3ATest.kt
@@ -36,27 +36,21 @@
 internal class Listener3ATest {
     @Test
     fun testListenersInvoked() {
-        val result3AStateListener = Result3AStateListenerImpl(
-            mapOf(
-                CaptureResult.CONTROL_AF_MODE to listOf(CaptureResult.CONTROL_AF_MODE_AUTO)
-            )
-        )
+        val result3AStateListener =
+            Result3AStateListenerImpl(
+                mapOf(CaptureResult.CONTROL_AF_MODE to listOf(CaptureResult.CONTROL_AF_MODE_AUTO)))
         val listener3A = Listener3A()
         listener3A.addListener(result3AStateListener)
 
         // The deferred result of 3a state listener shouldn't be complete right now.
         assertThat(result3AStateListener.result.isCompleted).isFalse()
-        listener3A.onRequestSequenceCreated(
-            FakeRequestMetadata(requestNumber = RequestNumber(1))
-        )
+        listener3A.onRequestSequenceCreated(FakeRequestMetadata(requestNumber = RequestNumber(1)))
 
         val requestMetadata = FakeRequestMetadata(requestNumber = RequestNumber(1))
         val frameNumber = FrameNumber(1L)
-        val captureResult = FakeFrameMetadata(
-            mapOf(
-                CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_AUTO
-            )
-        )
+        val captureResult =
+            FakeFrameMetadata(
+                mapOf(CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_AUTO))
         // Once the correct metadata is updated the listener3A should broadcast it to the
         // result3AState listener added to it, making the deferred result complete.
         listener3A.onPartialCaptureResult(requestMetadata, frameNumber, captureResult)
@@ -65,31 +59,28 @@
 
     @Test
     fun testListenersInvokedWithMultipleUpdates() {
-        val result3AStateListener = UpdateCounting3AStateListener(
-            Result3AStateListenerImpl(
-                mapOf(
-                    CaptureResult.CONTROL_AF_MODE to listOf(CaptureResult.CONTROL_AF_MODE_AUTO)
-                )
-            )
-        )
+        val result3AStateListener =
+            UpdateCounting3AStateListener(
+                Result3AStateListenerImpl(
+                    mapOf(
+                        CaptureResult.CONTROL_AF_MODE to
+                            listOf(CaptureResult.CONTROL_AF_MODE_AUTO))))
         val listener3A = Listener3A()
         listener3A.addListener(result3AStateListener)
 
-        listener3A.onRequestSequenceCreated(
-            FakeRequestMetadata(requestNumber = RequestNumber(1))
-        )
+        listener3A.onRequestSequenceCreated(FakeRequestMetadata(requestNumber = RequestNumber(1)))
 
         val requestMetadata = FakeRequestMetadata(requestNumber = RequestNumber(1))
-        val captureResult = FakeFrameMetadata(
-            mapOf(
-                CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE
-            )
-        )
-        val captureResult1 = FakeFrameMetadata(
-            mapOf(
-                CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE
-            )
-        )
+        val captureResult =
+            FakeFrameMetadata(
+                mapOf(
+                    CaptureResult.CONTROL_AF_MODE to
+                        CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE))
+        val captureResult1 =
+            FakeFrameMetadata(
+                mapOf(
+                    CaptureResult.CONTROL_AF_MODE to
+                        CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE))
         listener3A.onPartialCaptureResult(requestMetadata, FrameNumber(1), captureResult)
         assertThat(result3AStateListener.updateCount).isEqualTo(1)
 
@@ -101,20 +92,18 @@
 
     @Test
     fun testListenersAreRemovedWhenDone() {
-        val result3AStateListener1 = UpdateCounting3AStateListener(
-            Result3AStateListenerImpl(
-                mapOf(
-                    CaptureResult.CONTROL_AF_MODE to listOf(CaptureResult.CONTROL_AF_MODE_AUTO)
-                )
-            )
-        )
-        val result3AStateListener2 = UpdateCounting3AStateListener(
-            Result3AStateListenerImpl(
-                mapOf(
-                    CaptureResult.CONTROL_AE_MODE to listOf(CaptureResult.CONTROL_AE_MODE_OFF)
-                )
-            )
-        )
+        val result3AStateListener1 =
+            UpdateCounting3AStateListener(
+                Result3AStateListenerImpl(
+                    mapOf(
+                        CaptureResult.CONTROL_AF_MODE to
+                            listOf(CaptureResult.CONTROL_AF_MODE_AUTO))))
+        val result3AStateListener2 =
+            UpdateCounting3AStateListener(
+                Result3AStateListenerImpl(
+                    mapOf(
+                        CaptureResult.CONTROL_AE_MODE to
+                            listOf(CaptureResult.CONTROL_AE_MODE_OFF))))
 
         val listener3A = Listener3A()
         listener3A.addListener(result3AStateListener1)
@@ -122,19 +111,15 @@
 
         val requestMetadata = FakeRequestMetadata(requestNumber = RequestNumber(1))
         val frameNumber = FrameNumber(1L)
-        val captureResult = FakeFrameMetadata(
-            mapOf(
-                CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_AUTO
-            )
-        )
+        val captureResult =
+            FakeFrameMetadata(
+                mapOf(CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_AUTO))
 
         // There should be no update to either of the listeners right now.
         assertThat(result3AStateListener1.updateCount).isEqualTo(0)
         assertThat(result3AStateListener2.updateCount).isEqualTo(0)
 
-        listener3A.onRequestSequenceCreated(
-            FakeRequestMetadata(requestNumber = RequestNumber(1))
-        )
+        listener3A.onRequestSequenceCreated(FakeRequestMetadata(requestNumber = RequestNumber(1)))
 
         // Once the metadata for correct AF mode is updated, the listener3A should broadcast it to
         // the result3AState listeners added to it, making result3AStateListener1 complete.
@@ -145,12 +130,11 @@
         // Once the metadata for correct AE mode is updated, the listener3A should broadcast it to
         // the result3AState listeners added to it, making result3AStateListener2 complete. Since
         // result3AStateListener1 was already completed it will not be updated again.
-        val captureResult1 = FakeFrameMetadata(
-            mapOf(
-                CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_AUTO,
-                CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_OFF
-            )
-        )
+        val captureResult1 =
+            FakeFrameMetadata(
+                mapOf(
+                    CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_AUTO,
+                    CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_OFF))
         listener3A.onPartialCaptureResult(requestMetadata, frameNumber, captureResult1)
         assertThat(result3AStateListener1.updateCount).isEqualTo(1)
         assertThat(result3AStateListener2.updateCount).isEqualTo(2)
@@ -161,4 +145,4 @@
         assertThat(result3AStateListener1.updateCount).isEqualTo(1)
         assertThat(result3AStateListener2.updateCount).isEqualTo(2)
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Result3AStateListenerImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Result3AStateListenerImplTest.kt
index 32f3379..a7bcd0b 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Result3AStateListenerImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Result3AStateListenerImplTest.kt
@@ -55,34 +55,29 @@
 
     @Test
     fun testWithNoUpdate() {
-        val listenerForKeys = Result3AStateListenerImpl(
-            mapOf(
-                CaptureResult.CONTROL_AF_STATE to
-                    listOf(
-                        CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
-                    )
-            )
-        )
+        val listenerForKeys =
+            Result3AStateListenerImpl(
+                mapOf(
+                    CaptureResult.CONTROL_AF_STATE to
+                        listOf(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED)))
         assertThat(listenerForKeys.result.isCompleted).isFalse()
     }
 
     @Test
     fun testKeyWithUndesirableValueInFrameMetadata() {
-        val listenerForKeys = Result3AStateListenerImpl(
-            mapOf(
-                CaptureResult.CONTROL_AF_STATE to
-                    listOf(
-                        CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
-                    )
-            )
-        )
+        val listenerForKeys =
+            Result3AStateListenerImpl(
+                mapOf(
+                    CaptureResult.CONTROL_AF_STATE to
+                        listOf(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED)))
         listenerForKeys.onRequestSequenceCreated(RequestNumber(1))
 
-        val frameMetadata = FakeFrameMetadata(
-            resultMetadata = mapOf(
-                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN
-            )
-        )
+        val frameMetadata =
+            FakeFrameMetadata(
+                resultMetadata =
+                    mapOf(
+                        CaptureResult.CONTROL_AF_STATE to
+                            CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN))
 
         listenerForKeys.update(RequestNumber(1), frameMetadata)
         assertThat(listenerForKeys.result.isCompleted).isFalse()
@@ -90,160 +85,159 @@
 
     @Test
     fun testKeyWithDesirableValueInFrameMetadata() {
-        val listenerForKeys = Result3AStateListenerImpl(
-            mapOf(
-                CaptureResult.CONTROL_AF_STATE to
-                    listOf(
-                        CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
-                    )
-            )
-        )
+        val listenerForKeys =
+            Result3AStateListenerImpl(
+                mapOf(
+                    CaptureResult.CONTROL_AF_STATE to
+                        listOf(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED)))
         listenerForKeys.onRequestSequenceCreated(RequestNumber(1))
 
-        val frameMetadata = FakeFrameMetadata(
-            resultMetadata = mapOf(
-                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
-            )
-        )
+        val frameMetadata =
+            FakeFrameMetadata(
+                resultMetadata =
+                    mapOf(
+                        CaptureResult.CONTROL_AF_STATE to
+                            CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED))
         listenerForKeys.update(RequestNumber(1), frameMetadata)
         assertThat(listenerForKeys.result.isCompleted).isTrue()
     }
 
     @Test
     fun testKeyNotPresentInFrameMetadata() {
-        val listenerForKeys = Result3AStateListenerImpl(
-            mapOf(
-                CaptureResult.CONTROL_AF_STATE to
-                    listOf(
-                        CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
-                    )
-            )
-        )
+        val listenerForKeys =
+            Result3AStateListenerImpl(
+                mapOf(
+                    CaptureResult.CONTROL_AF_STATE to
+                        listOf(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED)))
         listenerForKeys.onRequestSequenceCreated(RequestNumber(1))
 
-        val frameMetadata = FakeFrameMetadata(
-            resultMetadata = mapOf(
-                CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_CONVERGED
-            )
-        )
+        val frameMetadata =
+            FakeFrameMetadata(
+                resultMetadata =
+                    mapOf(
+                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_CONVERGED))
         listenerForKeys.update(RequestNumber(1), frameMetadata)
         assertThat(listenerForKeys.result.isCompleted).isFalse()
     }
 
     @Test
     fun testMultipleKeysWithDesiredValuesInFrameMetadata() {
-        val listenerForKeys = Result3AStateListenerImpl(
-            mapOf(
-                CaptureResult.CONTROL_AF_STATE to
-                    listOf(
-                        CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
-                        CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED
-                    ),
-                CaptureResult.CONTROL_AE_STATE to listOf(CaptureResult.CONTROL_AE_STATE_LOCKED)
-            )
-        )
+        val listenerForKeys =
+            Result3AStateListenerImpl(
+                mapOf(
+                    CaptureResult.CONTROL_AF_STATE to
+                        listOf(
+                            CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                            CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED),
+                    CaptureResult.CONTROL_AE_STATE to
+                        listOf(CaptureResult.CONTROL_AE_STATE_LOCKED)))
         listenerForKeys.onRequestSequenceCreated(RequestNumber(1))
 
-        val frameMetadata = FakeFrameMetadata(
-            resultMetadata = mapOf(
-                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
-                CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
-            )
-        )
+        val frameMetadata =
+            FakeFrameMetadata(
+                resultMetadata =
+                    mapOf(
+                        CaptureResult.CONTROL_AF_STATE to
+                            CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED))
         listenerForKeys.update(RequestNumber(1), frameMetadata)
         assertThat(listenerForKeys.result.isCompleted).isTrue()
     }
 
     @Test
     fun testMultipleKeysWithDesiredValuesInFrameMetadataForASubset() {
-        val listenerForKeys = Result3AStateListenerImpl(
-            mapOf(
-                CaptureResult.CONTROL_AF_STATE to
-                    listOf(
-                        CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
-                        CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED
-                    ),
-                CaptureResult.CONTROL_AE_STATE to listOf(CaptureResult.CONTROL_AE_STATE_LOCKED)
-            )
-        )
+        val listenerForKeys =
+            Result3AStateListenerImpl(
+                mapOf(
+                    CaptureResult.CONTROL_AF_STATE to
+                        listOf(
+                            CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                            CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED),
+                    CaptureResult.CONTROL_AE_STATE to
+                        listOf(CaptureResult.CONTROL_AE_STATE_LOCKED)))
         listenerForKeys.onRequestSequenceCreated(RequestNumber(1))
 
-        val frameMetadata = FakeFrameMetadata(
-            resultMetadata = mapOf(
-                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
-            )
-        )
+        val frameMetadata =
+            FakeFrameMetadata(
+                resultMetadata =
+                    mapOf(
+                        CaptureResult.CONTROL_AF_STATE to
+                            CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED))
         listenerForKeys.update(RequestNumber(1), frameMetadata)
         assertThat(listenerForKeys.result.isCompleted).isFalse()
     }
 
     @Test
     fun testMultipleUpdates() {
-        val listenerForKeys = Result3AStateListenerImpl(
-            mapOf(
-                CaptureResult.CONTROL_AF_STATE to
-                    listOf(
-                        CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
-                        CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED
-                    ),
-                CaptureResult.CONTROL_AE_STATE to listOf(CaptureResult.CONTROL_AE_STATE_LOCKED)
-            )
-        )
+        val listenerForKeys =
+            Result3AStateListenerImpl(
+                mapOf(
+                    CaptureResult.CONTROL_AF_STATE to
+                        listOf(
+                            CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                            CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED),
+                    CaptureResult.CONTROL_AE_STATE to
+                        listOf(CaptureResult.CONTROL_AE_STATE_LOCKED)))
         listenerForKeys.onRequestSequenceCreated(RequestNumber(1))
 
-        val frameMetadata = FakeFrameMetadata(
-            resultMetadata = mapOf(
-                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
-            )
-        )
+        val frameMetadata =
+            FakeFrameMetadata(
+                resultMetadata =
+                    mapOf(
+                        CaptureResult.CONTROL_AF_STATE to
+                            CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED))
         listenerForKeys.update(RequestNumber(1), frameMetadata)
         assertThat(listenerForKeys.result.isCompleted).isFalse()
 
-        val frameMetadata1 = FakeFrameMetadata(
-            resultMetadata = mapOf(
-                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
-                CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
-            )
-        )
+        val frameMetadata1 =
+            FakeFrameMetadata(
+                resultMetadata =
+                    mapOf(
+                        CaptureResult.CONTROL_AF_STATE to
+                            CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED))
         listenerForKeys.update(RequestNumber(1), frameMetadata1)
         assertThat(listenerForKeys.result.isCompleted).isTrue()
     }
 
     @Test
     fun testTimeLimit() {
-        val listenerForKeys = Result3AStateListenerImpl(
-            exitConditionForKeys = mapOf(
-                CaptureResult.CONTROL_AF_STATE to
-                    listOf(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED)
-            ),
-            timeLimitNs = 1000000000L
-        )
+        val listenerForKeys =
+            Result3AStateListenerImpl(
+                exitConditionForKeys =
+                    mapOf(
+                        CaptureResult.CONTROL_AF_STATE to
+                            listOf(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED)),
+                timeLimitNs = 1000000000L)
         listenerForKeys.onRequestSequenceCreated(RequestNumber(1))
 
-        val frameMetadata1 = FakeFrameMetadata(
-            resultMetadata = mapOf(
-                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
-                CaptureResult.SENSOR_TIMESTAMP to 400000000L
-            )
-        )
+        val frameMetadata1 =
+            FakeFrameMetadata(
+                resultMetadata =
+                    mapOf(
+                        CaptureResult.CONTROL_AF_STATE to
+                            CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                        CaptureResult.SENSOR_TIMESTAMP to 400000000L))
         listenerForKeys.update(RequestNumber(1), frameMetadata1)
         assertThat(listenerForKeys.result.isCompleted).isFalse()
 
-        val frameMetadata2 = FakeFrameMetadata(
-            resultMetadata = mapOf(
-                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
-                CaptureResult.SENSOR_TIMESTAMP to 900000000L
-            )
-        )
+        val frameMetadata2 =
+            FakeFrameMetadata(
+                resultMetadata =
+                    mapOf(
+                        CaptureResult.CONTROL_AF_STATE to
+                            CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                        CaptureResult.SENSOR_TIMESTAMP to 900000000L))
         listenerForKeys.update(RequestNumber(1), frameMetadata2)
         assertThat(listenerForKeys.result.isCompleted).isFalse()
 
-        val frameMetadata3 = FakeFrameMetadata(
-            resultMetadata = mapOf(
-                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
-                CaptureResult.SENSOR_TIMESTAMP to 1500000000L
-            )
-        )
+        val frameMetadata3 =
+            FakeFrameMetadata(
+                resultMetadata =
+                    mapOf(
+                        CaptureResult.CONTROL_AF_STATE to
+                            CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                        CaptureResult.SENSOR_TIMESTAMP to 1500000000L))
         listenerForKeys.update(RequestNumber(1), frameMetadata3)
         val completedDeferred = listenerForKeys.result
         assertThat(completedDeferred.isCompleted).isTrue()
@@ -253,54 +247,58 @@
 
     @Test
     fun testFrameLimit() {
-        val listenerForKeys = Result3AStateListenerImpl(
-            exitConditionForKeys = mapOf(
-                CaptureResult.CONTROL_AF_STATE to
-                    listOf(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED)
-            ),
-            frameLimit = 10
-        )
+        val listenerForKeys =
+            Result3AStateListenerImpl(
+                exitConditionForKeys =
+                    mapOf(
+                        CaptureResult.CONTROL_AF_STATE to
+                            listOf(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED)),
+                frameLimit = 10)
         listenerForKeys.onRequestSequenceCreated(RequestNumber(1))
 
         listenerForKeys.onRequestSequenceCreated(RequestNumber(1))
 
-        val frameMetadata1 = FakeFrameMetadata(
-            resultMetadata = mapOf(
-                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
-                CaptureResult.SENSOR_TIMESTAMP to 400000000L
-            ),
-            frameNumber = FrameNumber(1)
-        )
+        val frameMetadata1 =
+            FakeFrameMetadata(
+                resultMetadata =
+                    mapOf(
+                        CaptureResult.CONTROL_AF_STATE to
+                            CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                        CaptureResult.SENSOR_TIMESTAMP to 400000000L),
+                frameNumber = FrameNumber(1))
         listenerForKeys.update(RequestNumber(1), frameMetadata1)
         assertThat(listenerForKeys.result.isCompleted).isFalse()
 
-        val frameMetadata2 = FakeFrameMetadata(
-            resultMetadata = mapOf(
-                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
-                CaptureResult.SENSOR_TIMESTAMP to 900000000L
-            ),
-            frameNumber = FrameNumber(3)
-        )
+        val frameMetadata2 =
+            FakeFrameMetadata(
+                resultMetadata =
+                    mapOf(
+                        CaptureResult.CONTROL_AF_STATE to
+                            CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                        CaptureResult.SENSOR_TIMESTAMP to 900000000L),
+                frameNumber = FrameNumber(3))
         listenerForKeys.update(RequestNumber(1), frameMetadata2)
         assertThat(listenerForKeys.result.isCompleted).isFalse()
 
-        val frameMetadata3 = FakeFrameMetadata(
-            resultMetadata = mapOf(
-                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
-                CaptureResult.SENSOR_TIMESTAMP to 1500000000L
-            ),
-            frameNumber = FrameNumber(10)
-        )
+        val frameMetadata3 =
+            FakeFrameMetadata(
+                resultMetadata =
+                    mapOf(
+                        CaptureResult.CONTROL_AF_STATE to
+                            CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                        CaptureResult.SENSOR_TIMESTAMP to 1500000000L),
+                frameNumber = FrameNumber(10))
         listenerForKeys.update(RequestNumber(1), frameMetadata3)
         assertThat(listenerForKeys.result.isCompleted).isFalse()
 
-        val frameMetadata4 = FakeFrameMetadata(
-            resultMetadata = mapOf(
-                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
-                CaptureResult.SENSOR_TIMESTAMP to 1700000000L
-            ),
-            frameNumber = FrameNumber(12)
-        )
+        val frameMetadata4 =
+            FakeFrameMetadata(
+                resultMetadata =
+                    mapOf(
+                        CaptureResult.CONTROL_AF_STATE to
+                            CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                        CaptureResult.SENSOR_TIMESTAMP to 1700000000L),
+                frameNumber = FrameNumber(12))
         listenerForKeys.update(RequestNumber(1), frameMetadata4)
         val completedDeferred = listenerForKeys.result
 
@@ -311,20 +309,18 @@
 
     @Test
     fun testIgnoreUpdatesFromEarlierRequests() {
-        val listenerForKeys = Result3AStateListenerImpl(
-            mapOf(
-                CaptureResult.CONTROL_AF_STATE to
-                    listOf(
-                        CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
-                    )
-            )
-        )
+        val listenerForKeys =
+            Result3AStateListenerImpl(
+                mapOf(
+                    CaptureResult.CONTROL_AF_STATE to
+                        listOf(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED)))
 
-        val frameMetadata = FakeFrameMetadata(
-            resultMetadata = mapOf(
-                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
-            )
-        )
+        val frameMetadata =
+            FakeFrameMetadata(
+                resultMetadata =
+                    mapOf(
+                        CaptureResult.CONTROL_AF_STATE to
+                            CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED))
         // The reference request number of not yet set on the listener, so the update will be
         // ignored.
         listenerForKeys.update(RequestNumber(1), frameMetadata)
@@ -345,4 +341,4 @@
         listenerForKeys.update(RequestNumber(3), frameMetadata)
         assertThat(listenerForKeys.result.isCompleted).isTrue()
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/StreamGraphImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/StreamGraphImplTest.kt
index 83ecdba..388e8b6 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/StreamGraphImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/StreamGraphImplTest.kt
@@ -78,26 +78,18 @@
     @Test
     fun testStreamWithMultipleOutputs() {
 
-        val streamConfig = CameraStream.Config.create(
-            listOf(
-                OutputStream.Config.create(
-                    Size(800, 600),
-                    StreamFormat.YUV_420_888
-                ),
-                OutputStream.Config.create(
-                    Size(1600, 1200),
-                    StreamFormat.YUV_420_888
-                ),
-                OutputStream.Config.create(
-                    Size(800, 600),
-                    StreamFormat.YUV_420_888
-                ),
+        val streamConfig =
+            CameraStream.Config.create(
+                listOf(
+                    OutputStream.Config.create(Size(800, 600), StreamFormat.YUV_420_888),
+                    OutputStream.Config.create(Size(1600, 1200), StreamFormat.YUV_420_888),
+                    OutputStream.Config.create(Size(800, 600), StreamFormat.YUV_420_888),
+                ))
+        val graphConfig =
+            CameraGraph.Config(
+                camera = CameraId("TestCamera"),
+                streams = listOf(streamConfig),
             )
-        )
-        val graphConfig = CameraGraph.Config(
-            camera = CameraId("TestCamera"),
-            streams = listOf(streamConfig),
-        )
         val streamGraph = StreamGraphImpl(config.fakeMetadata, graphConfig)
 
         assertThat(streamGraph.streams).hasSize(1)
@@ -228,4 +220,4 @@
         assertThat(stream2.outputs.single().streamUseCase)
             .isEqualTo(OutputStream.StreamUseCase.VIDEO_RECORD)
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/SurfaceGraphTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/SurfaceGraphTest.kt
index 57809dd..dc160cf 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/SurfaceGraphTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/SurfaceGraphTest.kt
@@ -45,9 +45,8 @@
     private val streamMap = StreamGraphImpl(config.fakeMetadata, config.graphConfig)
     private val controller = FakeCameraController()
     private val fakeSurfaceListener: CameraSurfaceManager.SurfaceListener = mock()
-    private val cameraSurfaceManager = CameraSurfaceManager().also {
-        it.addListener(fakeSurfaceListener)
-    }
+    private val cameraSurfaceManager =
+        CameraSurfaceManager().also { it.addListener(fakeSurfaceListener) }
     private val surfaceGraph = SurfaceGraph(streamMap, controller, cameraSurfaceManager)
 
     private val stream1 = streamMap[config.streamConfig1]!!
@@ -232,8 +231,6 @@
     @Test
     fun surfaceGraphDoesNotAllowDuplicateSurfaces() {
         surfaceGraph[stream1.id] = fakeSurface1
-        assertThrows<Exception> {
-            surfaceGraph[stream2.id] = fakeSurface1
-        }
+        assertThrows<Exception> { surfaceGraph[stream2.id] = fakeSurface1 }
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraController.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraController.kt
index d465a0f..b637133 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraController.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraController.kt
@@ -41,4 +41,4 @@
     override fun updateSurfaceMap(surfaceMap: Map<StreamId, Surface>) {
         this.surfaceMap = surfaceMap
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt
index c854130..33d454e 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt
@@ -33,9 +33,7 @@
 import androidx.camera.camera2.pipe.compat.SessionConfigData
 import kotlin.reflect.KClass
 
-/**
- * Fake implementation of [CameraDeviceWrapper] for tests.
- */
+/** Fake implementation of [CameraDeviceWrapper] for tests. */
 internal class FakeCameraDeviceWrapper(val fakeCamera: RobolectricCameras.FakeCamera) :
     CameraDeviceWrapper {
     override val cameraId: CameraId
@@ -55,8 +53,7 @@
             return Api23Compat.createReprocessCaptureRequest(fakeCamera.cameraDevice, inputResult)
         }
         throw UnsupportedOperationException(
-            "createReprocessCaptureRequest is not supported below API 23"
-        )
+            "createReprocessCaptureRequest is not supported below API 23")
     }
 
     override fun createCaptureSession(
@@ -119,8 +116,9 @@
     }
 
     @Suppress("UNCHECKED_CAST")
-    override fun <T : Any> unwrapAs(type: KClass<T>): T? = when (type) {
-        CameraDevice::class -> fakeCamera.cameraDevice as T
-        else -> null
-    }
-}
\ No newline at end of file
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? =
+        when (type) {
+            CameraDevice::class -> fakeCamera.cameraDevice as T
+            else -> null
+        }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadataProvider.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadataProvider.kt
index be5afc0..dc613b6 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadataProvider.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadataProvider.kt
@@ -18,14 +18,17 @@
 
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.CameraMetadata
-import androidx.camera.camera2.pipe.compat.CameraMetadataProvider
+import androidx.camera.camera2.pipe.compat.Camera2MetadataProvider
 
-/**
- * Utility class for providing fake metadata for tests.
- */
+/** Utility class for providing fake metadata for tests. */
 class FakeCameraMetadataProvider(
     private val fakeMetadata: Map<CameraId, CameraMetadata> = emptyMap()
-) : CameraMetadataProvider {
-    override suspend fun getMetadata(cameraId: CameraId): CameraMetadata = awaitMetadata(cameraId)
-    override fun awaitMetadata(cameraId: CameraId): CameraMetadata = fakeMetadata[cameraId]!!
-}
\ No newline at end of file
+) : Camera2MetadataProvider {
+    override suspend fun getCameraMetadata(cameraId: CameraId): CameraMetadata =
+        awaitCameraMetadata(cameraId)
+
+    override fun awaitCameraMetadata(cameraId: CameraId): CameraMetadata =
+        checkNotNull(fakeMetadata[cameraId]) {
+            "Failed to find metadata for $cameraId. Available fakeMetadata is $fakeMetadata"
+        }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCaptureSessionFactory.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCaptureSessionFactory.kt
index f3da831..f9cd71e 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCaptureSessionFactory.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCaptureSessionFactory.kt
@@ -23,9 +23,7 @@
 import androidx.camera.camera2.pipe.compat.CaptureSessionState
 import androidx.camera.camera2.pipe.compat.OutputConfigurationWrapper
 
-/**
- * Fake [CaptureSessionFactory] for use in tests. This fake does NOT invoke callbacks.
- */
+/** Fake [CaptureSessionFactory] for use in tests. This fake does NOT invoke callbacks. */
 internal class FakeCaptureSessionFactory(
     private val requiredStreams: Set<StreamId>,
     private val deferrableStreams: Set<StreamId>
@@ -50,4 +48,4 @@
         check(deferrableStreams.containsAll(deferredStreams))
         return deferredStreams.associateWith { FakeOutputConfigurationWrapper() }
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCaptureSessionWrapper.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCaptureSessionWrapper.kt
index 60b41dd..9688607e 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCaptureSessionWrapper.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCaptureSessionWrapper.kt
@@ -100,9 +100,7 @@
     }
 
     override fun finalizeOutputConfigurations(outputConfigs: List<OutputConfigurationWrapper>) {
-        throw UnsupportedOperationException(
-            "finalizeOutputConfigurations is not supported"
-        )
+        throw UnsupportedOperationException("finalizeOutputConfigurations is not supported")
     }
 
     override fun <T : Any> unwrapAs(type: KClass<T>): T? {
@@ -113,4 +111,4 @@
     override fun close() {
         closed = true
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphConfigs.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphConfigs.kt
index 862313f..8bd64cd 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphConfigs.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphConfigs.kt
@@ -33,109 +33,92 @@
     private val camera1 = CameraId("TestCamera-1")
     private val camera2 = CameraId("TestCamera-2")
 
-    val fakeMetadata = FakeCameraMetadata(
-        mapOf(
-            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL to
-                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
-            CameraCharacteristics.LENS_FACING to
-                CameraCharacteristics.LENS_FACING_BACK
-        ),
-        cameraId = camera1
-    )
-    val fakeMetadata2 = FakeCameraMetadata(
-        mapOf(
-            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL to
-                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
-            CameraCharacteristics.LENS_FACING to
-                CameraCharacteristics.LENS_FACING_FRONT
-        ),
-        cameraId = camera2
-    )
-    val fakeCameraBackend = FakeCameraBackend(
-        mapOf(
-            fakeMetadata.camera to fakeMetadata,
-            fakeMetadata2.camera to fakeMetadata2
-        )
-    )
+    val fakeMetadata =
+        FakeCameraMetadata(
+            mapOf(
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL to
+                    CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+                CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK),
+            cameraId = camera1)
+    val fakeMetadata2 =
+        FakeCameraMetadata(
+            mapOf(
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL to
+                    CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+                CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT),
+            cameraId = camera2)
+    val fakeCameraBackend =
+        FakeCameraBackend(
+            mapOf(fakeMetadata.camera to fakeMetadata, fakeMetadata2.camera to fakeMetadata2))
 
-    val streamConfig1 = CameraStream.Config.create(
-        size = Size(100, 100),
-        format = StreamFormat.YUV_420_888
-    )
-    val streamConfig2 = CameraStream.Config.create(
-        size = Size(123, 321),
-        format = StreamFormat.YUV_420_888,
-        camera = camera1
-    )
-    val streamConfig3 = CameraStream.Config.create(
-        size = Size(200, 200),
-        format = StreamFormat.YUV_420_888,
-        camera = camera2,
-        outputType = OutputStream.OutputType.SURFACE_TEXTURE
-    )
-    val streamConfig4 = CameraStream.Config.create(
-        size = Size(200, 200),
-        format = StreamFormat.YUV_420_888,
-        camera = camera2,
-        outputType = OutputStream.OutputType.SURFACE_TEXTURE,
-        mirrorMode = OutputStream.MirrorMode.MIRROR_MODE_H
-    )
-    val streamConfig5 = CameraStream.Config.create(
-        size = Size(200, 200),
-        format = StreamFormat.YUV_420_888,
-        camera = camera2,
-        outputType = OutputStream.OutputType.SURFACE_TEXTURE,
-        mirrorMode = OutputStream.MirrorMode.MIRROR_MODE_AUTO,
-        timestampBase = OutputStream.TimestampBase.TIMESTAMP_BASE_MONOTONIC
-    )
-    val streamConfig6 = CameraStream.Config.create(
-        size = Size(200, 200),
-        format = StreamFormat.YUV_420_888,
-        camera = camera2,
-        outputType = OutputStream.OutputType.SURFACE_TEXTURE,
-        mirrorMode = OutputStream.MirrorMode.MIRROR_MODE_AUTO,
-        timestampBase = OutputStream.TimestampBase.TIMESTAMP_BASE_DEFAULT,
-        dynamicRangeProfile = OutputStream.DynamicRangeProfile.PUBLIC_MAX
-    )
+    val streamConfig1 =
+        CameraStream.Config.create(size = Size(100, 100), format = StreamFormat.YUV_420_888)
+    val streamConfig2 =
+        CameraStream.Config.create(
+            size = Size(123, 321), format = StreamFormat.YUV_420_888, camera = camera1)
+    val streamConfig3 =
+        CameraStream.Config.create(
+            size = Size(200, 200),
+            format = StreamFormat.YUV_420_888,
+            camera = camera2,
+            outputType = OutputStream.OutputType.SURFACE_TEXTURE)
+    val streamConfig4 =
+        CameraStream.Config.create(
+            size = Size(200, 200),
+            format = StreamFormat.YUV_420_888,
+            camera = camera2,
+            outputType = OutputStream.OutputType.SURFACE_TEXTURE,
+            mirrorMode = OutputStream.MirrorMode.MIRROR_MODE_H)
+    val streamConfig5 =
+        CameraStream.Config.create(
+            size = Size(200, 200),
+            format = StreamFormat.YUV_420_888,
+            camera = camera2,
+            outputType = OutputStream.OutputType.SURFACE_TEXTURE,
+            mirrorMode = OutputStream.MirrorMode.MIRROR_MODE_AUTO,
+            timestampBase = OutputStream.TimestampBase.TIMESTAMP_BASE_MONOTONIC)
+    val streamConfig6 =
+        CameraStream.Config.create(
+            size = Size(200, 200),
+            format = StreamFormat.YUV_420_888,
+            camera = camera2,
+            outputType = OutputStream.OutputType.SURFACE_TEXTURE,
+            mirrorMode = OutputStream.MirrorMode.MIRROR_MODE_AUTO,
+            timestampBase = OutputStream.TimestampBase.TIMESTAMP_BASE_DEFAULT,
+            dynamicRangeProfile = OutputStream.DynamicRangeProfile.PUBLIC_MAX)
 
-    val streamConfig7 = CameraStream.Config.create(
-        size = Size(200, 200),
-        format = StreamFormat.YUV_420_888,
-        camera = camera2,
-        outputType = OutputStream.OutputType.SURFACE_TEXTURE,
-        mirrorMode = OutputStream.MirrorMode.MIRROR_MODE_AUTO,
-        timestampBase = OutputStream.TimestampBase.TIMESTAMP_BASE_DEFAULT,
-        dynamicRangeProfile = OutputStream.DynamicRangeProfile.STANDARD,
-        streamUseCase = OutputStream.StreamUseCase.VIDEO_RECORD
-    )
+    val streamConfig7 =
+        CameraStream.Config.create(
+            size = Size(200, 200),
+            format = StreamFormat.YUV_420_888,
+            camera = camera2,
+            outputType = OutputStream.OutputType.SURFACE_TEXTURE,
+            mirrorMode = OutputStream.MirrorMode.MIRROR_MODE_AUTO,
+            timestampBase = OutputStream.TimestampBase.TIMESTAMP_BASE_DEFAULT,
+            dynamicRangeProfile = OutputStream.DynamicRangeProfile.STANDARD,
+            streamUseCase = OutputStream.StreamUseCase.VIDEO_RECORD)
 
-    val sharedOutputConfig = OutputStream.Config.create(
-        size = Size(200, 200),
-        format = StreamFormat.YUV_420_888,
-        camera = camera1
-    )
+    val sharedOutputConfig =
+        OutputStream.Config.create(
+            size = Size(200, 200), format = StreamFormat.YUV_420_888, camera = camera1)
     val sharedStreamConfig1 = CameraStream.Config.create(sharedOutputConfig)
     val sharedStreamConfig2 = CameraStream.Config.create(sharedOutputConfig)
 
-    val graphConfig = CameraGraph.Config(
-        camera = camera1,
-        streams = listOf(
-            streamConfig1,
-            streamConfig2,
-            streamConfig3,
-            streamConfig4,
-            streamConfig5,
-            streamConfig6,
-            streamConfig7,
-            sharedStreamConfig1,
-            sharedStreamConfig2
-        ),
-        streamSharingGroups = listOf(listOf(streamConfig1, streamConfig2)),
-        defaultParameters = mapOf(
-            CaptureRequest.JPEG_THUMBNAIL_QUALITY to 24
-        ),
-        requiredParameters = mapOf(
-            CaptureRequest.JPEG_THUMBNAIL_QUALITY to 42
-        )
-    )
-}
\ No newline at end of file
+    val graphConfig =
+        CameraGraph.Config(
+            camera = camera1,
+            streams =
+                listOf(
+                    streamConfig1,
+                    streamConfig2,
+                    streamConfig3,
+                    streamConfig4,
+                    streamConfig5,
+                    streamConfig6,
+                    streamConfig7,
+                    sharedStreamConfig1,
+                    sharedStreamConfig2),
+            streamSharingGroups = listOf(listOf(streamConfig1, streamConfig2)),
+            defaultParameters = mapOf(CaptureRequest.JPEG_THUMBNAIL_QUALITY to 24),
+            requiredParameters = mapOf(CaptureRequest.JPEG_THUMBNAIL_QUALITY to 42))
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
index d6436c0..ef343be 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
@@ -32,9 +32,7 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.update
 
-/**
- * Fake implementation of a [GraphProcessor] for tests.
- */
+/** Fake implementation of a [GraphProcessor] for tests. */
 internal class FakeGraphProcessor(
     val graphState3A: GraphState3A = GraphState3A(),
     val defaultParameters: Map<*, Any?> = emptyMap<Any, Any?>(),
@@ -85,13 +83,13 @@
 
         return when {
             currProcessor == null || currRepeatingRequest == null -> false
-            else -> currProcessor.submit(
-                isRepeating = false,
-                requests = listOf(currRepeatingRequest),
-                defaultParameters = defaultParameters,
-                requiredParameters = requiredParameters,
-                listeners = defaultListeners
-            )
+            else ->
+                currProcessor.submit(
+                    isRepeating = false,
+                    requests = listOf(currRepeatingRequest),
+                    defaultParameters = defaultParameters,
+                    requiredParameters = requiredParameters,
+                    listeners = defaultListeners)
         }
     }
 
@@ -165,7 +163,6 @@
             requests = listOf(currRepeatingRequest),
             defaultParameters = defaultParameters,
             requiredParameters = requiredParameters,
-            listeners = defaultListeners
-        )
+            listeners = defaultListeners)
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeOutputConfigurationWrapper.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeOutputConfigurationWrapper.kt
index 60fa9c1..400b7c5 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeOutputConfigurationWrapper.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeOutputConfigurationWrapper.kt
@@ -21,9 +21,7 @@
 import androidx.camera.camera2.pipe.compat.OutputConfigurationWrapper
 import kotlin.reflect.KClass
 
-/**
- * Fake [OutputConfigurationWrapper] for use in tests.
- */
+/** Fake [OutputConfigurationWrapper] for use in tests. */
 class FakeOutputConfigurationWrapper(
     outputSurface: Surface? = null,
     override val physicalCameraId: CameraId? = null,
@@ -54,4 +52,4 @@
     }
 
     override fun <T : Any> unwrapAs(type: KClass<T>): T? = null
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
index d1439db..86b2ba2 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
@@ -32,10 +32,9 @@
  * This test runner disables instrumentation for the [androidx.camera.camera2.pipe] and
  * [androidx.camera.camera2.pipe.testing] packages.
  *
- * Robolectric tries to instrument Kotlin classes, and it throws errors when it encounters
- * companion objects, constructors with default values for parameters, and data classes with
- * inline classes. We don't need shadowing of our classes because we want to use the actual
- * objects in our tests.
+ * Robolectric tries to instrument Kotlin classes, and it throws errors when it encounters companion
+ * objects, constructors with default values for parameters, and data classes with inline classes.
+ * We don't need shadowing of our classes because we want to use the actual objects in our tests.
  */
 public class RobolectricCameraPipeTestRunner(testClass: Class<*>) :
     RobolectricTestRunner(testClass) {
@@ -47,36 +46,21 @@
     }
 }
 
-@JvmInline
-public value class TestValue(public val value: String)
+@JvmInline public value class TestValue(public val value: String)
 
-public data class TestData(
-    val value1: TestValue,
-    val value2: String
-)
+public data class TestData(val value1: TestValue, val value2: String)
 
 @RunWith(JUnit4::class)
 public class DataWithInlineClassJUnitTest {
     @Test
     public fun inlineClassesAreEqualInJUnit() {
-        assertThat(TestValue("42")).isEqualTo(
-            TestValue("42")
-        )
+        assertThat(TestValue("42")).isEqualTo(TestValue("42"))
     }
 
     @Test
     public fun dataWithInlineClassesAreEqualInJUnit() {
-        assertThat(
-            TestData(
-                value1 = TestValue("Test value #1"),
-                value2 = "Test value #2"
-            )
-        ).isEqualTo(
-            TestData(
-                value1 = TestValue("Test value #1"),
-                value2 = "Test value #2"
-            )
-        )
+        assertThat(TestData(value1 = TestValue("Test value #1"), value2 = "Test value #2"))
+            .isEqualTo(TestData(value1 = TestValue("Test value #1"), value2 = "Test value #2"))
     }
 }
 
@@ -85,23 +69,12 @@
 public class DataWithInlineClassRobolectricTest {
     @Test
     public fun inlineClassesAreEqualInRobolectric() {
-        assertThat(TestValue("42")).isEqualTo(
-            TestValue("42")
-        )
+        assertThat(TestValue("42")).isEqualTo(TestValue("42"))
     }
 
     @Test
     public fun dataWithInlineClassesAreEqualInRobolectric() {
-        assertThat(
-            TestData(
-                value1 = TestValue("Test value #1"),
-                value2 = "Test value #2"
-            )
-        ).isEqualTo(
-            TestData(
-                value1 = TestValue("Test value #1"),
-                value2 = "Test value #2"
-            )
-        )
+        assertThat(TestData(value1 = TestValue("Test value #1"), value2 = "Test value #2"))
+            .isEqualTo(TestData(value1 = TestValue("Test value #1"), value2 = "Test value #2"))
     }
 }
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt
index d121323..e680af6 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt
@@ -44,9 +44,7 @@
 import org.robolectric.shadows.ShadowCameraCharacteristics
 import org.robolectric.shadows.ShadowCameraManager
 
-/**
- * Utility class for creating, configuring, and interacting with Robolectric's [CameraManager].
- */
+/** Utility class for creating, configuring, and interacting with Robolectric's [CameraManager]. */
 public object RobolectricCameras {
     private val cameraIds = atomic(0)
 
@@ -69,12 +67,8 @@
      * CameraDevice objects to be created for tests.
      */
     @Suppress("MissingPermission")
-    fun create(
-        metadata: Map<CameraCharacteristics.Key<*>, Any> = emptyMap()
-    ): CameraId {
-        val shadowCameraManager = Shadow.extract<Any>(
-            cameraManager
-        ) as ShadowCameraManager
+    fun create(metadata: Map<CameraCharacteristics.Key<*>, Any> = emptyMap()): CameraId {
+        val shadowCameraManager = Shadow.extract<Any>(cameraManager) as ShadowCameraManager
 
         val characteristics = ShadowCameraCharacteristics.newCameraCharacteristics()
         val shadowCharacteristics = Shadow.extract<ShadowCameraCharacteristics>(characteristics)
@@ -105,41 +99,29 @@
     fun open(cameraId: CameraId): FakeCamera {
         check(initializedCameraIds.contains(cameraId))
         val characteristics = cameraManager.getCameraCharacteristics(cameraId.value)
-        val metadata = Camera2CameraMetadata(
-            cameraId,
-            false,
-            characteristics,
-            FakeCameraMetadataProvider(),
-            emptyMap(),
-            emptySet()
-        )
+        val metadata =
+            Camera2CameraMetadata(
+                cameraId,
+                false,
+                characteristics,
+                FakeCameraMetadataProvider(),
+                emptyMap(),
+                emptySet())
 
-        @Suppress("SyntheticAccessor")
-        val callback = CameraStateCallback(cameraId)
-        cameraManager.openCamera(
-            cameraId.value,
-            callback,
-            Handler()
-        )
+        @Suppress("SyntheticAccessor") val callback = CameraStateCallback(cameraId)
+        cameraManager.openCamera(cameraId.value, callback, Handler())
 
         // Wait until the camera is "opened" by robolectric.
         shadowOf(Looper.myLooper()).idle()
         val cameraDevice = callback.camera!!
 
         @Suppress("SyntheticAccessor")
-        return FakeCamera(
-            cameraId,
-            characteristics,
-            metadata,
-            cameraDevice
-        )
+        return FakeCamera(cameraId, characteristics, metadata, cameraDevice)
     }
 
     /** Remove all fake camera instances from Robolectric */
     fun clear() {
-        val shadowCameraManager = Shadow.extract<Any>(
-            cameraManager
-        ) as ShadowCameraManager
+        val shadowCameraManager = Shadow.extract<Any>(cameraManager) as ShadowCameraManager
         for (cameraId in initializedCameraIds) {
             try {
                 shadowCameraManager.removeCamera(cameraId.value)
@@ -150,9 +132,7 @@
         initializedCameraIds.clear()
     }
 
-    /**
-     * The [FakeCamera] instance wraps up several useful objects for use in tests.
-     */
+    /** The [FakeCamera] instance wraps up several useful objects for use in tests. */
     data class FakeCamera(
         val cameraId: CameraId,
         val characteristics: CameraCharacteristics,
@@ -170,14 +150,10 @@
 
         override fun onDisconnected(camera: CameraDevice) {
             throw UnsupportedOperationException(
-                "onDisconnected is not expected for Robolectric Camera"
-            )
+                "onDisconnected is not expected for Robolectric Camera")
         }
 
-        override fun onError(
-            camera: CameraDevice,
-            error: Int
-        ) {
+        override fun onError(camera: CameraDevice, error: Int) {
             throw UnsupportedOperationException("onError is not expected for Robolectric Camera")
         }
     }
@@ -191,9 +167,9 @@
 
     @Test
     fun fakeCamerasCanBeOpened() {
-        val fakeCameraId = RobolectricCameras.create(
-            mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
-        )
+        val fakeCameraId =
+            RobolectricCameras.create(
+                mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK))
         val fakeCamera = RobolectricCameras.open(fakeCameraId)
 
         Truth.assertThat(fakeCamera).isNotNull()
@@ -210,4 +186,4 @@
         mainLooper.idle()
         RobolectricCameras.clear()
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/UpdateCounting3AStateListener.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/UpdateCounting3AStateListener.kt
index 56b6a37..06ecbf1 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/UpdateCounting3AStateListener.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/UpdateCounting3AStateListener.kt
@@ -24,9 +24,8 @@
  * Wrapper on Result3AStateListenerImpl to keep track of the number of times the update method is
  * called.
  */
-internal class UpdateCounting3AStateListener(
-    private val listener: Result3AStateListener
-) : Result3AStateListener {
+internal class UpdateCounting3AStateListener(private val listener: Result3AStateListener) :
+    Result3AStateListener {
     var updateCount = 0
     override fun onRequestSequenceCreated(requestNumber: RequestNumber) {
         listener.onRequestSequenceCreated(requestNumber)
@@ -36,4 +35,4 @@
         updateCount++
         return listener.update(requestNumber, frameMetadata)
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index 1056392..b2a19f8 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -63,6 +63,7 @@
 import androidx.camera.core.impl.ImmediateSurface;
 import androidx.camera.core.impl.Observable;
 import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.testing.CameraUtil;
 import androidx.camera.testing.HandlerUtil;
@@ -664,7 +665,8 @@
                         CameraSelector.LENS_FACING_BACK).build();
         TestUseCase testUseCase = new TestUseCase(template, config,
                 selector, mMockOnImageAvailableListener, mMockRepeatingCaptureCallback);
-        testUseCase.updateSuggestedResolution(new Size(640, 480));
+
+        testUseCase.updateSuggestedStreamSpec(StreamSpec.builder(new Size(640, 480)).build());
         mFakeUseCases.add(testUseCase);
         return testUseCase;
     }
@@ -976,7 +978,7 @@
     }
 
     private void changeUseCaseSurface(UseCase useCase) {
-        useCase.updateSuggestedResolution(new Size(640, 480));
+        useCase.updateSuggestedStreamSpec(StreamSpec.builder(new Size(640, 480)).build());
     }
 
     private void waitForCameraClose(Camera2CameraImpl camera2CameraImpl)
@@ -1035,7 +1037,7 @@
             bindToCamera(new FakeCamera(mCameraId, null,
                             new FakeCameraInfoInternal(mCameraId, 0, lensFacing)),
                     null, null);
-            updateSuggestedResolution(new Size(640, 480));
+            updateSuggestedStreamSpec(StreamSpec.builder(new Size(640, 480)).build());
         }
 
         public void close() {
@@ -1054,8 +1056,8 @@
 
         @Override
         @NonNull
-        protected Size onSuggestedResolutionUpdated(
-                @NonNull Size suggestedResolution) {
+        protected StreamSpec onSuggestedStreamSpecUpdated(
+                @NonNull StreamSpec suggestedStreamSpec) {
             SessionConfig.Builder builder = SessionConfig.Builder.createFrom(mConfig);
 
             builder.setTemplateType(mTemplate);
@@ -1064,6 +1066,7 @@
             if (mDeferrableSurface != null) {
                 mDeferrableSurface.close();
             }
+            Size suggestedResolution = suggestedStreamSpec.getResolution();
             ImageReader imageReader =
                     ImageReader.newInstance(
                             suggestedResolution.getWidth(),
@@ -1080,7 +1083,7 @@
             builder.addSurface(mDeferrableSurface);
 
             updateSessionConfig(builder.build());
-            return suggestedResolution;
+            return suggestedStreamSpec;
         }
     }
 }
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
index b895158..75e0fe5 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
@@ -51,7 +51,6 @@
 import android.media.Image;
 import android.media.ImageReader;
 import android.media.ImageReader.OnImageAvailableListener;
-import android.media.MediaCodec;
 import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -70,9 +69,6 @@
 import androidx.camera.camera2.internal.compat.quirk.ConfigureSurfaceToSecondarySessionFailQuirk;
 import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
 import androidx.camera.camera2.internal.compat.quirk.PreviewOrientationIncorrectQuirk;
-import androidx.camera.core.ImageAnalysis;
-import androidx.camera.core.ImageCapture;
-import androidx.camera.core.Preview;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraCaptureCallbacks;
 import androidx.camera.core.impl.CameraCaptureResult;
@@ -86,9 +82,7 @@
 import androidx.camera.core.impl.utils.futures.FutureCallback;
 import androidx.camera.core.impl.utils.futures.Futures;
 import androidx.camera.testing.CameraUtil;
-import androidx.camera.testing.fakes.FakeUseCase;
 import androidx.concurrent.futures.CallbackToFutureAdapter;
-import androidx.concurrent.futures.ResolvableFuture;
 import androidx.core.os.HandlerCompat;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -115,10 +109,8 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.Map;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
@@ -159,16 +151,6 @@
     private final List<CaptureSession> mCaptureSessions = new ArrayList<>();
     private final List<DeferrableSurface> mDeferrableSurfaces = new ArrayList<>();
 
-    DeferrableSurface mMockSurface = new DeferrableSurface() {
-        private final ListenableFuture<Surface> mSurfaceFuture = ResolvableFuture.create();
-        @NonNull
-        @Override
-        protected ListenableFuture<Surface> provideSurface() {
-            // Return a never complete future.
-            return mSurfaceFuture;
-        }
-    };
-
     @Rule
     public TestRule getUseCameraRule() {
         if (SDK_INT >= 19) {
@@ -243,7 +225,6 @@
 
         mTestParameters0.tearDown();
         mTestParameters1.tearDown();
-        mDeferrableSurfaces.add(mMockSurface);
         for (DeferrableSurface deferrableSurface : mDeferrableSurfaces) {
             deferrableSurface.close();
         }
@@ -339,121 +320,6 @@
                 == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW);
     }
 
-    @SdkSuppress(maxSdkVersion = 32, minSdkVersion = 21)
-    @Test
-    public void getStreamUseCaseFromUseCaseNotSupported() {
-        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
-        mMockSurface.setContainerClass(Preview.class);
-        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
-                new ArrayList<>(), streamUseCaseMap);
-        assertTrue(streamUseCaseMap.isEmpty());
-    }
-
-    @SdkSuppress(minSdkVersion = 33)
-    @Test
-    public void getStreamUseCaseFromUseCaseEmptyUseCase() {
-        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
-        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
-                new ArrayList<>(), streamUseCaseMap);
-        assertTrue(streamUseCaseMap.isEmpty());
-    }
-
-    @SdkSuppress(minSdkVersion = 33)
-    @Test
-    public void getStreamUseCaseFromUseCaseNoPreview() {
-        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
-        mMockSurface.setContainerClass(FakeUseCase.class);
-        SessionConfig sessionConfig =
-                new SessionConfig.Builder()
-                        .addSurface(mMockSurface).build();
-        ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
-        sessionConfigs.add(sessionConfig);
-        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
-                sessionConfigs, streamUseCaseMap);
-        assertTrue(streamUseCaseMap.isEmpty());
-    }
-
-    @SdkSuppress(minSdkVersion = 33)
-    @Test
-    public void getStreamUseCaseFromUseCasePreview() {
-        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
-        mMockSurface.setContainerClass(Preview.class);
-        SessionConfig sessionConfig =
-                new SessionConfig.Builder()
-                        .addSurface(mMockSurface).build();
-        ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
-        sessionConfigs.add(sessionConfig);
-        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
-                sessionConfigs, streamUseCaseMap);
-        assertTrue(streamUseCaseMap.get(mMockSurface)
-                == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW);
-    }
-
-    @SdkSuppress(minSdkVersion = 33)
-    @Test
-    public void getStreamUseCaseFromUseCaseZSL() {
-        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
-        mMockSurface.setContainerClass(Preview.class);
-        SessionConfig sessionConfig =
-                new SessionConfig.Builder()
-                        .addSurface(mMockSurface)
-                        .setTemplateType(
-                                CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG).build();
-        ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
-        sessionConfigs.add(sessionConfig);
-        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
-                sessionConfigs, streamUseCaseMap);
-        assertTrue(streamUseCaseMap.isEmpty());
-    }
-
-    @SdkSuppress(minSdkVersion = 33)
-    @Test
-    public void getStreamUseCaseFromUseCaseImageAnalysis() {
-        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
-        mMockSurface.setContainerClass(ImageAnalysis.class);
-        SessionConfig sessionConfig =
-                new SessionConfig.Builder()
-                        .addSurface(mMockSurface).build();
-        ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
-        sessionConfigs.add(sessionConfig);
-        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
-                sessionConfigs, streamUseCaseMap);
-        assertTrue(streamUseCaseMap.get(mMockSurface)
-                == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW);
-    }
-
-    @SdkSuppress(minSdkVersion = 33)
-    @Test
-    public void getStreamUseCaseFromUseCaseConfigsImageCapture() {
-        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
-        mMockSurface.setContainerClass(ImageCapture.class);
-        SessionConfig sessionConfig =
-                new SessionConfig.Builder()
-                        .addSurface(mMockSurface).build();
-        ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
-        sessionConfigs.add(sessionConfig);
-        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
-                sessionConfigs, streamUseCaseMap);
-        assertTrue(streamUseCaseMap.get(mMockSurface)
-                == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_STILL_CAPTURE);
-    }
-
-    @SdkSuppress(minSdkVersion = 33)
-    @Test
-    public void getStreamUseCaseFromUseCaseConfigsVideoCapture() {
-        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
-        mMockSurface.setContainerClass(MediaCodec.class);
-        SessionConfig sessionConfig =
-                new SessionConfig.Builder()
-                        .addSurface(mMockSurface).build();
-        ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
-        sessionConfigs.add(sessionConfig);
-        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
-                sessionConfigs, streamUseCaseMap);
-        assertTrue(streamUseCaseMap.get(mMockSurface)
-                == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD);
-    }
-
     // Sharing surface of YUV format is supported since API 28
     @SdkSuppress(minSdkVersion = 28)
     @Test
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
index 51c93bb..0e6f94a 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
@@ -55,6 +55,7 @@
 import androidx.camera.core.impl.DeferrableSurface;
 import androidx.camera.core.impl.ImmediateSurface;
 import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.internal.CameraUseCaseAdapter;
 import androidx.camera.testing.CameraUtil;
@@ -167,8 +168,8 @@
 
         FakeCameraDeviceSurfaceManager fakeCameraDeviceSurfaceManager =
                 new FakeCameraDeviceSurfaceManager();
-        fakeCameraDeviceSurfaceManager.setSuggestedResolution(mCameraId, FakeUseCaseConfig.class,
-                new Size(640, 480));
+        fakeCameraDeviceSurfaceManager.setSuggestedStreamSpec(mCameraId, FakeUseCaseConfig.class,
+                StreamSpec.builder(new Size(640, 480)).build());
 
         mCameraUseCaseAdapter = new CameraUseCaseAdapter(
                 new LinkedHashSet<>(Collections.singleton(mCamera2CameraImpl)),
@@ -436,14 +437,14 @@
 
         @Override
         @NonNull
-        protected Size onSuggestedResolutionUpdated(
-                @NonNull Size suggestedResolution) {
-            createPipeline(suggestedResolution);
+        protected StreamSpec onSuggestedStreamSpecUpdated(
+                @NonNull StreamSpec suggestedStreamSpec) {
+            createPipeline(suggestedStreamSpec);
             notifyActive();
-            return suggestedResolution;
+            return suggestedStreamSpec;
         }
 
-        private void createPipeline(Size resolution) {
+        private void createPipeline(StreamSpec streamSpec) {
             SessionConfig.Builder builder = SessionConfig.Builder.createFrom(getCurrentConfig());
 
             builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
@@ -452,6 +453,7 @@
             }
 
             // Create the metering DeferrableSurface
+            Size resolution = streamSpec.getResolution();
             SurfaceTexture surfaceTexture = new SurfaceTexture(0);
             surfaceTexture.setDefaultBufferSize(resolution.getWidth(), resolution.getHeight());
             Surface surface = new Surface(surfaceTexture);
@@ -477,7 +479,7 @@
 
             builder.addErrorListener((sessionConfig, error) -> {
                 // Create new pipeline and it will close the old one.
-                createPipeline(resolution);
+                createPipeline(streamSpec);
             });
             updateSessionConfig(builder.build());
         }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
index 5c049d2..d14efa6 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
@@ -192,6 +192,9 @@
     @NonNull
     private final DisplayInfoManager mDisplayInfoManager;
 
+    @NonNull
+    private final CameraCharacteristicsCompat mCameraCharacteristicsCompat;
+
     /**
      * Constructor for a camera.
      *
@@ -224,9 +227,9 @@
         mCaptureSession = newCaptureSession();
 
         try {
-            CameraCharacteristicsCompat cameraCharacteristicsCompat =
+            mCameraCharacteristicsCompat =
                     mCameraManager.getCameraCharacteristicsCompat(cameraId);
-            mCameraControlInternal = new Camera2CameraControlImpl(cameraCharacteristicsCompat,
+            mCameraControlInternal = new Camera2CameraControlImpl(mCameraCharacteristicsCompat,
                     mScheduledExecutorService, mExecutor, new ControlUpdateListenerInternal(),
                     cameraInfoImpl.getCameraQuirks());
             mCameraInfoInternal = cameraInfoImpl;
@@ -692,7 +695,7 @@
          * use count to recover the additional increment here.
          */
         mCameraControlInternal.incrementUseCount();
-        notifyStateAttachedToUseCases(new ArrayList<>(useCases));
+        notifyStateAttachedAndCameraControlReady(new ArrayList<>(useCases));
         List<UseCaseInfo> useCaseInfos = new ArrayList<>(toUseCaseInfos(useCases));
         try {
             mExecutor.execute(() -> {
@@ -798,7 +801,7 @@
         return mCameraConfig;
     }
 
-    private void notifyStateAttachedToUseCases(List<UseCase> useCases) {
+    private void notifyStateAttachedAndCameraControlReady(List<UseCase> useCases) {
         for (UseCase useCase : useCases) {
             String useCaseId = getUseCaseId(useCase);
             if (mNotifyStateAttachedSet.contains(useCaseId)) {
@@ -807,6 +810,7 @@
 
             mNotifyStateAttachedSet.add(useCaseId);
             useCase.onStateAttached();
+            useCase.onCameraControlReady();
         }
     }
 
@@ -1130,7 +1134,7 @@
         Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
         StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
                 mUseCaseAttachState.getAttachedSessionConfigs(),
-                streamUseCaseMap);
+                streamUseCaseMap, mCameraCharacteristicsCompat);
 
         mCaptureSession.setStreamUseCaseMap(streamUseCaseMap);
 
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index 9865423..df26798 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -267,7 +267,7 @@
 
     @Override
     public boolean hasFlashUnit() {
-        return FlashAvailabilityChecker.isFlashAvailable(mCameraCharacteristicsCompat);
+        return FlashAvailabilityChecker.isFlashAvailable(mCameraCharacteristicsCompat::get);
     }
 
     @NonNull
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
index 6681bc2..7da2f82 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
@@ -156,7 +156,7 @@
         mExecutor = executor;
         mCameraQuirk = cameraQuirks;
         mUseTorchAsFlash = new UseTorchAsFlash(cameraQuirks);
-        mHasFlashUnit = FlashAvailabilityChecker.isFlashAvailable(cameraCharacteristics);
+        mHasFlashUnit = FlashAvailabilityChecker.isFlashAvailable(cameraCharacteristics::get);
     }
 
     @ExecutedBy("mExecutor")
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
index 98e8758..c2e353ff 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
@@ -29,6 +29,7 @@
 import androidx.camera.core.CameraUnavailableException;
 import androidx.camera.core.impl.AttachedSurfaceInfo;
 import androidx.camera.core.impl.CameraDeviceSurfaceManager;
+import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.SurfaceConfig;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.core.util.Preconditions;
@@ -164,14 +165,14 @@
     }
 
     /**
-     * Retrieves a map of suggested resolutions for the given list of use cases.
+     * Retrieves a map of suggested stream specifications for the given list of use cases.
      *
      * @param cameraId          the camera id of the camera device used by the use cases
      * @param existingSurfaces  list of surfaces already configured and used by the camera. The
-     *                          resolutions for these surface can not change.
+     *                          stream specifications for these surface can not change.
      * @param newUseCaseConfigs list of configurations of the use cases that will be given a
-     *                          suggested resolution
-     * @return map of suggested resolutions for given use cases
+     *                          suggested stream specification
+     * @return map of suggested stream specifications for given use cases
      * @throws IllegalStateException    if not initialized
      * @throws IllegalArgumentException if {@code newUseCaseConfigs} is an empty list, if
      *                                  there isn't a supported combination of surfaces
@@ -180,7 +181,7 @@
      */
     @NonNull
     @Override
-    public Map<UseCaseConfig<?>, Size> getSuggestedResolutions(
+    public Map<UseCaseConfig<?>, StreamSpec> getSuggestedStreamSpecs(
             @NonNull String cameraId,
             @NonNull List<AttachedSurfaceInfo> existingSurfaces,
             @NonNull List<UseCaseConfig<?>> newUseCaseConfigs) {
@@ -194,7 +195,7 @@
                     + cameraId);
         }
 
-        return supportedSurfaceCombination.getSuggestedResolutions(existingSurfaces,
+        return supportedSurfaceCombination.getSuggestedStreamSpecifications(existingSurfaces,
                 newUseCaseConfigs);
     }
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java
index cb1a181..f2c6f0e 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java
@@ -18,6 +18,7 @@
 
 import static androidx.camera.camera2.impl.Camera2ImplConfig.STREAM_USE_CASE_OPTION;
 
+import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraDevice;
 import android.hardware.camera2.CameraMetadata;
 import android.media.MediaCodec;
@@ -27,6 +28,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
 import androidx.camera.core.ImageAnalysis;
 import androidx.camera.core.ImageCapture;
@@ -37,7 +39,9 @@
 
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * A class that contains utility methods for stream use case.
@@ -60,10 +64,23 @@
     @OptIn(markerClass = ExperimentalCamera2Interop.class)
     public static void populateSurfaceToStreamUseCaseMapping(
             @NonNull Collection<SessionConfig> sessionConfigs,
-            @NonNull Map<DeferrableSurface, Long> streamUseCaseMap) {
+            @NonNull Map<DeferrableSurface, Long> streamUseCaseMap,
+            @NonNull CameraCharacteristicsCompat cameraCharacteristicsCompat) {
         if (Build.VERSION.SDK_INT < 33) {
             return;
         }
+
+        if (cameraCharacteristicsCompat.get(
+                CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES) == null) {
+            return;
+        }
+
+        Set<Long> supportedStreamUseCases = new HashSet<>();
+        for (long useCase : cameraCharacteristicsCompat.get(
+                CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES)) {
+            supportedStreamUseCases.add(useCase);
+        }
+
         for (SessionConfig sessionConfig : sessionConfigs) {
             if (sessionConfig.getTemplateType()
                     == CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG
@@ -73,34 +90,49 @@
                 return;
             }
             for (DeferrableSurface surface : sessionConfig.getSurfaces()) {
-                if (sessionConfig.getImplementationOptions().containsOption(STREAM_USE_CASE_OPTION)
-                        &&
-                        sessionConfig.getImplementationOptions()
-                                .retrieveOption(STREAM_USE_CASE_OPTION) != null
-                ) {
-                    streamUseCaseMap.put(
-                            surface,
-                            sessionConfig.getImplementationOptions()
-                                    .retrieveOption(STREAM_USE_CASE_OPTION));
-
+                if (sessionConfig.getImplementationOptions().containsOption(
+                        STREAM_USE_CASE_OPTION) && putStreamUseCaseToMappingIfAvailable(
+                        streamUseCaseMap,
+                        surface,
+                        sessionConfig.getImplementationOptions().retrieveOption(
+                                STREAM_USE_CASE_OPTION),
+                        supportedStreamUseCases)) {
                     continue;
                 }
 
-                @Nullable Long flag = getUseCaseToStreamUseCaseMapping()
+                Long streamUseCase = getUseCaseToStreamUseCaseMapping()
                         .get(surface.getContainerClass());
-                if (flag != null) {
-                    streamUseCaseMap.put(surface, flag);
-                }
+                putStreamUseCaseToMappingIfAvailable(streamUseCaseMap,
+                        surface,
+                        streamUseCase,
+                        supportedStreamUseCases);
             }
         }
     }
 
+    private static boolean putStreamUseCaseToMappingIfAvailable(
+            Map<DeferrableSurface, Long> streamUseCaseMap,
+            DeferrableSurface surface,
+            @Nullable Long streamUseCase,
+            Set<Long> availableStreamUseCases) {
+        if (streamUseCase == null) {
+            return false;
+        }
+
+        if (!availableStreamUseCases.contains(streamUseCase)) {
+            return false;
+        }
+
+        streamUseCaseMap.put(surface, streamUseCase);
+        return true;
+    }
+
     /**
      * Returns the mapping between the container class of a surface and the StreamUseCase
      * associated with that class. Refer to {@link UseCase} for the potential UseCase as the
      * container class for a given surface.
      */
-    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+    @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
     private static Map<Class<?>, Long> getUseCaseToStreamUseCaseMapping() {
         if (sUseCaseToStreamUseCaseMapping == null) {
             sUseCaseToStreamUseCaseMapping = new HashMap<>();
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
index f3818b8..32e8f86 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
@@ -41,6 +41,7 @@
 import android.media.CamcorderProfile;
 import android.media.MediaRecorder;
 import android.util.Pair;
+import android.util.Range;
 import android.util.Rational;
 import android.util.Size;
 import android.view.Surface;
@@ -63,6 +64,7 @@
 import androidx.camera.core.ResolutionSelector;
 import androidx.camera.core.impl.AttachedSurfaceInfo;
 import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.SurfaceCombination;
 import androidx.camera.core.impl.SurfaceConfig;
 import androidx.camera.core.impl.SurfaceSizeDefinition;
@@ -105,7 +107,7 @@
     private boolean mIsBurstCaptureSupported = false;
     @VisibleForTesting
     SurfaceSizeDefinition mSurfaceSizeDefinition;
-    private Map<Integer, Size[]> mOutputSizesCache = new HashMap<>();
+    private final Map<Integer, Size[]> mOutputSizesCache = new HashMap<>();
     @NonNull
     private final DisplayInfoManager mDisplayInfoManager;
     private final ResolutionCorrector mResolutionCorrector = new ResolutionCorrector();
@@ -208,26 +210,80 @@
         return SurfaceConfig.transformSurfaceConfig(imageFormat, size, mSurfaceSizeDefinition);
     }
 
+    static int getMaxFramerate(CameraCharacteristicsCompat characteristics, int imageFormat,
+            Size size) {
+        int maxFramerate = 0;
+        try {
+            maxFramerate = (int) (1000000000.0
+                    / characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
+                    .getOutputMinFrameDuration(imageFormat,
+                            size));
+        } catch (Exception e) {
+            //TODO
+            //this try catch is in place for the rare that a surface config has a size
+            // incompatible for getOutputMinFrameDuration...  put into a Quirk
+        }
+        return maxFramerate;
+    }
+
     /**
-     * Finds the suggested resolutions of the newly added UseCaseConfig.
+     * @param newTargetFramerate    an incoming framerate range
+     * @param storedTargetFramerate a stored framerate range to be modified
+     * @return adjusted target frame rate
      *
-     * @param existingSurfaces  the existing surfaces.
+     * If the two ranges are both nonnull and disjoint of each other, then the range that was
+     * already stored will be used
+     */
+    private Range<Integer> getUpdatedTargetFramerate(Range<Integer> newTargetFramerate,
+            Range<Integer> storedTargetFramerate) {
+        Range<Integer> updatedTarget = storedTargetFramerate;
+
+        if (storedTargetFramerate == null) {
+            // if stored value was null before, set it to the new value
+            updatedTarget = newTargetFramerate;
+        } else if (newTargetFramerate != null) {
+            try {
+                // get intersection of existing target fps
+                updatedTarget =
+                        storedTargetFramerate
+                                .intersect(newTargetFramerate);
+            } catch (IllegalArgumentException e) {
+                // no intersection, keep the previously stored value
+                updatedTarget = storedTargetFramerate;
+            }
+        }
+        return updatedTarget;
+    }
+
+    /**
+     * @param currentMaxFps the previously stored Max FPS
+     * @param imageFormat   the image format of the incoming surface
+     * @param size          the size of the incoming surface
+     */
+    private int getUpdatedMaximumFps(int currentMaxFps, int imageFormat, Size size) {
+        return Math.min(currentMaxFps, getMaxFramerate(mCharacteristics, imageFormat, size));
+    }
+
+    /**
+     * Finds the suggested stream specifications of the newly added UseCaseConfig.
+     *
+     * @param attachedSurfaces  the existing surfaces.
      * @param newUseCaseConfigs newly added UseCaseConfig.
-     * @return the suggested resolutions, which is a mapping from UseCaseConfig to the suggested
-     * resolution.
+     * @return the suggested stream specifications, which is a mapping from UseCaseConfig to the
+     * suggested stream specification.
      * @throws IllegalArgumentException if the suggested solution for newUseCaseConfigs cannot be
      *                                  found. This may be due to no available output size or no
      *                                  available surface combination.
      */
     @NonNull
-    Map<UseCaseConfig<?>, Size> getSuggestedResolutions(
-            @NonNull List<AttachedSurfaceInfo> existingSurfaces,
+    Map<UseCaseConfig<?>, StreamSpec> getSuggestedStreamSpecifications(
+            @NonNull List<AttachedSurfaceInfo> attachedSurfaces,
             @NonNull List<UseCaseConfig<?>> newUseCaseConfigs) {
         // Refresh Preview Size based on current display configurations.
         refreshPreviewSize();
         List<SurfaceConfig> surfaceConfigs = new ArrayList<>();
-        for (AttachedSurfaceInfo scc : existingSurfaces) {
-            surfaceConfigs.add(scc.getSurfaceConfig());
+        for (AttachedSurfaceInfo attachedSurface : attachedSurfaces) {
+            surfaceConfigs.add(attachedSurface.getSurfaceConfig());
         }
 
         // Use the small size (640x480) for new use cases to check whether there is any possible
@@ -243,20 +299,31 @@
             throw new IllegalArgumentException(
                     "No supported surface combination is found for camera device - Id : "
                             + mCameraId + ".  May be attempting to bind too many use cases. "
-                            + "Existing surfaces: " + existingSurfaces + " New configs: "
+                            + "Existing surfaces: " + attachedSurfaces + " New configs: "
                             + newUseCaseConfigs);
         }
 
+        Range<Integer> targetFramerateForConfig = null;
+        int existingSurfaceFrameRateCeiling = Integer.MAX_VALUE;
+
+        for (AttachedSurfaceInfo attachedSurfaceInfo : attachedSurfaces) {
+            // init target fps range for new configs from existing surfaces
+            targetFramerateForConfig = getUpdatedTargetFramerate(
+                    attachedSurfaceInfo.getTargetFrameRate(),
+                    targetFramerateForConfig);
+            //get the fps ceiling for existing surfaces
+            existingSurfaceFrameRateCeiling = getUpdatedMaximumFps(
+                    existingSurfaceFrameRateCeiling,
+                    attachedSurfaceInfo.getImageFormat(), attachedSurfaceInfo.getSize());
+        }
+
         // Get the index order list by the use case priority for finding stream configuration
-        List<Integer> useCasesPriorityOrder =
-                getUseCasesPriorityOrder(
-                        newUseCaseConfigs);
+        List<Integer> useCasesPriorityOrder = getUseCasesPriorityOrder(newUseCaseConfigs);
         List<List<Size>> supportedOutputSizesList = new ArrayList<>();
 
         // Collect supported output sizes for all use cases
         for (Integer index : useCasesPriorityOrder) {
-            List<Size> supportedOutputSizes =
-                    getSupportedOutputSizes(newUseCaseConfigs.get(index));
+            List<Size> supportedOutputSizes = getSupportedOutputSizes(newUseCaseConfigs.get(index));
             supportedOutputSizesList.add(supportedOutputSizes);
         }
 
@@ -265,13 +332,27 @@
                 getAllPossibleSizeArrangements(
                         supportedOutputSizesList);
 
-        Map<UseCaseConfig<?>, Size> suggestedResolutionsMap = null;
+        // update target fps for new configs using new use cases' priority order
+        for (Integer index : useCasesPriorityOrder) {
+            targetFramerateForConfig =
+                    getUpdatedTargetFramerate(
+                            newUseCaseConfigs.get(index).getTargetFramerate(null),
+                            targetFramerateForConfig);
+        }
+
+        Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecMap;
+        List<Size> savedSizes = null;
+        int savedConfigMaxFps = Integer.MAX_VALUE;
+
         // Transform use cases to SurfaceConfig list and find the first (best) workable combination
         for (List<Size> possibleSizeList : allPossibleSizeArrangements) {
             // Attach SurfaceConfig of original use cases since it will impact the new use cases
             List<SurfaceConfig> surfaceConfigList = new ArrayList<>();
-            for (AttachedSurfaceInfo sc : existingSurfaces) {
-                surfaceConfigList.add(sc.getSurfaceConfig());
+            int currentConfigFramerateCeiling = existingSurfaceFrameRateCeiling;
+            boolean isConfigFrameRateAcceptable = true;
+
+            for (AttachedSurfaceInfo attachedSurfaceInfo : attachedSurfaces) {
+                surfaceConfigList.add(attachedSurfaceInfo.getSurfaceConfig());
             }
 
             // Attach SurfaceConfig of new use cases
@@ -279,33 +360,69 @@
                 Size size = possibleSizeList.get(i);
                 UseCaseConfig<?> newUseCase =
                         newUseCaseConfigs.get(useCasesPriorityOrder.get(i));
+                // add new use case/size config to list of surfaces
                 surfaceConfigList.add(
                         SurfaceConfig.transformSurfaceConfig(newUseCase.getInputFormat(), size,
                                 mSurfaceSizeDefinition));
+
+                // get the maximum fps of the new surface and update the maximum fps of the
+                // proposed configuration
+                currentConfigFramerateCeiling = getUpdatedMaximumFps(
+                        currentConfigFramerateCeiling,
+                        newUseCase.getInputFormat(),
+                        size);
+            }
+            if (targetFramerateForConfig != null) {
+                if (existingSurfaceFrameRateCeiling > currentConfigFramerateCeiling
+                        && currentConfigFramerateCeiling < targetFramerateForConfig.getLower()) {
+                    // if the max fps before adding new use cases supports our target fps range
+                    // BUT the max fps of the new configuration is below
+                    // our target fps range, we'll want to check the next configuration until we
+                    // get one that supports our target FPS
+                    isConfigFrameRateAcceptable = false;
+                }
             }
 
-            // Check whether the SurfaceConfig combination can be supported
+            // only change the saved config if you get another that has a better max fps
             if (checkSupported(surfaceConfigList)) {
-                suggestedResolutionsMap = new HashMap<>();
-                for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
-                    suggestedResolutionsMap.put(
-                            useCaseConfig,
-                            possibleSizeList.get(
-                                    useCasesPriorityOrder.indexOf(
-                                            newUseCaseConfigs.indexOf(useCaseConfig))));
+                // if the config is supported by the device but doesn't meet the target framerate,
+                // save the config
+                if (savedConfigMaxFps == Integer.MAX_VALUE) {
+                    savedConfigMaxFps = currentConfigFramerateCeiling;
+                    savedSizes = possibleSizeList;
+                } else if (savedConfigMaxFps < currentConfigFramerateCeiling) {
+                    // only change the saved config if the max fps is better
+                    savedConfigMaxFps = currentConfigFramerateCeiling;
+                    savedSizes = possibleSizeList;
                 }
-                break;
+
+                // if we have a configuration where the max fps is acceptable for our target, break
+                if (isConfigFrameRateAcceptable) {
+                    savedConfigMaxFps = currentConfigFramerateCeiling;
+                    savedSizes = possibleSizeList;
+                    break;
+                }
             }
         }
-        if (suggestedResolutionsMap == null) {
+
+        // Map the saved supported SurfaceConfig combination
+        if (savedSizes != null) {
+            suggestedStreamSpecMap = new HashMap<>();
+            for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
+                suggestedStreamSpecMap.put(
+                        useCaseConfig,
+                        StreamSpec.builder(savedSizes.get(useCasesPriorityOrder.indexOf(
+                                newUseCaseConfigs.indexOf(useCaseConfig)))).build());
+            }
+        } else {
             throw new IllegalArgumentException(
                     "No supported surface combination is found for camera device - Id : "
                             + mCameraId + " and Hardware level: " + mHardwareLevel
                             + ". May be the specified resolution is too large and not supported."
-                            + " Existing surfaces: " + existingSurfaces
+                            + " Existing surfaces: " + attachedSurfaces
                             + " New configs: " + newUseCaseConfigs);
         }
-        return suggestedResolutionsMap;
+        return suggestedStreamSpecMap;
     }
 
     /**
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
index eb0606a..a14c48e 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
@@ -79,7 +79,7 @@
         mCamera2CameraControlImpl = camera2CameraControlImpl;
         mExecutor = executor;
 
-        mHasFlashUnit = FlashAvailabilityChecker.isFlashAvailable(cameraCharacteristics);
+        mHasFlashUnit = FlashAvailabilityChecker.isFlashAvailable(cameraCharacteristics::get);
         mTorchState = new MutableLiveData<>(DEFAULT_TORCH_STATE);
         Camera2CameraControlImpl.CaptureResultListener captureResultListener = captureResult -> {
             if (mEnableTorchCompleter != null) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/FlashAvailabilityBufferUnderflowQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/FlashAvailabilityBufferUnderflowQuirk.java
index a0be578..d3ef576 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/FlashAvailabilityBufferUnderflowQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/FlashAvailabilityBufferUnderflowQuirk.java
@@ -40,14 +40,15 @@
  *                  {@link BufferUnderflowException} is thrown. This is an undocumented exception
  *                  on the {@link CameraCharacteristics#get(CameraCharacteristics.Key)} method,
  *                  so this violates the API contract.
- *     Device(s): LEMFO LEMP (a.k.a. Spreadtrum LEMP)
+ *     Device(s): Spreadtrum devices including LEMFO LEMP and DM20C
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class FlashAvailabilityBufferUnderflowQuirk implements Quirk {
     private static final Set<Pair<String, String>> KNOWN_AFFECTED_MODELS = new HashSet<>(
             Arrays.asList(
                     // Devices enumerated as Pair(Build.MANUFACTURER, Build.MODEL)
-                    new Pair<>("sprd", "lemp")
+                    new Pair<>("sprd", "lemp"),
+                    new Pair<>("sprd", "DM20C")
             ));
 
     static boolean load() {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/CameraCharacteristicsProvider.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/CameraCharacteristicsProvider.java
new file mode 100644
index 0000000..cfb0255
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/CameraCharacteristicsProvider.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal.compat.workaround;
+
+import android.hardware.camera2.CameraCharacteristics;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+/**
+ * An interface for providing camera characteristics from
+ * {@link android.hardware.camera2.CameraCharacteristics.Key}.
+ *
+ * <p>This layer of abstraction can be used to customize the behavior of providing
+ * characteristics for testing.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public interface CameraCharacteristicsProvider {
+    /** Retrieves the camera characteristic value from the provided key. */
+    @Nullable
+    <T> T get(@NonNull CameraCharacteristics.Key<T> key);
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/FlashAvailabilityChecker.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/FlashAvailabilityChecker.java
index 4060d49..49fe78f 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/FlashAvailabilityChecker.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/FlashAvailabilityChecker.java
@@ -17,10 +17,10 @@
 package androidx.camera.camera2.internal.compat.workaround;
 
 import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
-import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
 import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
 import androidx.camera.camera2.internal.compat.quirk.FlashAvailabilityBufferUnderflowQuirk;
 import androidx.camera.core.Logger;
@@ -40,37 +40,56 @@
     /**
      * Checks whether the camera characteristics advertise that flash is available safely.
      *
-     * @param characteristics the characteristics to check for
-     *                        {@link CameraCharacteristics#FLASH_INFO_AVAILABLE}.
+     * @param provider the characteristics provider to check for
+     *                 {@link CameraCharacteristics#FLASH_INFO_AVAILABLE}.
      * @return the value of {@link CameraCharacteristics#FLASH_INFO_AVAILABLE} if it is contained
      * in the characteristics, or {@code false} if it is not or a
      * {@link java.nio.BufferUnderflowException} is thrown while checking.
      */
-    public static boolean isFlashAvailable(@NonNull CameraCharacteristicsCompat characteristics) {
-        if (DeviceQuirks.get(FlashAvailabilityBufferUnderflowQuirk.class) != null) {
-            Logger.d(TAG, "Device has quirk "
-                    + FlashAvailabilityBufferUnderflowQuirk.class.getSimpleName()
-                    + ". Checking for flash availability safely...");
-            return checkFlashAvailabilityWithPossibleBufferUnderflow(characteristics);
-        } else {
-            return checkFlashAvailabilityNormally(characteristics);
-        }
-
+    public static boolean isFlashAvailable(@NonNull CameraCharacteristicsProvider provider) {
+        return isFlashAvailable(/*allowRethrowOnError=*/false, provider);
     }
 
-    private static boolean checkFlashAvailabilityWithPossibleBufferUnderflow(
-            @NonNull CameraCharacteristicsCompat characteristics) {
+    /**
+     * Checks whether the camera characteristics advertise that flash is available safely.
+     *
+     * @param allowRethrowOnError whether exceptions can be rethrown on devices that are not
+     *                            known to be problematic. If {@code false}, these devices will be
+     *                            logged as an error instead.
+     * @param provider            the characteristics provider to check for
+     *                            {@link CameraCharacteristics#FLASH_INFO_AVAILABLE}.
+     * @return the value of {@link CameraCharacteristics#FLASH_INFO_AVAILABLE} if it is contained
+     * in the characteristics, or {@code false} if it is not or a
+     * {@link java.nio.BufferUnderflowException} is thrown while checking.
+     */
+    public static boolean isFlashAvailable(boolean allowRethrowOnError,
+            @NonNull CameraCharacteristicsProvider provider) {
+        Boolean flashAvailable;
         try {
-            return checkFlashAvailabilityNormally(characteristics);
+            flashAvailable = provider.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
         } catch (BufferUnderflowException e) {
-            return false;
-        }
-    }
+            if (DeviceQuirks.get(FlashAvailabilityBufferUnderflowQuirk.class) != null) {
+                Logger.d(TAG, String.format("Device is known to throw an exception while "
+                        + "checking flash availability. Flash is not available. "
+                        + "[Manufacturer: %s, Model: %s, API Level: %d].",
+                        Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT));
+            } else {
+                Logger.e(TAG, String.format("Exception thrown while checking for flash "
+                                + "availability on device not known to throw exceptions during "
+                                + "this check. Please file an issue at "
+                                + "https://issuetracker.google.com/issues/new?component=618491"
+                                + "&template=1257717 with this error message "
+                                + "[Manufacturer: %s, Model: %s, API Level: %d].\n"
+                                + "Flash is not available.",
+                        Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT), e);
+            }
 
-    private static boolean checkFlashAvailabilityNormally(
-            @NonNull CameraCharacteristicsCompat characteristics) {
-        Boolean flashAvailable =
-                characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
+            if (allowRethrowOnError) {
+                throw e;
+            } else {
+                flashAvailable = false;
+            }
+        }
         if (flashAvailable == null) {
             Logger.w(TAG, "Characteristics did not contain key FLASH_INFO_AVAILABLE. Flash is not"
                     + " available.");
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-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java
new file mode 100644
index 0000000..f245ae2
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import static junit.framework.TestCase.assertTrue;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraMetadata;
+import android.media.MediaCodec;
+import android.os.Build;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.camera.camera2.impl.Camera2ImplConfig;
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.core.ImageAnalysis;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.Preview;
+import androidx.camera.core.impl.DeferrableSurface;
+import androidx.camera.core.impl.MutableOptionsBundle;
+import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.testing.fakes.FakeUseCase;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.test.filters.SdkSuppress;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowCameraCharacteristics;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class StreamUseCaseTest {
+
+    private CameraCharacteristics mCameraCharacteristics;
+
+    DeferrableSurface mMockSurface = new DeferrableSurface() {
+        private final ListenableFuture<Surface> mSurfaceFuture = ResolvableFuture.create();
+
+        @NonNull
+        @Override
+        protected ListenableFuture<Surface> provideSurface() {
+            // Return a never complete future.
+            return mSurfaceFuture;
+        }
+    };
+
+    @Before
+    public void setup() {
+        mCameraCharacteristics = ShadowCameraCharacteristics.newCameraCharacteristics();
+    }
+
+    @After
+    public void tearDown() {
+        mMockSurface.close();
+    }
+
+    @SdkSuppress(maxSdkVersion = 32, minSdkVersion = 21)
+    @Test
+    public void getStreamUseCaseFromOsNotSupported() {
+        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+        mMockSurface.setContainerClass(Preview.class);
+        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+                new ArrayList<>(), streamUseCaseMap, getCameraCharacteristicsCompat());
+        assertTrue(streamUseCaseMap.isEmpty());
+    }
+
+    @SdkSuppress(minSdkVersion = 33)
+    @Test
+    public void getStreamUseCaseFromUseCaseEmptyUseCase() {
+        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+                new ArrayList<>(), streamUseCaseMap, getCameraCharacteristicsCompat());
+        assertTrue(streamUseCaseMap.isEmpty());
+    }
+
+    @SdkSuppress(minSdkVersion = 33)
+    @Test
+    public void getStreamUseCaseFromUseCaseNoPreview() {
+        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+        mMockSurface.setContainerClass(FakeUseCase.class);
+        SessionConfig sessionConfig =
+                new SessionConfig.Builder()
+                        .addSurface(mMockSurface).build();
+        ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+        sessionConfigs.add(sessionConfig);
+        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+                sessionConfigs, streamUseCaseMap, getCameraCharacteristicsCompat());
+        assertTrue(streamUseCaseMap.isEmpty());
+    }
+
+    @Test
+    public void getStreamUseCaseFromUseCasePreview() {
+        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+        mMockSurface.setContainerClass(Preview.class);
+        SessionConfig sessionConfig =
+                new SessionConfig.Builder()
+                        .addSurface(mMockSurface).build();
+        ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+        sessionConfigs.add(sessionConfig);
+        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+                sessionConfigs, streamUseCaseMap, getCameraCharacteristicsCompat());
+        assertTrue(streamUseCaseMap.get(mMockSurface)
+                == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW);
+    }
+
+    @SdkSuppress(minSdkVersion = 33)
+    @Test
+    public void getStreamUseCaseFromUseCaseZSL() {
+        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+        mMockSurface.setContainerClass(Preview.class);
+        SessionConfig sessionConfig =
+                new SessionConfig.Builder()
+                        .addSurface(mMockSurface)
+                        .setTemplateType(
+                                CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG).build();
+        ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+        sessionConfigs.add(sessionConfig);
+        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+                sessionConfigs, streamUseCaseMap, getCameraCharacteristicsCompat());
+        assertTrue(streamUseCaseMap.isEmpty());
+    }
+
+    @SdkSuppress(minSdkVersion = 33)
+    @Test
+    public void getStreamUseCaseFromUseCaseImageAnalysis() {
+        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+        mMockSurface.setContainerClass(ImageAnalysis.class);
+        SessionConfig sessionConfig =
+                new SessionConfig.Builder()
+                        .addSurface(mMockSurface).build();
+        ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+        sessionConfigs.add(sessionConfig);
+        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+                sessionConfigs, streamUseCaseMap, getCameraCharacteristicsCompat());
+        assertTrue(streamUseCaseMap.get(mMockSurface)
+                == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW);
+    }
+
+    @SdkSuppress(minSdkVersion = 33)
+    @Test
+    public void getStreamUseCaseFromUseCaseConfigsImageCapture() {
+        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+        mMockSurface.setContainerClass(ImageCapture.class);
+        SessionConfig sessionConfig =
+                new SessionConfig.Builder()
+                        .addSurface(mMockSurface).build();
+        ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+        sessionConfigs.add(sessionConfig);
+        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+                sessionConfigs, streamUseCaseMap, getCameraCharacteristicsCompat());
+        assertTrue(streamUseCaseMap.get(mMockSurface)
+                == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_STILL_CAPTURE);
+    }
+
+    @SdkSuppress(minSdkVersion = 33)
+    @Test
+    public void getStreamUseCaseFromUseCaseConfigsVideoCapture() {
+        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+        mMockSurface.setContainerClass(MediaCodec.class);
+        SessionConfig sessionConfig =
+                new SessionConfig.Builder()
+                        .addSurface(mMockSurface).build();
+        ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+        sessionConfigs.add(sessionConfig);
+        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+                sessionConfigs, streamUseCaseMap, getCameraCharacteristicsCompat());
+        assertTrue(streamUseCaseMap.get(mMockSurface)
+                == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD);
+    }
+
+    @SdkSuppress(minSdkVersion = 33)
+    @Test
+    public void getStreamUseCaseWithNullAvailableUseCases() {
+        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+        mMockSurface.setContainerClass(FakeUseCase.class);
+        SessionConfig sessionConfig =
+                new SessionConfig.Builder()
+                        .addSurface(mMockSurface).build();
+        ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+        sessionConfigs.add(sessionConfig);
+        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+                sessionConfigs, streamUseCaseMap,
+                CameraCharacteristicsCompat.toCameraCharacteristicsCompat(mCameraCharacteristics));
+        assertTrue(streamUseCaseMap.isEmpty());
+    }
+
+    @SdkSuppress(minSdkVersion = 33)
+    @Test
+    public void getStreamUseCaseWithEmptyAvailableUseCases() {
+        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+        mMockSurface.setContainerClass(Preview.class);
+        SessionConfig sessionConfig =
+                new SessionConfig.Builder()
+                        .addSurface(mMockSurface).build();
+        ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+        sessionConfigs.add(sessionConfig);
+        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+                sessionConfigs,
+                streamUseCaseMap,
+                getCameraCharacteristicsCompatWithEmptyUseCases());
+        assertTrue(streamUseCaseMap.isEmpty());
+    }
+
+    @Test
+    public void getStreamUseCaseFromCamera2Interop() {
+        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+        mMockSurface.setContainerClass(Preview.class);
+        MutableOptionsBundle testStreamUseCaseConfig = MutableOptionsBundle.create();
+        testStreamUseCaseConfig.insertOption(Camera2ImplConfig.STREAM_USE_CASE_OPTION, 3L);
+        SessionConfig sessionConfig =
+                new SessionConfig.Builder()
+                        .addSurface(mMockSurface).addImplementationOptions(
+                                testStreamUseCaseConfig).build();
+        ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+        sessionConfigs.add(sessionConfig);
+        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+                sessionConfigs, streamUseCaseMap, getCameraCharacteristicsCompat());
+        assertTrue(streamUseCaseMap.get(mMockSurface) == 3L);
+    }
+
+    @Test
+    public void getUnsupportedStreamUseCaseFromCamera2Interop() {
+        Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+        mMockSurface.setContainerClass(Preview.class);
+        MutableOptionsBundle testStreamUseCaseConfig = MutableOptionsBundle.create();
+        testStreamUseCaseConfig.insertOption(Camera2ImplConfig.STREAM_USE_CASE_OPTION, -1L);
+        SessionConfig sessionConfig =
+                new SessionConfig.Builder()
+                        .addSurface(mMockSurface).addImplementationOptions(
+                                testStreamUseCaseConfig).build();
+        ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+        sessionConfigs.add(sessionConfig);
+        StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+                sessionConfigs, streamUseCaseMap, getCameraCharacteristicsCompat());
+        assertTrue(streamUseCaseMap.get(mMockSurface)
+                == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW);
+    }
+
+    private CameraCharacteristicsCompat getCameraCharacteristicsCompat() {
+        ShadowCameraCharacteristics shadowCharacteristics0 = Shadow.extract(mCameraCharacteristics);
+        if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            long[] uc = new long[]{CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT,
+                    CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW,
+                    CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL,
+                    CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_STILL_CAPTURE,
+                    CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL,
+                    CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD};
+            shadowCharacteristics0.set(CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES, uc);
+        }
+        return CameraCharacteristicsCompat.toCameraCharacteristicsCompat(
+                mCameraCharacteristics);
+    }
+
+    private CameraCharacteristicsCompat getCameraCharacteristicsCompatWithEmptyUseCases() {
+        ShadowCameraCharacteristics shadowCharacteristics0 = Shadow.extract(mCameraCharacteristics);
+        if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            shadowCharacteristics0.set(CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES,
+                    new long[]{});
+        }
+        return CameraCharacteristicsCompat.toCameraCharacteristicsCompat(
+                mCameraCharacteristics);
+    }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedOutputSizesCollectorTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedOutputSizesCollectorTest.kt
index 0691c5d..e7c1d634 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedOutputSizesCollectorTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedOutputSizesCollectorTest.kt
@@ -60,6 +60,8 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mockito
 import org.robolectric.ParameterizedRobolectricTestRunner
 import org.robolectric.Shadows
@@ -1242,6 +1244,35 @@
                 Mockito.`when`(it.getOutputSizes(MediaRecorder::class.java))
                     .thenReturn(supportedSizes)
 
+                // setup to return different minimum frame durations depending on resolution
+                // minimum frame durations were designated only for the purpose of testing
+                Mockito.`when`(it.getOutputMinFrameDuration(anyInt(), eq(Size(4032, 3024))))
+                    .thenReturn(50000000L) // 20 fps, size maximum
+
+                Mockito.`when`(it.getOutputMinFrameDuration(anyInt(), eq(Size(3840, 2160))))
+                    .thenReturn(40000000L) // 25, size record
+
+                Mockito.`when`(it.getOutputMinFrameDuration(anyInt(), eq(Size(1920, 1440))))
+                    .thenReturn(30000000L) // 30
+
+                Mockito.`when`(it.getOutputMinFrameDuration(anyInt(), eq(Size(1920, 1080))))
+                    .thenReturn(28000000L) // 35
+
+                Mockito.`when`(it.getOutputMinFrameDuration(anyInt(), eq(Size(1280, 960))))
+                    .thenReturn(25000000L) // 40
+
+                Mockito.`when`(it.getOutputMinFrameDuration(anyInt(), eq(Size(1280, 720))))
+                    .thenReturn(22000000L) // 45, size preview/display
+
+                Mockito.`when`(it.getOutputMinFrameDuration(anyInt(), eq(Size(960, 544))))
+                    .thenReturn(20000000L) // 50
+
+                Mockito.`when`(it.getOutputMinFrameDuration(anyInt(), eq(Size(800, 450))))
+                    .thenReturn(16666000L) // 60fps
+
+                Mockito.`when`(it.getOutputMinFrameDuration(anyInt(), eq(Size(640, 480))))
+                    .thenReturn(16666000L) // 60fps
+
                 // Sets up the supported high resolution sizes
                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                     Mockito.`when`(it.getHighResolutionOutputSizes(ArgumentMatchers.anyInt()))
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
index 2c2a694..760a818 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 package androidx.camera.camera2.internal
+
 import android.content.Context
 import android.graphics.ImageFormat
 import android.hardware.camera2.CameraCharacteristics
@@ -22,6 +23,7 @@
 import android.media.CamcorderProfile
 import android.os.Build
 import android.util.Pair
+import android.util.Range
 import android.util.Rational
 import android.util.Size
 import android.view.Surface
@@ -30,6 +32,7 @@
 import androidx.camera.camera2.Camera2Config
 import androidx.camera.camera2.internal.SupportedOutputSizesCollectorTest.Companion.createUseCaseByResolutionSelector
 import androidx.camera.camera2.internal.SupportedOutputSizesCollectorTest.Companion.setupCamera
+import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat
 import androidx.camera.camera2.internal.compat.CameraManagerCompat
 import androidx.camera.core.AspectRatio
 import androidx.camera.core.CameraSelector.LensFacing
@@ -41,15 +44,18 @@
 import androidx.camera.core.Preview
 import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.UseCase
+import androidx.camera.core.impl.AttachedSurfaceInfo
 import androidx.camera.core.impl.CameraDeviceSurfaceManager
 import androidx.camera.core.impl.CameraFactory
 import androidx.camera.core.impl.MutableStateObservable
 import androidx.camera.core.impl.SizeCoordinate
+import androidx.camera.core.impl.StreamSpec
 import androidx.camera.core.impl.SurfaceCombination
 import androidx.camera.core.impl.SurfaceConfig
 import androidx.camera.core.impl.SurfaceConfig.ConfigSize
 import androidx.camera.core.impl.SurfaceConfig.ConfigType
 import androidx.camera.core.impl.UseCaseConfig
+import androidx.camera.core.impl.UseCaseConfig.OPTION_TARGET_FRAME_RATE
 import androidx.camera.core.impl.UseCaseConfigFactory
 import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3
 import androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio
@@ -162,6 +168,8 @@
             targetRotation: Int,
             preferredAspectRatio: Int,
             preferredResolution: Size?,
+            targetFrameRate: Range<Int>?,
+            surfaceOccupancyPriority: Int,
             maxResolution: Size?,
             highResolutionEnabled: Boolean,
             defaultResolution: Size?,
@@ -173,6 +181,8 @@
                 targetRotation,
                 preferredAspectRatio,
                 preferredResolution,
+                targetFrameRate,
+                surfaceOccupancyPriority,
                 maxResolution,
                 defaultResolution,
                 supportedResolutions,
@@ -187,6 +197,8 @@
             targetRotation: Int,
             preferredAspectRatio: Int,
             preferredResolution: Size?,
+            targetFrameRate: Range<Int>?,
+            surfaceOccupancyPriority: Int,
             maxResolution: Size?,
             highResolutionEnabled: Boolean,
             defaultResolution: Size?,
@@ -214,6 +226,8 @@
             targetRotation: Int,
             preferredAspectRatio: Int,
             preferredResolution: Size?,
+            targetFrameRate: Range<Int>?,
+            surfaceOccupancyPriority: Int,
             maxResolution: Size?,
             highResolutionEnabled: Boolean,
             defaultResolution: Size?,
@@ -510,9 +524,9 @@
         )
         val maxJpegSize = supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.JPEG)
         val maxJpegAspectRatio = Rational(maxJpegSize.width, maxJpegSize.height)
-        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
-        val selectedSize = suggestedResolutionMap[useCase]
-        val resultAspectRatio = Rational(selectedSize!!.width, selectedSize.height)
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
+        val selectedSize = suggestedStreamSpecMap[useCase]!!.resolution
+        val resultAspectRatio = Rational(selectedSize.width, selectedSize.height)
         // The targetAspectRatio value will only be set to the same aspect ratio as maximum
         // supported jpeg size in Legacy + API 21 combination. For other combinations, it should
         // keep the original targetAspectRatio set for the use case.
@@ -570,13 +584,13 @@
         )
         val maxJpegSize = supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.JPEG)
         val maxJpegAspectRatio = Rational(maxJpegSize.width, maxJpegSize.height)
-        val suggestedResolutionMap = getSuggestedResolutionMap(
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
             supportedSurfaceCombination, preview,
             imageCapture, imageAnalysis
         )
-        val previewResolution = suggestedResolutionMap[preview]
-        val imageCaptureResolution = suggestedResolutionMap[imageCapture]
-        val imageAnalysisResolution = suggestedResolutionMap[imageAnalysis]
+        val previewResolution = suggestedStreamSpecMap[preview]!!.resolution
+        val imageCaptureResolution = suggestedStreamSpecMap[imageCapture]!!.resolution
+        val imageAnalysisResolution = suggestedStreamSpecMap[imageAnalysis]!!.resolution
         // The targetAspectRatio value will only be set to the same aspect ratio as maximum
         // supported jpeg size in Legacy + API 21 combination. For other combinations, it should
         // keep the original targetAspectRatio set for the use case.
@@ -584,16 +598,16 @@
             // Checks targetAspectRatio and maxJpegAspectRatio, which is the ratio of maximum size
             // in the mSupportedSizes, are not equal to make sure this test case is valid.
             assertThat(targetAspectRatio).isNotEqualTo(maxJpegAspectRatio)
-            assertThat(hasMatchingAspectRatio(previewResolution!!, maxJpegAspectRatio)).isTrue()
+            assertThat(hasMatchingAspectRatio(previewResolution, maxJpegAspectRatio)).isTrue()
             assertThat(
                 hasMatchingAspectRatio(
-                    imageCaptureResolution!!,
+                    imageCaptureResolution,
                     maxJpegAspectRatio
                 )
             ).isTrue()
             assertThat(
                 hasMatchingAspectRatio(
-                    imageAnalysisResolution!!,
+                    imageAnalysisResolution,
                     maxJpegAspectRatio
                 )
             ).isTrue()
@@ -601,19 +615,19 @@
             // Checks no correction is needed.
             assertThat(
                 hasMatchingAspectRatio(
-                    previewResolution!!,
+                    previewResolution,
                     targetAspectRatio
                 )
             ).isTrue()
             assertThat(
                 hasMatchingAspectRatio(
-                    imageCaptureResolution!!,
+                    imageCaptureResolution,
                     targetAspectRatio
                 )
             ).isTrue()
             assertThat(
                 hasMatchingAspectRatio(
-                    imageAnalysisResolution!!,
+                    imageAnalysisResolution,
                     targetAspectRatio
                 )
             ).isTrue()
@@ -654,13 +668,13 @@
         // Preview/ImageCapture/ImageAnalysis' default config settings that will be applied after
         // bound to lifecycle. Calling bindToLifecycle here to make sure sizes matching to
         // default aspect ratio will be selected.
-        val suggestedResolutionMap = getSuggestedResolutionMap(
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
             supportedSurfaceCombination, preview,
             imageCapture, imageAnalysis
         )
-        val previewSize = suggestedResolutionMap[preview]!!
-        val imageCaptureSize = suggestedResolutionMap[imageCapture]!!
-        val imageAnalysisSize = suggestedResolutionMap[imageAnalysis]!!
+        val previewSize = suggestedStreamSpecMap[preview]!!.resolution
+        val imageCaptureSize = suggestedStreamSpecMap[imageCapture]!!.resolution
+        val imageAnalysisSize = suggestedStreamSpecMap[imageAnalysis]!!.resolution
 
         val previewAspectRatio = Rational(previewSize.width, previewSize.height)
         val imageCaptureAspectRatio = Rational(imageCaptureSize.width, imageCaptureSize.height)
@@ -696,7 +710,7 @@
             PREVIEW_USE_CASE,
             targetResolution = Size(displayHeight, displayWidth)
         )
-        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, preview)
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, preview)
         // Checks the preconditions.
         val preconditionSize = Size(256, 144)
         val targetRatio = Rational(displayHeight, displayWidth)
@@ -705,7 +719,7 @@
             assertThat(Rational(it.width, it.height)).isNotEqualTo(targetRatio)
         }
         // Checks the mechanism has filtered out the sizes which are smaller than default size 480p.
-        val previewSize = suggestedResolutionMap[preview]
+        val previewSize = suggestedStreamSpecMap[preview]
         assertThat(previewSize).isNotEqualTo(preconditionSize)
     }
 
@@ -741,9 +755,9 @@
                 Surface.ROTATION_90,
                 preferredResolution = it
             )
-            val suggestedResolutionMap =
-                getSuggestedResolutionMap(supportedSurfaceCombination, imageCapture)
-            assertThat(it).isEqualTo(suggestedResolutionMap[imageCapture])
+            val suggestedStreamSpecMap =
+                getSuggestedStreamSpecMap(supportedSurfaceCombination, imageCapture)
+            assertThat(it).isEqualTo(suggestedStreamSpecMap[imageCapture]!!.resolution)
         }
     }
 
@@ -792,23 +806,23 @@
             Surface.ROTATION_90,
             preferredResolution = resolution
         )
-        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
-        assertThat(suggestedResolutionMap[useCase]).isEqualTo(expectedResult)
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
+        assertThat(suggestedStreamSpecMap[useCase]!!.resolution).isEqualTo(expectedResult)
     }
 
     @Test
-    fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice_LegacyApi() {
-        suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice(legacyUseCaseCreator)
+    fun suggestedStreamSpecsForMixedUseCaseNotSupportedInLegacyDevice_LegacyApi() {
+        suggestedStreamSpecsForMixedUseCaseNotSupportedInLegacyDevice(legacyUseCaseCreator)
     }
 
     @Test
-    fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice_ResolutionSelector() {
-        suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice(
+    fun suggestedStreamSpecsForMixedUseCaseNotSupportedInLegacyDevice_ResolutionSelector() {
+        suggestedStreamSpecsForMixedUseCaseNotSupportedInLegacyDevice(
             resolutionSelectorUseCaseCreator
         )
     }
 
-    private fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice(
+    private fun suggestedStreamSpecsForMixedUseCaseNotSupportedInLegacyDevice(
         useCaseCreator: UseCaseCreator
     ) {
         setupCameraAndInitCameraX(
@@ -829,7 +843,7 @@
         // An IllegalArgumentException will be thrown because a LEGACY level device can't support
         // ImageCapture + VideoCapture + Preview
         assertThrows(IllegalArgumentException::class.java) {
-            getSuggestedResolutionMap(
+            getSuggestedStreamSpecMap(
                 supportedSurfaceCombination,
                 imageCapture,
                 videoCapture,
@@ -839,18 +853,18 @@
     }
 
     @Test
-    fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice_LegacyApi() {
-        suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice(legacyUseCaseCreator)
+    fun suggestedStreamSpecsForCustomizeResolutionsNotSupportedInLegacyDevice_LegacyApi() {
+        suggestedStreamSpecsForCustomizeResolutionsNotSupportedInLegacyDevice(legacyUseCaseCreator)
     }
 
     @Test
-    fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice_ResolutionSelector() {
-        suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice(
+    fun suggestedStreamSpecsForCustomizeResolutionsNotSupportedInLegacyDevice_ResolutionSelector() {
+        suggestedStreamSpecsForCustomizeResolutionsNotSupportedInLegacyDevice(
             resolutionSelectorUseCaseCreator
         )
     }
 
-    private fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice(
+    private fun suggestedStreamSpecsForCustomizeResolutionsNotSupportedInLegacyDevice(
         useCaseCreator: UseCaseCreator
     ) {
         setupCameraAndInitCameraX(
@@ -871,21 +885,21 @@
         // An IllegalArgumentException will be thrown because the VideoCapture requests to only
         // support a RECORD size but the configuration can't be supported on a LEGACY level device.
         assertThrows(IllegalArgumentException::class.java) {
-            getSuggestedResolutionMap(supportedSurfaceCombination, videoCapture, preview)
+            getSuggestedStreamSpecMap(supportedSurfaceCombination, videoCapture, preview)
         }
     }
 
     @Test
-    fun getSuggestedResolutionsForMixedUseCaseInLimitedDevice_LegacyApi() {
-        getSuggestedResolutionsForMixedUseCaseInLimitedDevice(legacyUseCaseCreator)
+    fun getsuggestedStreamSpecsForMixedUseCaseInLimitedDevice_LegacyApi() {
+        getsuggestedStreamSpecsForMixedUseCaseInLimitedDevice(legacyUseCaseCreator)
     }
 
     @Test
-    fun getSuggestedResolutionsForMixedUseCaseInLimitedDevice_ResolutionSelector() {
-        getSuggestedResolutionsForMixedUseCaseInLimitedDevice(resolutionSelectorUseCaseCreator)
+    fun getsuggestedStreamSpecsForMixedUseCaseInLimitedDevice_ResolutionSelector() {
+        getsuggestedStreamSpecsForMixedUseCaseInLimitedDevice(resolutionSelectorUseCaseCreator)
     }
 
-    private fun getSuggestedResolutionsForMixedUseCaseInLimitedDevice(
+    private fun getsuggestedStreamSpecsForMixedUseCaseInLimitedDevice(
         useCaseCreator: UseCaseCreator
     ) {
         setupCameraAndInitCameraX(
@@ -903,16 +917,16 @@
             PREVIEW_USE_CASE,
             preferredAspectRatio = AspectRatio.RATIO_16_9
         )
-        val suggestedResolutionMap = getSuggestedResolutionMap(
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
             supportedSurfaceCombination,
             imageCapture,
             videoCapture,
             preview
         )
         // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
-        assertThat(suggestedResolutionMap[imageCapture]).isEqualTo(RECORD_SIZE)
-        assertThat(suggestedResolutionMap[videoCapture]).isEqualTo(RECORD_SIZE)
-        assertThat(suggestedResolutionMap[preview]).isEqualTo(PREVIEW_SIZE)
+        assertThat(suggestedStreamSpecMap[imageCapture]!!.resolution).isEqualTo(RECORD_SIZE)
+        assertThat(suggestedStreamSpecMap[videoCapture]!!.resolution).isEqualTo(RECORD_SIZE)
+        assertThat(suggestedStreamSpecMap[preview]!!.resolution).isEqualTo(PREVIEW_SIZE)
     }
 
     // For the use case in b/230651237,
@@ -920,19 +934,19 @@
     // VideoCapture should have higher priority to choose size than ImageCapture.
     @Test
     @Throws(CameraUnavailableException::class)
-    fun getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage_LegacyApi() {
-        getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage(legacyUseCaseCreator)
+    fun getsuggestedStreamSpecsInFullDevice_videoHasHigherPriorityThanImage_LegacyApi() {
+        getsuggestedStreamSpecsInFullDevice_videoHasHigherPriorityThanImage(legacyUseCaseCreator)
     }
 
     @Test
     @Throws(CameraUnavailableException::class)
-    fun getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage_ResolutionSelector() {
-        getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage(
+    fun getsuggestedStreamSpecsInFullDevice_videoHasHigherPriorityThanImage_ResolutionSelector() {
+        getsuggestedStreamSpecsInFullDevice_videoHasHigherPriorityThanImage(
             resolutionSelectorUseCaseCreator
         )
     }
 
-    private fun getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage(
+    private fun getsuggestedStreamSpecsInFullDevice_videoHasHigherPriorityThanImage(
         useCaseCreator: UseCaseCreator
     ) {
         setupCameraAndInitCameraX(
@@ -953,7 +967,7 @@
             PREVIEW_USE_CASE,
             preferredAspectRatio = AspectRatio.RATIO_16_9
         )
-        val suggestedResolutionMap = getSuggestedResolutionMap(
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
             supportedSurfaceCombination,
             imageCapture,
             videoCapture,
@@ -962,9 +976,9 @@
         // There are two possible combinations in Full level device
         // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD) => should be applied
         // (PRIV, PREVIEW) + (PRIV, PREVIEW) + (JPEG, MAXIMUM)
-        assertThat(suggestedResolutionMap[imageCapture]).isEqualTo(RECORD_SIZE)
-        assertThat(suggestedResolutionMap[videoCapture]).isEqualTo(RECORD_SIZE)
-        assertThat(suggestedResolutionMap[preview]).isEqualTo(PREVIEW_SIZE)
+        assertThat(suggestedStreamSpecMap[imageCapture]!!.resolution).isEqualTo(RECORD_SIZE)
+        assertThat(suggestedStreamSpecMap[videoCapture]!!.resolution).isEqualTo(RECORD_SIZE)
+        assertThat(suggestedStreamSpecMap[preview]!!.resolution).isEqualTo(PREVIEW_SIZE)
     }
 
     @Test
@@ -1001,7 +1015,7 @@
             PREVIEW_USE_CASE,
             preferredAspectRatio = AspectRatio.RATIO_16_9
         )
-        val suggestedResolutionMap = getSuggestedResolutionMap(
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
             supportedSurfaceCombination,
             imageCapture,
             videoCapture,
@@ -1010,36 +1024,37 @@
         // There are two possible combinations in Full level device
         // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
         // (PRIV, PREVIEW) + (PRIV, PREVIEW) + (JPEG, MAXIMUM) => should be applied
-        assertThat(suggestedResolutionMap[imageCapture]).isEqualTo(MAXIMUM_SIZE)
-        assertThat(suggestedResolutionMap[videoCapture]).isEqualTo(PREVIEW_SIZE) // Quality.HD
-        assertThat(suggestedResolutionMap[preview]).isEqualTo(PREVIEW_SIZE)
+        assertThat(suggestedStreamSpecMap[imageCapture]!!.resolution).isEqualTo(MAXIMUM_SIZE)
+        // Quality.HD
+        assertThat(suggestedStreamSpecMap[videoCapture]!!.resolution).isEqualTo(PREVIEW_SIZE)
+        assertThat(suggestedStreamSpecMap[preview]!!.resolution).isEqualTo(PREVIEW_SIZE)
     }
 
     @Test
-    fun getSuggestedResolutionsWithSameSupportedListForDifferentUseCases_LegacyApi() {
-        getSuggestedResolutionsWithSameSupportedListForDifferentUseCases(
+    fun getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases_LegacyApi() {
+        getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases(
             legacyUseCaseCreator,
             DISPLAY_SIZE
         )
     }
 
     @Test
-    fun getSuggestedResolutionsWithSameSupportedListForDifferentUseCases_RS_SensorSize() {
-        getSuggestedResolutionsWithSameSupportedListForDifferentUseCases(
+    fun getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases_RS_SensorSize() {
+        getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases(
             resolutionSelectorUseCaseCreator,
             PREVIEW_SIZE
         )
     }
 
     @Test
-    fun getSuggestedResolutionsWithSameSupportedListForDifferentUseCases_RS_ViewSize() {
-        getSuggestedResolutionsWithSameSupportedListForDifferentUseCases(
+    fun getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases_RS_ViewSize() {
+        getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases(
             viewSizeResolutionSelectorUseCaseCreator,
             PREVIEW_SIZE
         )
     }
 
-    private fun getSuggestedResolutionsWithSameSupportedListForDifferentUseCases(
+    private fun getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases(
         useCaseCreator: UseCaseCreator,
         preferredResolution: Size
     ) {
@@ -1082,15 +1097,15 @@
             IMAGE_ANALYSIS_USE_CASE,
             preferredResolution = preferredResolution
         )
-        val suggestedResolutionMap = getSuggestedResolutionMap(
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
             supportedSurfaceCombination,
             imageCapture,
             imageAnalysis,
             preview
         )
-        assertThat(suggestedResolutionMap[imageCapture]).isEqualTo(PREVIEW_SIZE)
-        assertThat(suggestedResolutionMap[imageAnalysis]).isEqualTo(PREVIEW_SIZE)
-        assertThat(suggestedResolutionMap[preview]).isEqualTo(PREVIEW_SIZE)
+        assertThat(suggestedStreamSpecMap[imageCapture]!!.resolution).isEqualTo(PREVIEW_SIZE)
+        assertThat(suggestedStreamSpecMap[imageAnalysis]!!.resolution).isEqualTo(PREVIEW_SIZE)
+        assertThat(suggestedStreamSpecMap[preview]!!.resolution).isEqualTo(PREVIEW_SIZE)
     }
 
     @Test
@@ -1122,7 +1137,7 @@
             IMAGE_ANALYSIS_USE_CASE,
             preferredAspectRatio = AspectRatio.RATIO_16_9
         )
-        val suggestedResolutionMap = getSuggestedResolutionMap(
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
             supportedSurfaceCombination,
             preview,
             imageCapture,
@@ -1130,35 +1145,35 @@
         )
         assertThat(
             hasMatchingAspectRatio(
-                suggestedResolutionMap[preview]!!,
+                suggestedStreamSpecMap[preview]!!.resolution,
                 ASPECT_RATIO_16_9
             )
         ).isTrue()
         assertThat(
             hasMatchingAspectRatio(
-                suggestedResolutionMap[imageCapture]!!,
+                suggestedStreamSpecMap[imageCapture]!!.resolution,
                 ASPECT_RATIO_16_9
             )
         ).isTrue()
         assertThat(
             hasMatchingAspectRatio(
-                suggestedResolutionMap[imageAnalysis]!!,
+                suggestedStreamSpecMap[imageAnalysis]!!.resolution,
                 ASPECT_RATIO_16_9
             )
         ).isTrue()
     }
 
     @Test
-    fun getSuggestedResolutionsForCustomizedSupportedResolutions_LegacyApi() {
-        getSuggestedResolutionsForCustomizedSupportedResolutions(legacyUseCaseCreator)
+    fun getsuggestedStreamSpecsForCustomizedSupportedResolutions_LegacyApi() {
+        getsuggestedStreamSpecsForCustomizedSupportedResolutions(legacyUseCaseCreator)
     }
 
     @Test
-    fun getSuggestedResolutionsForCustomizedSupportedResolutions_ResolutionSelector() {
-        getSuggestedResolutionsForCustomizedSupportedResolutions(resolutionSelectorUseCaseCreator)
+    fun getsuggestedStreamSpecsForCustomizedSupportedResolutions_ResolutionSelector() {
+        getsuggestedStreamSpecsForCustomizedSupportedResolutions(resolutionSelectorUseCaseCreator)
     }
 
-    private fun getSuggestedResolutionsForCustomizedSupportedResolutions(
+    private fun getsuggestedStreamSpecsForCustomizedSupportedResolutions(
         useCaseCreator: UseCaseCreator
     ) {
         setupCameraAndInitCameraX(
@@ -1182,16 +1197,16 @@
             PREVIEW_USE_CASE,
             supportedResolutions = formatResolutionsPairList
         )
-        val suggestedResolutionMap = getSuggestedResolutionMap(
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
             supportedSurfaceCombination,
             imageCapture,
             videoCapture,
             preview
         )
-        // Checks all suggested resolutions will become 640x480.
-        assertThat(suggestedResolutionMap[imageCapture]).isEqualTo(RESOLUTION_VGA)
-        assertThat(suggestedResolutionMap[videoCapture]).isEqualTo(RESOLUTION_VGA)
-        assertThat(suggestedResolutionMap[preview]).isEqualTo(RESOLUTION_VGA)
+        // Checks all resolutions in suggested stream specs will become 640x480.
+        assertThat(suggestedStreamSpecMap[imageCapture]?.resolution).isEqualTo(RESOLUTION_VGA)
+        assertThat(suggestedStreamSpecMap[videoCapture]?.resolution).isEqualTo(RESOLUTION_VGA)
+        assertThat(suggestedStreamSpecMap[preview]?.resolution).isEqualTo(RESOLUTION_VGA)
     }
 
     @Test
@@ -1334,8 +1349,8 @@
             preferredAspectRatio = AspectRatio.RATIO_16_9,
             preferredResolution = MOD16_SIZE
         )
-        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
-        assertThat(suggestedResolutionMap[useCase]).isEqualTo(MOD16_SIZE)
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
+        assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(MOD16_SIZE)
     }
 
     @Test
@@ -1826,9 +1841,9 @@
             PREVIEW_USE_CASE,
             maxResolution = MAXIMUM_SIZE
         )
-        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
         // Checks mMaximumSize is final selected for the use case.
-        assertThat(suggestedResolutionMap[useCase]).isEqualTo(MAXIMUM_SIZE)
+        assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(MAXIMUM_SIZE)
     }
 
     @Test
@@ -2406,9 +2421,9 @@
             targetRotation = Surface.ROTATION_90,
             preferredResolution = RESOLUTION_VGA
         )
-        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
         // Checks 640x480 is final selected for the use case.
-        assertThat(suggestedResolutionMap[useCase]).isEqualTo(RESOLUTION_VGA)
+        assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(RESOLUTION_VGA)
     }
 
     @Test
@@ -2438,9 +2453,9 @@
             FAKE_USE_CASE,
             maxResolution = DISPLAY_SIZE
         )
-        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
         // Checks 480x480 is final selected for the use case.
-        assertThat(suggestedResolutionMap[useCase]).isEqualTo(Size(480, 480))
+        assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(Size(480, 480))
     }
 
     @Test
@@ -2502,13 +2517,13 @@
             targetRotation = Surface.ROTATION_90,
             preferredResolution = RECORD_SIZE
         )
-        val suggestedResolutionMap = getSuggestedResolutionMap(
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
             supportedSurfaceCombination,
             preview,
             imageCapture,
             imageAnalysis
         )
-        assertThat(suggestedResolutionMap[imageAnalysis]).isEqualTo(expectedResult)
+        assertThat(suggestedStreamSpecMap[imageAnalysis]?.resolution).isEqualTo(expectedResult)
     }
 
     @Test
@@ -2570,13 +2585,13 @@
             targetRotation = Surface.ROTATION_90,
             preferredResolution = RECORD_SIZE
         )
-        val suggestedResolutionMap = getSuggestedResolutionMap(
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
             supportedSurfaceCombination,
             preview,
             imageCapture,
             imageAnalysis
         )
-        assertThat(suggestedResolutionMap[imageAnalysis]).isEqualTo(RECORD_SIZE)
+        assertThat(suggestedStreamSpecMap[imageAnalysis]?.resolution).isEqualTo(RECORD_SIZE)
     }
 
     @Config(minSdk = Build.VERSION_CODES.M)
@@ -2594,10 +2609,10 @@
         )
 
         val useCase = createUseCaseByResolutionSelector(FAKE_USE_CASE, highResolutionEnabled = true)
-        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
 
         // Checks 8000x6000 is final selected for the use case.
-        assertThat(suggestedResolutionMap[useCase]).isEqualTo(Size(8000, 6000))
+        assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(Size(8000, 6000))
     }
 
     @Config(minSdk = Build.VERSION_CODES.M)
@@ -2612,10 +2627,10 @@
         )
 
         val useCase = createUseCaseByResolutionSelector(FAKE_USE_CASE, highResolutionEnabled = true)
-        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
 
         // Checks 8000x6000 is final selected for the use case.
-        assertThat(suggestedResolutionMap[useCase]).isEqualTo(Size(4032, 3024))
+        assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(Size(4032, 3024))
     }
 
     @Config(minSdk = Build.VERSION_CODES.M)
@@ -2634,10 +2649,10 @@
 
         val useCase =
             createUseCaseByResolutionSelector(FAKE_USE_CASE, preferredResolution = Size(8000, 6000))
-        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
 
         // Checks 8000x6000 is final selected for the use case.
-        assertThat(suggestedResolutionMap[useCase]).isEqualTo(Size(4032, 3024))
+        assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(Size(4032, 3024))
     }
 
     @Config(minSdk = Build.VERSION_CODES.M)
@@ -2659,10 +2674,340 @@
             preferredAspectRatio = AspectRatio.RATIO_16_9,
             highResolutionEnabled = true
         )
-        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
 
         // Checks 8000x6000 is final selected for the use case.
-        assertThat(suggestedResolutionMap[useCase]).isEqualTo(Size(8000, 4500))
+        assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(Size(8000, 4500))
+    }
+
+    @Test
+    @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
+    fun getSupportedOutputSizes_single_valid_targetFPS() {
+        // a valid target means the device is capable of that fps
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+
+        // use case with target fps
+        val useCase1 = createUseCaseByLegacyApi(
+            FAKE_USE_CASE,
+            targetFrameRate = Range<Int>(25, 30)
+        )
+
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
+            supportedSurfaceCombination,
+            useCase1
+        )
+        // single selected size should be equal to 3840 x 2160
+        assertThat(suggestedStreamSpecMap[useCase1]!!.resolution).isEqualTo(Size(3840, 2160))
+    }
+
+    @Test
+    @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
+    fun getSupportedOutputSizes_single_invalid_targetFPS() {
+        // an invalid target means the device would neve be able to reach that fps
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+
+        // use case with target fps
+        val useCase1 = createUseCaseByLegacyApi(
+            FAKE_USE_CASE,
+            targetFrameRate = Range<Int>(65, 70)
+        )
+
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
+            supportedSurfaceCombination,
+            useCase1
+        )
+        // single selected size should be equal to 3840 x 2160
+        assertThat(suggestedStreamSpecMap[useCase1]!!.resolution).isEqualTo(Size(800, 450))
+    }
+
+    @Test
+    @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
+    fun getSupportedOutputSizes_multiple_targetFPS_first_is_larger() {
+        // a valid target means the device is capable of that fps
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+
+        val useCase1 = createUseCaseByLegacyApi(
+            FAKE_USE_CASE,
+            targetFrameRate = Range<Int>(30, 35),
+            surfaceOccupancyPriority = 1
+        )
+
+        val useCase2 = createUseCaseByLegacyApi(
+            FAKE_USE_CASE,
+            targetFrameRate = Range<Int>(15, 25)
+        )
+
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
+            supportedSurfaceCombination,
+            useCase1,
+            useCase2
+        )
+        // both selected size should be no larger than 1920 x 1080
+        assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase1]!!.resolution, Size(1920, 1445)))
+            .isTrue()
+        assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase2]!!.resolution, Size(1920, 1445)))
+            .isTrue()
+    }
+
+    @Test
+    @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
+    fun getSupportedOutputSizes_multiple_targetFPS_first_is_smaller() {
+        // a valid target means the device is capable of that fps
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+
+        val useCase1 = createUseCaseByLegacyApi(
+            FAKE_USE_CASE,
+            targetFrameRate = Range<Int>(30, 35),
+            surfaceOccupancyPriority = 1
+        )
+
+        val useCase2 = createUseCaseByLegacyApi(
+            FAKE_USE_CASE,
+            targetFrameRate = Range<Int>(45, 50)
+        )
+
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
+            supportedSurfaceCombination,
+            useCase1,
+            useCase2
+        )
+        // both selected size should be no larger than 1920 x 1440
+        assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase1]!!.resolution, Size(1920, 1440)))
+            .isTrue()
+        assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase2]!!.resolution, Size(1920, 1440)))
+            .isTrue()
+    }
+
+    @Test
+    @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
+    fun getSupportedOutputSizes_multiple_targetFPS_intersect() {
+        // first and second new use cases have target fps that intersect each other
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+
+        val useCase1 = createUseCaseByLegacyApi(
+            FAKE_USE_CASE,
+            targetFrameRate = Range<Int>(30, 40),
+            surfaceOccupancyPriority = 1
+        )
+
+        val useCase2 = createUseCaseByLegacyApi(
+            FAKE_USE_CASE,
+            targetFrameRate = Range<Int>(35, 45)
+        )
+
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
+            supportedSurfaceCombination,
+            useCase1,
+            useCase2
+        )
+        // effective target fps becomes 35-40
+        // both selected size should be no larger than 1920 x 1080
+        assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase1]!!.resolution, Size(1920, 1080)))
+            .isTrue()
+        assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase2]!!.resolution, Size(1920, 1080)))
+            .isTrue()
+    }
+    @Test
+    @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
+    fun getSupportedOutputSizes_multiple_cases_first_has_targetFPS() {
+        // first new use case has a target fps, second new use case does not
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+
+        val useCase1 = createUseCaseByLegacyApi(
+            FAKE_USE_CASE,
+            targetFrameRate = Range<Int>(30, 35),
+            surfaceOccupancyPriority = 1
+        )
+
+        val useCase2 = createUseCaseByLegacyApi(
+            FAKE_USE_CASE
+        )
+
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
+            supportedSurfaceCombination,
+            useCase1,
+            useCase2
+        )
+        // both selected size should be no larger than 1920 x 1440
+        assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase1]!!.resolution, Size(1920, 1440)))
+            .isTrue()
+        assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase2]!!.resolution, Size(1920, 1440)))
+            .isTrue()
+    }
+
+    @Test
+    @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
+    fun getSupportedOutputSizes_multiple_cases_second_has_targetFPS() {
+        // second new use case does not have a target fps, first new use case does not
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+
+        val useCase1 = createUseCaseByLegacyApi(
+            FAKE_USE_CASE
+        )
+        val useCase2 = createUseCaseByLegacyApi(
+            FAKE_USE_CASE,
+            targetFrameRate = Range<Int>(30, 35)
+        )
+
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
+            supportedSurfaceCombination,
+            useCase1,
+            useCase2
+        )
+        // both selected size should be no larger than 1920 x 1440
+        assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase1]!!.resolution, Size(1920, 1440)))
+            .isTrue()
+        assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase2]!!.resolution, Size(1920, 1440)))
+            .isTrue()
+    }
+
+    @Test
+    @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
+    fun getSupportedOutputSizes_attached_with_targetFPS_no_new_targetFPS() {
+        // existing surface with target fps + new use case without a target fps
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+
+        // existing surface w/ target fps
+        val attachedSurfaceInfo = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                ConfigType.JPEG,
+                ConfigSize.PREVIEW
+            ), ImageFormat.JPEG,
+            Size(1280, 720), Range(40, 50)
+        )
+
+        // new use case with no target fps
+        val useCase1 = createUseCaseByLegacyApi(FAKE_USE_CASE)
+
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
+            supportedSurfaceCombination,
+            useCase1,
+            attachedSurfaces = listOf(attachedSurfaceInfo)
+        )
+        // size should be no larger than 1280 x 960
+        assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase1]!!.resolution, Size(1280, 960)))
+            .isTrue()
+    }
+    @Test
+    @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
+    fun getSupportedOutputSizes_attached_with_targetFPS_and_new_targetFPS_no_intersect() {
+        // existing surface with target fps + new use case with target fps that does not intersect
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+
+        // existing surface w/ target fps
+        val attachedSurfaceInfo = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                ConfigType.JPEG,
+                ConfigSize.PREVIEW
+            ), ImageFormat.JPEG,
+            Size(1280, 720), Range(40, 50)
+        )
+
+        // new use case with target fps
+        val useCase1 = createUseCaseByLegacyApi(
+            FAKE_USE_CASE,
+            targetFrameRate = Range<Int>(30, 35)
+
+        )
+
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
+            supportedSurfaceCombination,
+            useCase1,
+            attachedSurfaces = listOf(attachedSurfaceInfo)
+        )
+        // size of new surface should be no larger than 1280 x 960
+        assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase1]!!.resolution, Size(1280, 960)))
+            .isTrue()
+    }
+
+    @Test
+    @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
+    fun getSupportedOutputSizes_attached_with_targetFPS_and_new_targetFPS_with_intersect() {
+        // existing surface with target fps + new use case with target fps that intersect each other
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+
+        // existing surface w/ target fps
+        val attachedSurfaceInfo = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                ConfigType.JPEG,
+                ConfigSize.PREVIEW
+            ), ImageFormat.JPEG,
+            Size(1280, 720), Range(40, 50)
+        )
+
+        // new use case with target fps
+        val useCase1 = createUseCaseByLegacyApi(
+            FAKE_USE_CASE,
+            targetFrameRate = Range<Int>(45, 50)
+
+        )
+
+        val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
+            supportedSurfaceCombination,
+            useCase1,
+            attachedSurfaces = listOf(attachedSurfaceInfo)
+        )
+        // size of new surface should be no larger than 1280 x 720
+        assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase1]!!.resolution, Size(1280, 720)))
+            .isTrue()
+    }
+
+    /**
+     * Helper function that returns whether size is <= maxSize
+     *
+     */
+    private fun sizeIsAtMost(size: Size, maxSize: Size): Boolean {
+        return (size.height * size.width) <= (maxSize.height * maxSize.width)
     }
 
     /**
@@ -2781,30 +3126,32 @@
      * Gets the suggested resolution map by the converted ResolutionSelector use case config which
      * will also be converted when a use case is bound to the lifecycle.
      */
-    private fun getSuggestedResolutionMap(
+    private fun getSuggestedStreamSpecMap(
         supportedSurfaceCombination: SupportedSurfaceCombination,
         vararg useCases: UseCase,
+        attachedSurfaces: List<AttachedSurfaceInfo>? = null,
         cameraFactory: CameraFactory = this.cameraFactory!!,
         cameraId: String = DEFAULT_CAMERA_ID,
         useCaseConfigFactory: UseCaseConfigFactory = this.useCaseConfigFactory!!
-    ): Map<UseCase, Size?> {
+    ): Map<UseCase, StreamSpec?> {
         // Generates the use case to new ResolutionSelector use case config map
         val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
             cameraFactory.getCamera(cameraId).cameraInfoInternal,
             listOf(*useCases),
             useCaseConfigFactory
         )
-        // Uses the use case config list to get suggested resolutions
-        val useCaseConfigResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
-            emptyList(),
-            mutableListOf<UseCaseConfig<*>?>().apply { addAll(useCaseToConfigMap.values) }
+        // Uses the use case config list to get suggested stream specs
+        val useCaseConfigStreamSpecMap = supportedSurfaceCombination
+            .getSuggestedStreamSpecifications(
+                attachedSurfaces ?: emptyList(),
+                mutableListOf<UseCaseConfig<*>?>().apply { addAll(useCaseToConfigMap.values) }
         )
-        val useCaseResolutionMap = mutableMapOf<UseCase, Size?>()
+        val useCaseStreamSpecMap = mutableMapOf<UseCase, StreamSpec?>()
         // Maps the use cases to the suggestion resolutions
         for (useCase in useCases) {
-            useCaseResolutionMap[useCase] = useCaseConfigResolutionMap[useCaseToConfigMap[useCase]]
+            useCaseStreamSpecMap[useCase] = useCaseConfigStreamSpecMap[useCaseToConfigMap[useCase]]
         }
-        return useCaseResolutionMap
+        return useCaseStreamSpecMap
     }
 
     /**
@@ -2849,6 +3196,8 @@
         targetRotation: Int = UNKNOWN_ROTATION,
         targetAspectRatio: Int = UNKNOWN_ASPECT_RATIO,
         targetResolution: Size? = null,
+        targetFrameRate: Range<Int>? = null,
+        surfaceOccupancyPriority: Int = -1,
         maxResolution: Size? = null,
         defaultResolution: Size? = null,
         supportedResolutions: List<Pair<Int, Array<Size>>>? = null,
@@ -2866,6 +3215,10 @@
         if (targetAspectRatio != UNKNOWN_ASPECT_RATIO) {
             builder.setTargetAspectRatio(targetAspectRatio)
         }
+        if (surfaceOccupancyPriority >= 0) {
+            builder.setSurfaceOccupancyPriority(surfaceOccupancyPriority)
+        }
+        builder.mutableConfig.insertOption(OPTION_TARGET_FRAME_RATE, targetFrameRate)
         targetResolution?.let { builder.setTargetResolution(it) }
         maxResolution?.let { builder.setMaxResolution(it) }
         defaultResolution?.let { builder.setDefaultResolution(it) }
@@ -2918,6 +3271,8 @@
             targetRotation: Int = UNKNOWN_ROTATION,
             preferredAspectRatio: Int = UNKNOWN_ASPECT_RATIO,
             preferredResolution: Size? = null,
+            targetFrameRate: Range<Int>? = null,
+            surfaceOccupancyPriority: Int = -1,
             maxResolution: Size? = null,
             highResolutionEnabled: Boolean = false,
             defaultResolution: Size? = null,
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/FlashAvailabilityCheckerTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/FlashAvailabilityCheckerTest.kt
new file mode 100644
index 0000000..d0e4ac1
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/FlashAvailabilityCheckerTest.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.internal.compat.workaround
+
+import android.hardware.camera2.CameraCharacteristics
+import android.os.Build
+import com.google.common.truth.Truth.assertThat
+import java.nio.BufferUnderflowException
+import org.junit.Assert.assertThrows
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers
+
+private const val FAKE_OEM = "fake_oem"
+
+// @Config() is left out since there currently aren't any API level dependencies in this workaround
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+class FlashAvailabilityCheckerTest(
+    private val manufacturer: String,
+    private val model: String,
+    private val characteristicsProvider: CameraCharacteristicsProvider
+) {
+    companion object {
+        @JvmStatic
+        @ParameterizedRobolectricTestRunner.Parameters(name = "manufacturer={0}, model={1}")
+        fun data() = mutableListOf<Array<Any?>>().apply {
+            add(arrayOf("sprd", "LEMP", BufferUnderflowProvider()))
+            add(arrayOf("sprd", "DM20C", BufferUnderflowProvider()))
+            add(arrayOf(FAKE_OEM, "unexpected_throwing_device", BufferUnderflowProvider()))
+            add(arrayOf(FAKE_OEM, "not_a_real_device", FlashAvailabilityTrueProvider()))
+            add(arrayOf(FAKE_OEM, "null_returning_device", FlashAvailabilityNullProvider()))
+        }
+    }
+
+    @Before
+    fun setup() {
+        ReflectionHelpers.setStaticField(Build::class.java, "MANUFACTURER", manufacturer)
+        ReflectionHelpers.setStaticField(Build::class.java, "MODEL", model)
+    }
+
+    @Test
+    fun isFlashAvailable_doesNotThrow_whenRethrowDisabled() {
+        FlashAvailabilityChecker.isFlashAvailable(characteristicsProvider)
+    }
+
+    @Test
+    fun isFlashAvailable_throwsForUnexpectedDevice() {
+        assumeTrue(Build.MODEL == "unexpected_throwing_device")
+        assertThrows(BufferUnderflowException::class.java) {
+            FlashAvailabilityChecker.isFlashAvailable(/*rethrowOnError=*/true,
+                characteristicsProvider
+            )
+        }
+    }
+
+    @Test
+    fun isFlashAvailable_returnsFalse_whenFlashAvailableReturnsNull() {
+        assumeTrue(Build.MODEL == "null_returning_device")
+
+        assertThat(FlashAvailabilityChecker.isFlashAvailable(characteristicsProvider)).isFalse()
+    }
+}
+
+private class FlashAvailabilityTrueProvider : CameraCharacteristicsProvider {
+    @Suppress("UNCHECKED_CAST")
+    override fun <T : Any?> get(key: CameraCharacteristics.Key<T>): T? = when (key) {
+        CameraCharacteristics.FLASH_INFO_AVAILABLE -> true as T?
+        else -> null
+    }
+}
+
+private class BufferUnderflowProvider : CameraCharacteristicsProvider {
+    override fun <T : Any?> get(key: CameraCharacteristics.Key<T>): T =
+        throw BufferUnderflowException()
+}
+
+private class FlashAvailabilityNullProvider : CameraCharacteristicsProvider {
+    override fun <T : Any?> get(key: CameraCharacteristics.Key<T>): T? = null
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java
index e011f44..0b1fc01 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java
@@ -16,13 +16,12 @@
 
 package androidx.camera.core;
 
-import android.util.Size;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
 import androidx.camera.testing.fakes.FakeUseCase;
@@ -80,8 +79,8 @@
 
     @Override
     @NonNull
-    protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
-        return suggestedResolution;
+    protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
+        return suggestedStreamSpec;
     }
 
     /** Returns true if {@link #onUnbind()} has been called previously. */
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
index 5c09967..3ea1b92 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
@@ -40,6 +40,7 @@
 import androidx.camera.core.impl.CameraCaptureMetaData;
 import androidx.camera.core.impl.CaptureConfig;
 import androidx.camera.core.impl.ImageCaptureConfig;
+import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.UseCaseConfigFactory;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.internal.CameraUseCaseAdapter;
@@ -96,9 +97,9 @@
 
         FakeCameraDeviceSurfaceManager fakeCameraDeviceSurfaceManager =
                 new FakeCameraDeviceSurfaceManager();
-        fakeCameraDeviceSurfaceManager.setSuggestedResolution("fakeCameraId",
+        fakeCameraDeviceSurfaceManager.setSuggestedStreamSpec("fakeCameraId",
                 ImageCaptureConfig.class,
-                new Size(640, 480));
+                StreamSpec.builder(new Size(640, 480)).build());
 
         UseCaseConfigFactory useCaseConfigFactory = new FakeUseCaseConfigFactory();
 
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
index 4ab66e3..16ee9c4c 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
@@ -28,6 +28,7 @@
 import androidx.camera.core.impl.Config
 import androidx.camera.core.impl.ImageOutputConfig
 import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.StreamSpec
 import androidx.camera.core.impl.UseCaseConfigFactory
 import androidx.camera.core.internal.CameraUseCaseAdapter
 import androidx.camera.testing.fakes.FakeCamera
@@ -145,7 +146,7 @@
             "UseCase"
         ).useCaseConfig
         val testUseCase = TestUseCase(config)
-        testUseCase.updateSuggestedResolution(Size(640, 480))
+        testUseCase.updateSuggestedStreamSpec(TEST_STREAM_SPEC)
         Truth.assertThat(testUseCase.attachedSurfaceResolution).isNotNull()
         testUseCase.bindToCamera(mockCameraInternal!!, null, null)
         testUseCase.unbindFromCamera(mockCameraInternal!!)
@@ -153,6 +154,19 @@
     }
 
     @Test
+    fun attachedStreamSpecCanBeReset_whenOnDetach() {
+        val config = FakeUseCaseConfig.Builder().setTargetName(
+            "UseCase"
+        ).useCaseConfig
+        val testUseCase = TestUseCase(config)
+        testUseCase.updateSuggestedStreamSpec(TEST_STREAM_SPEC)
+        Truth.assertThat(testUseCase.attachedStreamSpec).isNotNull()
+        testUseCase.bindToCamera(mockCameraInternal!!, null, null)
+        testUseCase.unbindFromCamera(mockCameraInternal!!)
+        Truth.assertThat(testUseCase.attachedStreamSpec).isNull()
+    }
+
+    @Test
     fun viewPortCropRectCanBeReset_whenOnDetach() {
         val config = FakeUseCaseConfig.Builder().setTargetName(
             "UseCase"
@@ -259,10 +273,10 @@
             FakeCameraInfoInternal(cameraId)
         )
         val fakeCameraDeviceSurfaceManager = FakeCameraDeviceSurfaceManager()
-        fakeCameraDeviceSurfaceManager.setSuggestedResolution(
+        fakeCameraDeviceSurfaceManager.setSuggestedStreamSpec(
             cameraId,
             FakeUseCaseConfig::class.java,
-            SURFACE_RESOLUTION
+            TEST_STREAM_SPEC
         )
         val useCaseConfigFactory: UseCaseConfigFactory = FakeUseCaseConfigFactory()
         return CameraUseCaseAdapter(
@@ -285,12 +299,15 @@
             notifyUpdated()
         }
 
-        override fun onSuggestedResolutionUpdated(suggestedResolution: Size): Size {
-            return suggestedResolution
+        override fun onSuggestedStreamSpecUpdated(suggestedStreamSpec: StreamSpec): StreamSpec {
+            return suggestedStreamSpec
         }
     }
 
     companion object {
         private val SURFACE_RESOLUTION: Size by lazy { Size(640, 480) }
+        private val TEST_STREAM_SPEC: StreamSpec by lazy {
+            StreamSpec.builder(SURFACE_RESOLUTION).build()
+        }
     }
 }
\ No newline at end of file
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/ViewPortsTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/ViewPortsTest.kt
index 37e533d..5a63cf5 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/ViewPortsTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/ViewPortsTest.kt
@@ -686,10 +686,10 @@
             // Arrange.
             // Convert the sizes into a UseCase map.
             val orderedUseCases: MutableList<UseCase> = ArrayList()
-            val useCaseSizeMap = HashMap<UseCase?, Size?>().apply {
+            val useCaseStreamSpecMap = HashMap<UseCase?, StreamSpec?>().apply {
                 for (size in surfaceSizes) {
                     val fakeUseCase = FakeUseCaseConfig.Builder().build()
-                    put(fakeUseCase, size)
+                    put(fakeUseCase, StreamSpec.builder(size).build())
                     orderedUseCases.add(fakeUseCase)
                 }
             }
@@ -702,7 +702,7 @@
                 rotationDegrees,
                 scaleType,
                 layoutDirection,
-                useCaseSizeMap
+                useCaseStreamSpecMap
             )
 
             // Assert.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java
index fd28040..58183ca 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java
@@ -81,7 +81,7 @@
      */
     @Retention(RetentionPolicy.SOURCE)
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @IntDef(flag = true, value = {PREVIEW, IMAGE_CAPTURE})
+    @IntDef(flag = true, value = {PREVIEW, VIDEO_CAPTURE, IMAGE_CAPTURE})
     public @interface Targets {
     }
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
index 0dac3fc..d41351c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
@@ -74,6 +74,7 @@
 import androidx.camera.core.impl.MutableOptionsBundle;
 import androidx.camera.core.impl.OptionsBundle;
 import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
 import androidx.camera.core.impl.utils.Threads;
@@ -302,8 +303,9 @@
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     SessionConfig.Builder createPipeline(@NonNull String cameraId,
-            @NonNull ImageAnalysisConfig config, @NonNull Size resolution) {
+            @NonNull ImageAnalysisConfig config, @NonNull StreamSpec streamSpec) {
         Threads.checkMainThread();
+        Size resolution = streamSpec.getResolution();
 
         Executor backgroundExecutor = Preconditions.checkNotNull(config.getBackgroundExecutor(
                 CameraXExecutors.highPriorityExecutor()));
@@ -387,7 +389,7 @@
             if (isCurrentCamera(cameraId)) {
                 // Only reset the pipeline when the bound camera is the same.
                 SessionConfig.Builder errorSessionConfigBuilder = createPipeline(cameraId, config,
-                        resolution);
+                        streamSpec);
                 updateSessionConfig(errorSessionConfigBuilder.build());
 
                 notifyReset();
@@ -726,14 +728,14 @@
     @Override
     @RestrictTo(Scope.LIBRARY_GROUP)
     @NonNull
-    protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
+    protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
         final ImageAnalysisConfig config = (ImageAnalysisConfig) getCurrentConfig();
 
         SessionConfig.Builder sessionConfigBuilder = createPipeline(getCameraId(), config,
-                suggestedResolution);
+                suggestedStreamSpec);
         updateSessionConfig(sessionConfigBuilder.build());
 
-        return suggestedResolution;
+        return suggestedStreamSpec;
     }
 
     /**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 2e91175..d5f8971 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -105,6 +105,7 @@
 import androidx.camera.core.impl.MutableOptionsBundle;
 import androidx.camera.core.impl.OptionsBundle;
 import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
 import androidx.camera.core.impl.utils.CameraOrientationUtil;
@@ -370,10 +371,10 @@
     @UiThread
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     SessionConfig.Builder createPipeline(@NonNull String cameraId,
-            @NonNull ImageCaptureConfig config, @NonNull Size resolution) {
+            @NonNull ImageCaptureConfig config, @NonNull StreamSpec streamSpec) {
         checkMainThread();
         if (isNodeEnabled()) {
-            return createPipelineWithNode(cameraId, config, resolution);
+            return createPipelineWithNode(cameraId, config, streamSpec);
         }
         SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
 
@@ -382,6 +383,7 @@
         }
 
         // Setup the ImageReader to do processing
+        Size resolution = streamSpec.getResolution();
         if (config.getImageReaderProxyProvider() != null) {
             mImageReader =
                     new SafeCloseImageReaderProxy(
@@ -450,7 +452,7 @@
             //  to this use case so we don't need to do this check.
             if (isCurrentCamera(cameraId)) {
                 // Only reset the pipeline when the bound camera is the same.
-                mSessionConfigBuilder = createPipeline(cameraId, config, resolution);
+                mSessionConfigBuilder = createPipeline(cameraId, config, streamSpec);
 
                 if (mImageCaptureRequestProcessor != null) {
                     // Restore the unfinished requests to the created pipeline
@@ -616,7 +618,7 @@
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
     @Override
-    protected void onCameraControlReady() {
+    public void onCameraControlReady() {
         trySetFlashModeToCameraControl();
     }
 
@@ -1527,16 +1529,16 @@
     @NonNull
     @Override
     @RestrictTo(Scope.LIBRARY_GROUP)
-    protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
+    protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
         mSessionConfigBuilder = createPipeline(getCameraId(),
-                (ImageCaptureConfig) getCurrentConfig(), suggestedResolution);
+                (ImageCaptureConfig) getCurrentConfig(), suggestedStreamSpec);
 
         updateSessionConfig(mSessionConfigBuilder.build());
 
         // In order to speed up the take picture process, notifyActive at an early stage to
         // attach the session capture callback to repeating and get capture result all the time.
         notifyActive();
-        return suggestedResolution;
+        return suggestedStreamSpec;
     }
 
     /**
@@ -1655,10 +1657,11 @@
     @OptIn(markerClass = ExperimentalZeroShutterLag.class)
     @MainThread
     private SessionConfig.Builder createPipelineWithNode(@NonNull String cameraId,
-            @NonNull ImageCaptureConfig config, @NonNull Size resolution) {
+            @NonNull ImageCaptureConfig config, @NonNull StreamSpec streamSpec) {
         checkMainThread();
-        Log.d(TAG, String.format("createPipelineWithNode(cameraId: %s, resolution: %s)",
-                cameraId, resolution));
+        Log.d(TAG, String.format("createPipelineWithNode(cameraId: %s, streamSpec: %s)",
+                cameraId, streamSpec));
+        Size resolution = streamSpec.getResolution();
 
         checkState(mImagePipeline == null);
         mImagePipeline = new ImagePipeline(config, resolution, mCameraEffect);
@@ -1679,7 +1682,7 @@
             if (isCurrentCamera(cameraId)) {
                 mTakePictureManager.pause();
                 clearPipelineWithNode(/*keepTakePictureManager=*/ true);
-                mSessionConfigBuilder = createPipeline(cameraId, config, resolution);
+                mSessionConfigBuilder = createPipeline(cameraId, config, streamSpec);
                 updateSessionConfig(mSessionConfigBuilder.build());
                 notifyReset();
                 mTakePictureManager.resume();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index 68c1bfa..c9de0b5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -78,6 +78,7 @@
 import androidx.camera.core.impl.OptionsBundle;
 import androidx.camera.core.impl.PreviewConfig;
 import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
@@ -209,11 +210,11 @@
     @MainThread
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     SessionConfig.Builder createPipeline(@NonNull String cameraId, @NonNull PreviewConfig config,
-            @NonNull Size resolution) {
+            @NonNull StreamSpec streamSpec) {
         // Build pipeline with node if processor is set. Eventually we will move all the code to
         // createPipelineWithNode.
         if (mSurfaceProcessor != null) {
-            return createPipelineWithNode(cameraId, config, resolution);
+            return createPipelineWithNode(cameraId, config, streamSpec);
         }
 
         checkMainThread();
@@ -222,8 +223,8 @@
         // Close previous session's deferrable surface before creating new one
         clearPipeline();
 
-        final SurfaceRequest surfaceRequest = new SurfaceRequest(resolution, getCamera(),
-                this::notifyReset);
+        final SurfaceRequest surfaceRequest = new SurfaceRequest(streamSpec.getResolution(),
+                getCamera(), this::notifyReset);
         mCurrentSurfaceRequest = surfaceRequest;
 
         if (mSurfaceProvider != null) {
@@ -232,7 +233,7 @@
         }
 
         mSessionDeferrableSurface = surfaceRequest.getDeferrableSurface();
-        addCameraSurfaceAndErrorListener(sessionConfigBuilder, cameraId, config, resolution);
+        addCameraSurfaceAndErrorListener(sessionConfigBuilder, cameraId, config, streamSpec);
         return sessionConfigBuilder;
     }
 
@@ -247,7 +248,7 @@
     private SessionConfig.Builder createPipelineWithNode(
             @NonNull String cameraId,
             @NonNull PreviewConfig config,
-            @NonNull Size resolution) {
+            @NonNull StreamSpec streamSpec) {
         // Check arguments
         checkMainThread();
         checkNotNull(mSurfaceProcessor);
@@ -262,12 +263,12 @@
         checkState(mCameraEdge == null);
         mCameraEdge = new SurfaceEdge(
                 PREVIEW,
-                resolution,
+                streamSpec,
                 new Matrix(),
-                /*hasCameraTransform=*/true,
-                requireNonNull(getCropRect(resolution)),
+                getHasCameraTransform(),
+                requireNonNull(getCropRect(streamSpec.getResolution())),
                 getRelativeRotation(camera),
-                /*mirroring=*/isFrontCamera(camera));
+                shouldMirror());
         mCameraEdge.addOnInvalidatedListener(this::notifyReset);
         SurfaceProcessorNode.OutConfig outConfig = SurfaceProcessorNode.OutConfig.of(mCameraEdge);
         SurfaceProcessorNode.In nodeInput = SurfaceProcessorNode.In.of(mCameraEdge,
@@ -286,7 +287,7 @@
 
         // Send the camera Surface to the camera2.
         SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
-        addCameraSurfaceAndErrorListener(sessionConfigBuilder, cameraId, config, resolution);
+        addCameraSurfaceAndErrorListener(sessionConfigBuilder, cameraId, config, streamSpec);
         return sessionConfigBuilder;
     }
 
@@ -300,9 +301,13 @@
         }
     }
 
-    private static boolean isFrontCamera(@NonNull CameraInternal camera) {
-        Integer lensFacing = camera.getCameraInfoInternal().getLensFacing();
-        return lensFacing != null && lensFacing == CameraSelector.LENS_FACING_FRONT;
+    private boolean shouldMirror() {
+        boolean isFrontCamera = requireNonNull(getCamera()).getCameraInfoInternal().getLensFacing()
+                == CameraSelector.LENS_FACING_FRONT;
+        // Since PreviewView cannot mirror, we will always mirror preview stream during buffer
+        // copy. If there has been a buffer copy, it means it's already mirrored. Otherwise,
+        // mirror it for the front camera.
+        return getHasCameraTransform() && isFrontCamera;
     }
 
     /**
@@ -356,7 +361,7 @@
             @NonNull SessionConfig.Builder sessionConfigBuilder,
             @NonNull String cameraId,
             @NonNull PreviewConfig config,
-            @NonNull Size resolution) {
+            @NonNull StreamSpec streamSpec) {
         // TODO(b/245309800): Add the Surface if post-processing pipeline is used. Post-processing
         //  pipeline always provide a Surface.
 
@@ -375,7 +380,7 @@
             if (isCurrentCamera(cameraId)) {
                 // Only reset the pipeline when the bound camera is the same.
                 SessionConfig.Builder sessionConfigBuilder1 = createPipeline(cameraId, config,
-                        resolution);
+                        streamSpec);
 
                 updateSessionConfig(sessionConfigBuilder1.build());
                 notifyReset();
@@ -424,7 +429,7 @@
                         cropRect,
                         getRelativeRotation(cameraInternal),
                         getAppTargetRotation(),
-                        /*hasCameraTransform=*/true));
+                        getHasCameraTransform()));
             } else {
                 mCameraEdge.setRotationDegrees(getRelativeRotation(cameraInternal));
             }
@@ -479,7 +484,7 @@
             // config and let createPipeline() sends a new SurfaceRequest.
             if (getAttachedSurfaceResolution() != null) {
                 updateConfigAndOutput(getCameraId(), (PreviewConfig) getCurrentConfig(),
-                        getAttachedSurfaceResolution());
+                        getAttachedStreamSpec());
                 notifyReset();
             }
         }
@@ -511,8 +516,8 @@
     }
 
     private void updateConfigAndOutput(@NonNull String cameraId, @NonNull PreviewConfig config,
-            @NonNull Size resolution) {
-        updateSessionConfig(createPipeline(cameraId, config, resolution).build());
+            @NonNull StreamSpec streamSpec) {
+        updateSessionConfig(createPipeline(cameraId, config, streamSpec).build());
     }
 
     /**
@@ -643,11 +648,11 @@
     @Override
     @RestrictTo(Scope.LIBRARY_GROUP)
     @NonNull
-    protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
-        mSurfaceSize = suggestedResolution;
+    protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
+        mSurfaceSize = suggestedStreamSpec.getResolution();
         updateConfigAndOutput(getCameraId(), (PreviewConfig) getCurrentConfig(),
-                mSurfaceSize);
-        return suggestedResolution;
+                suggestedStreamSpec);
+        return suggestedStreamSpec;
     }
 
     /**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index 613a40c..e37d862 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -19,6 +19,7 @@
 import android.annotation.SuppressLint;
 import android.graphics.Matrix;
 import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
 import android.media.ImageReader;
 import android.util.Size;
 import android.view.Surface;
@@ -40,10 +41,12 @@
 import androidx.camera.core.impl.ImageOutputConfig;
 import androidx.camera.core.impl.MutableOptionsBundle;
 import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
 import androidx.camera.core.internal.TargetConfig;
 import androidx.camera.core.internal.utils.UseCaseConfigUtil;
+import androidx.camera.core.streamsharing.StreamSharing;
 import androidx.core.util.Preconditions;
 import androidx.lifecycle.LifecycleOwner;
 
@@ -105,9 +108,9 @@
     ////////////////////////////////////////////////////////////////////////////////////////////
 
     /**
-     * The resolution assigned to the {@link UseCase} based on the attached camera.
+     * The {@link StreamSpec} assigned to the {@link UseCase} based on the attached camera.
      */
-    private Size mAttachedResolution;
+    private StreamSpec mAttachedStreamSpec;
 
     /**
      * The camera implementation provided Config. Its options has lowest priority and will be
@@ -123,6 +126,17 @@
     private Rect mViewPortCropRect;
 
     /**
+     * Whether the producer writes camera transform to the {@link Surface}.
+     *
+     * <p> Camera2 writes the camera transform to the {@link Surface}, which can be used to
+     * correct the output. However, if the producer is not the camera, for example, a OpenGL
+     * renderer in {@link StreamSharing}, then this field will be false.
+     *
+     * @see SurfaceTexture#getTransformMatrix
+     */
+    private boolean mHasCameraTransform = true;
+
+    /**
      * The sensor to image buffer transform matrix.
      */
     @NonNull
@@ -155,7 +169,7 @@
      * Retrieve the default {@link UseCaseConfig} for the UseCase.
      *
      * @param applyDefaultConfig true if this is the base config applied to a UseCase.
-     * @param factory the factory that contains the default UseCases.
+     * @param factory            the factory that contains the default UseCases.
      * @return The UseCaseConfig or null if there is no default Config.
      * @hide
      */
@@ -181,9 +195,8 @@
      * @param extendedConfig      configs that take priority over the UseCase's default config
      * @param cameraDefaultConfig configs that have lower priority than the UseCase's default.
      *                            This Config comes from the camera implementation.
-     *
      * @throws IllegalArgumentException if there exists conflicts in the merged config that can
-     * not be resolved
+     *                                  not be resolved
      * @hide
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
@@ -205,7 +218,7 @@
         // over all options.
         for (Option<?> opt : mUseCaseConfig.listOptions()) {
             @SuppressWarnings("unchecked") // Options/values are being copied directly
-                    Option<Object> objectOpt = (Option<Object>) opt;
+            Option<Object> objectOpt = (Option<Object>) opt;
 
             mergedConfig.insertOption(objectOpt,
                     mUseCaseConfig.getOptionPriority(opt),
@@ -217,7 +230,7 @@
             // just copy over all options.
             for (Option<?> opt : extendedConfig.listOptions()) {
                 @SuppressWarnings("unchecked") // Options/values are being copied directly
-                        Option<Object> objectOpt = (Option<Object>) opt;
+                Option<Object> objectOpt = (Option<Object>) opt;
                 if (objectOpt.getId().equals(TargetConfig.OPTION_TARGET_NAME.getId())) {
                     continue;
                 }
@@ -257,7 +270,7 @@
      *                   resolution
      * @return the conflict resolved config
      * @throws IllegalArgumentException if there exists conflicts in the merged config that can
-     * not be resolved
+     *                                  not be resolved
      * @hide
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
@@ -524,17 +537,29 @@
     @RestrictTo(Scope.LIBRARY_GROUP)
     @Nullable
     public Size getAttachedSurfaceResolution() {
-        return mAttachedResolution;
+        return mAttachedStreamSpec != null ? mAttachedStreamSpec.getResolution() : null;
     }
 
     /**
-     * Offers suggested resolution for the UseCase.
+     * Retrieves the currently attached stream specification.
+     *
+     * @return the currently attached stream specification.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public StreamSpec getAttachedStreamSpec() {
+        return mAttachedStreamSpec;
+    }
+
+    /**
+     * Offers suggested stream specification for the UseCase.
      *
      * @hide
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
-    public void updateSuggestedResolution(@NonNull Size suggestedResolution) {
-        mAttachedResolution = onSuggestedResolutionUpdated(suggestedResolution);
+    public void updateSuggestedStreamSpec(@NonNull StreamSpec suggestedStreamSpec) {
+        mAttachedStreamSpec = onSuggestedStreamSpecUpdated(suggestedStreamSpec);
     }
 
     /**
@@ -542,18 +567,18 @@
      * CameraSelector, UseCase...)}.
      *
      * <p>Override to create necessary objects like {@link ImageReader} depending
-     * on the resolution.
+     * on the stream specification.
      *
-     * @param suggestedResolution The suggested resolution that depends on camera device
+     * @param suggestedStreamSpec The suggested stream specification that depends on camera device
      *                            capability and what and how many use cases will be bound.
-     * @return The resolution that finally used to create the SessionConfig to
+     * @return The stream specification that finally used to create the SessionConfig to
      * attach to the camera device.
      * @hide
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
     @NonNull
-    protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
-        return suggestedResolution;
+    protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
+        return suggestedStreamSpec;
     }
 
     /**
@@ -564,7 +589,7 @@
      * @hide
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
-    protected void onCameraControlReady() {
+    public void onCameraControlReady() {
     }
 
     /**
@@ -621,7 +646,7 @@
      * make the use case work correctly.
      *
      * <p>After this function is invoked, CameraX will also provide the selected resolution
-     * information to subclasses via {@link #onSuggestedResolutionUpdated}. Subclasses should
+     * information to subclasses via {@link #onSuggestedStreamSpecUpdated}. Subclasses should
      * override it to set up the pipeline according to the selected resolution, so that UseCase
      * becomes ready to receive data from the camera.
      *
@@ -665,7 +690,7 @@
             mCamera = null;
         }
 
-        mAttachedResolution = null;
+        mAttachedStreamSpec = null;
         mViewPortCropRect = null;
 
         // Resets the mUseCaseConfig to the initial status when the use case was created to make
@@ -695,15 +720,11 @@
      * session with the use case session config. The use case can receive the frame data from the
      * camera after the capture session is configured.
      *
-     * <p>The {@link CameraControlInternal} retrieved by {@link #getCameraControl()} is ready to
-     * use after this callback function is invoked.
-     *
      * @hide
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
     @CallSuper
     public void onStateAttached() {
-        onCameraControlReady();
     }
 
     /**
@@ -747,6 +768,30 @@
     }
 
     /**
+     * Sets whether the producer writes camera transform to the {@link Surface}.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @CallSuper
+    public void setHasCameraTransform(boolean hasCameraTransform) {
+        mHasCameraTransform = hasCameraTransform;
+    }
+
+    /**
+     * Gets whether the producer writes camera transform to the {@link Surface}.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @CallSuper
+    public boolean getHasCameraTransform() {
+        return mHasCameraTransform;
+    }
+
+
+
+    /**
      * Gets the view port crop rect.
      *
      * @hide
@@ -799,7 +844,7 @@
      *
      * @return the resolution information if the use case has been bound by the
      * {@link androidx.camera.lifecycle.ProcessCameraProvider#bindToLifecycle(LifecycleOwner
-     * , CameraSelector, UseCase...)} API, or null if the use case is not bound yet.
+     *, CameraSelector, UseCase...)} API, or null if the use case is not bound yet.
      * @hide
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/AttachedSurfaceInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/AttachedSurfaceInfo.java
index 3fb6ad5..9e6c442 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/AttachedSurfaceInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/AttachedSurfaceInfo.java
@@ -61,7 +61,6 @@
     /** Returns the configuration target frame rate. */
     @Nullable
     public abstract Range<Integer> getTargetFrameRate();
-
 }
 
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
index c77004a..5bacbf2 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
@@ -78,22 +78,22 @@
             @NonNull Size size);
 
     /**
-     * Retrieves a map of suggested resolutions for the given list of use cases.
+     * Retrieves a map of suggested stream specifications for the given list of use cases.
      *
      * @param cameraId          the camera id of the camera device used by the use cases
      * @param existingSurfaces  list of surfaces already configured and used by the camera. The
-     *                          resolutions for these surface can not change.
+     *                          stream specifications for these surface can not change.
      * @param newUseCaseConfigs list of configurations of the use cases that will be given a
-     *                          suggested resolution
-     * @return map of suggested resolutions for given use cases
-     *
+     *                          suggested stream specification
+     * @return map of suggested stream specifications for given use cases
      * @throws IllegalStateException    if not initialized
      * @throws IllegalArgumentException if {@code newUseCaseConfigs} is an empty list, if
-     *      there isn't a supported combination of surfaces available, or if the {@code cameraId}
-     *      is not a valid id.
+     *                                  there isn't a supported combination of surfaces
+     *                                  available, or if the {@code cameraId}
+     *                                  is not a valid id.
      */
     @NonNull
-    Map<UseCaseConfig<?>, Size> getSuggestedResolutions(
+    Map<UseCaseConfig<?>, StreamSpec> getSuggestedStreamSpecs(
             @NonNull String cameraId,
             @NonNull List<AttachedSurfaceInfo> existingSurfaces,
             @NonNull List<UseCaseConfig<?>> newUseCaseConfigs);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/StreamSpec.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/StreamSpec.java
new file mode 100644
index 0000000..932e71a
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/StreamSpec.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl;
+
+import android.util.Range;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * A stream specification defining how a camera frame stream should be configured.
+ *
+ * <p>The values communicated by this class specify what the camera is expecting to produce as a
+ * frame stream, and can be useful for configuring the frame consumer.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@AutoValue
+public abstract class StreamSpec {
+
+    /** A frame rate range with no specified upper or lower bound. */
+    public static final Range<Integer> FRAME_RATE_RANGE_UNSPECIFIED = new Range<>(0, 0);
+
+    /**
+     * Returns the resolution for the stream associated with this stream specification.
+     * @return the resolution for the stream.
+     */
+    @NonNull
+    public abstract Size getResolution();
+
+    /**
+     * Returns the expected frame rate range for the stream associated with this stream
+     * specification.
+     * @return the expected frame rate range for the stream.
+     */
+    @NonNull
+    public abstract Range<Integer> getExpectedFrameRateRange();
+
+    /** Returns a build for a stream configuration that takes a required resolution. */
+    @NonNull
+    public static Builder builder(@NonNull Size resolution) {
+        return new AutoValue_StreamSpec.Builder()
+                .setResolution(resolution)
+                .setExpectedFrameRateRange(FRAME_RATE_RANGE_UNSPECIFIED);
+    }
+
+    /** Returns a builder pre-populated with the current specification. */
+    @NonNull
+    public abstract Builder toBuilder();
+
+    /** A builder for a stream specification */
+    @AutoValue.Builder
+    public abstract static class Builder {
+        // Restrict construction to same package
+        Builder() {
+        }
+
+        /** Sets the resolution, overriding the existing resolution set in this builder. */
+        @NonNull
+        public abstract Builder setResolution(@NonNull Size resolution);
+
+        /**
+         * Sets the expected frame rate range.
+         *
+         * <p>If not set, the default expected frame rate range is
+         * {@link #FRAME_RATE_RANGE_UNSPECIFIED}.
+         */
+        @NonNull
+        public abstract Builder setExpectedFrameRateRange(@NonNull Range<Integer> range);
+
+        /** Builds the stream specification */
+        @NonNull
+        public abstract StreamSpec build();
+    }
+
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
index f97ec40..45d12bd 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
@@ -16,9 +16,13 @@
 
 package androidx.camera.core.internal;
 
+import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+import static androidx.core.util.Preconditions.checkArgument;
+
 import static java.util.Collections.emptyList;
 import static java.util.Objects.requireNonNull;
 
+import android.graphics.ImageFormat;
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -49,20 +53,24 @@
 import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.SurfaceConfig;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.processing.SurfaceProcessorWithExecutor;
+import androidx.camera.core.streamsharing.StreamSharing;
 import androidx.core.util.Preconditions;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * A {@link CameraInternal} adapter which checks that the UseCases to make sure that the resolutions
@@ -76,19 +84,22 @@
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public final class CameraUseCaseAdapter implements Camera {
     @NonNull
-    private CameraInternal mCameraInternal;
+    private final CameraInternal mCameraInternal;
     private final LinkedHashSet<CameraInternal> mCameraInternals;
     private final CameraDeviceSurfaceManager mCameraDeviceSurfaceManager;
     private final UseCaseConfigFactory mUseCaseConfigFactory;
 
     private static final String TAG = "CameraUseCaseAdapter";
+    private boolean mStreamSharingEnabled = false;
 
     private final CameraId mId;
 
-    // This includes app provided use cases and the extra placeholder use cases (mExtraUseCases)
-    // created by CameraX.
+    // UseCases from the app. This does not include internal UseCases created by CameraX.
     @GuardedBy("mLock")
-    private final List<UseCase> mUseCases = new ArrayList<>();
+    private final Set<UseCase> mUseCases = new HashSet<>();
+    // UseCases sent to the camera including internal UseCases created by CameraX.
+    @GuardedBy("mLock")
+    private final Set<UseCase> mCameraUseCases = new HashSet<>();
 
     @GuardedBy("mLock")
     @Nullable
@@ -114,9 +125,16 @@
     @GuardedBy("mLock")
     private Config mInteropConfig = null;
 
-    // The extra placeholder use cases created by CameraX to make sure the camera can work normally.
+    // The placeholder UseCase created to meet combination criteria for Extensions. e.g. When
+    // Extensions require both Preview and ImageCapture and app only provides one of them,
+    // CameraX will create the other and track it with this variable.
     @GuardedBy("mLock")
-    private List<UseCase> mExtraUseCases = new ArrayList<>();
+    @Nullable
+    private UseCase mPlaceholderForExtensions;
+    // Current StreamSharing parent UseCase if exists.
+    @GuardedBy("mLock")
+    @Nullable
+    private StreamSharing mStreamSharing;
 
     /**
      * Create a new {@link CameraUseCaseAdapter} instance.
@@ -186,123 +204,190 @@
      * @throws CameraException Thrown if the combination of newly added UseCases and the
      *                         currently added UseCases exceed the capability of the camera.
      */
-    public void addUseCases(@NonNull Collection<UseCase> useCases) throws CameraException {
+    public void addUseCases(@NonNull Collection<UseCase> appUseCasesToAdd) throws CameraException {
         synchronized (mLock) {
-            // TODO: merge UseCase for stream sharing. e.g. replace Preview and VideoCapture with a
-            //  StreamSharing UseCase.
-            List<UseCase> newUseCases = new ArrayList<>();
-            for (UseCase useCase : useCases) {
-                if (mUseCases.contains(useCase)) {
-                    Logger.d(TAG, "Attempting to attach already attached UseCase");
-                } else {
-                    newUseCases.add(useCase);
-                }
-            }
-
-            List<UseCase> allUseCases = new ArrayList<>(mUseCases);
-            List<UseCase> requiredExtraUseCases = emptyList();
-            List<UseCase> removedExtraUseCases = emptyList();
-
-            if (isCoexistingPreviewImageCaptureRequired()) {
-                // Collects all use cases that will be finally bound by the application
-                allUseCases.removeAll(mExtraUseCases);
-                allUseCases.addAll(newUseCases);
-
-                // Calculates the required extra use cases according to the use cases finally bound
-                // by the application and the existing extra use cases.
-                requiredExtraUseCases = calculateRequiredExtraUseCases(allUseCases,
-                        new ArrayList<>(mExtraUseCases));
-
-                // Calculates the new added extra use cases
-                List<UseCase> addedExtraUseCases = new ArrayList<>(requiredExtraUseCases);
-                addedExtraUseCases.removeAll(mExtraUseCases);
-
-                // Adds the new added extra use cases to the newUseCases list
-                newUseCases.addAll(addedExtraUseCases);
-
-                // Calculates the removed extra use cases
-                removedExtraUseCases = new ArrayList<>(mExtraUseCases);
-                removedExtraUseCases.removeAll(requiredExtraUseCases);
-            }
-
-            Map<UseCase, ConfigPair> configs = getConfigs(newUseCases,
-                    mCameraConfig.getUseCaseConfigFactory(), mUseCaseConfigFactory);
-
-            Map<UseCase, Size> suggestedResolutionsMap;
+            Set<UseCase> appUseCasesAfter = new HashSet<>(mUseCases);
+            appUseCasesAfter.addAll(appUseCasesToAdd);
             try {
-                // Removes the unnecessary extra use cases and then checks whether all uses cases
-                // including all the use cases finally bound by the application and the needed
-                // extra use cases can be supported by guaranteed supported configurations tables.
-                List<UseCase> boundUseCases = new ArrayList<>(mUseCases);
-                boundUseCases.removeAll(removedExtraUseCases);
-                suggestedResolutionsMap =
-                        calculateSuggestedResolutions(mCameraInternal.getCameraInfoInternal(),
-                                newUseCases, boundUseCases, configs);
+                updateUseCases(appUseCasesAfter);
             } catch (IllegalArgumentException e) {
                 throw new CameraException(e.getMessage());
             }
-            updateViewPort(suggestedResolutionsMap, useCases);
-            updateEffects(mEffects, useCases);
-
-            // Saves the updated extra use cases set after confirming the use case combination
-            // can be supported.
-            mExtraUseCases = requiredExtraUseCases;
-
-            // Detaches the unnecessary existing extra use cases
-            detachUnnecessaryUseCases(removedExtraUseCases);
-
-            // At this point the binding will succeed since all the calculations are done
-            // Do all attaching related work
-            for (UseCase useCase : newUseCases) {
-                ConfigPair configPair = configs.get(useCase);
-                useCase.bindToCamera(mCameraInternal, configPair.mExtendedConfig,
-                        configPair.mCameraConfig);
-                useCase.updateSuggestedResolution(
-                        Preconditions.checkNotNull(suggestedResolutionsMap.get(useCase)));
-            }
-
-            // The added use cases will include the app provided use cases and the new added extra
-            // use cases.
-            mUseCases.addAll(newUseCases);
-            if (mAttached) {
-                mCameraInternal.attachUseCases(newUseCases);
-            }
-
-            // Once all use cases are attached, they need to notify the CameraInternal of its state
-            for (UseCase useCase : newUseCases) {
-                useCase.notifyState();
-            }
         }
     }
 
     /**
      * Remove the specified collection of {@link UseCase} from the adapter.
      */
-    public void removeUseCases(@NonNull Collection<UseCase> useCases) {
+    public void removeUseCases(@NonNull Collection<UseCase> useCasesToRemove) {
         synchronized (mLock) {
-            detachUnnecessaryUseCases(new ArrayList<>(useCases));
+            Set<UseCase> appUseCasesAfter = new HashSet<>(mUseCases);
+            appUseCasesAfter.removeAll(useCasesToRemove);
+            updateUseCases(appUseCasesAfter);
+        }
+    }
 
-            // Calls addUseCases() function to calculate and add extra use cases if coexisting
-            // Preview and ImageCapture are required.
-            if (isCoexistingPreviewImageCaptureRequired()) {
-                // The useCases might include extra use cases when unbinding all use cases.
-                // Removes the unbound extra use cases from mExtraUseCases.
-                mExtraUseCases.removeAll(useCases);
+    /**
+     * Updates the states based the new app UseCases.
+     */
+    void updateUseCases(@NonNull Set<UseCase> appUseCases) {
+        // TODO(b/265820449): set applyStreamSharing to true if Effects requires it..
+        updateUseCases(appUseCases, /*applyStreamSharing*/false);
+    }
 
-                try {
-                    // Calls addUseCases with empty list to add required extra fake use case.
-                    addUseCases(emptyList());
-                } catch (CameraException e) {
-                    // This should not happen because the extra fake use case should be only
-                    // added to replace the removed one which the use case combination can be
-                    // supported.
-                    throw new IllegalArgumentException("Failed to add extra fake Preview or "
-                            + "ImageCapture use case!");
+    /**
+     * Updates the states based the new app UseCases.
+     *
+     * <p> This method calculates the new camera UseCases based on the input and the current state,
+     * attach/detach the camera UseCases, and save the updated state in following member variables:
+     * {@link #mCameraUseCases}, {@link #mUseCases} and {@link #mPlaceholderForExtensions}.
+     *
+     * @throws IllegalArgumentException if the UseCase combination is not supported. In that case,
+     *                                  it will not update the internal states.
+     */
+    void updateUseCases(@NonNull Set<UseCase> appUseCases, boolean applyStreamSharing) {
+        synchronized (mLock) {
+            // Calculate camera UseCases and keep the result in local variables in case they don't
+            // meet the stream combination rules.
+            UseCase placeholderForExtensions = calculatePlaceholderForExtensions(appUseCases);
+            StreamSharing streamSharing = applyStreamSharing
+                    ? createOrReuseStreamSharing(appUseCases) : null;
+            Set<UseCase> cameraUseCases =
+                    calculateCameraUseCases(appUseCases, placeholderForExtensions, streamSharing);
+
+            // Calculate the action items.
+            Set<UseCase> cameraUseCasesToAttach = new HashSet<>(cameraUseCases);
+            cameraUseCasesToAttach.removeAll(mCameraUseCases);
+            Set<UseCase> cameraUseCasesToKeep = new HashSet<>(cameraUseCases);
+            cameraUseCasesToKeep.retainAll(mCameraUseCases);
+            Set<UseCase> cameraUseCasesToDetach = new HashSet<>(mCameraUseCases);
+            cameraUseCasesToDetach.removeAll(cameraUseCases);
+
+            // Calculate suggested resolutions. This step throws exception if the camera UseCases
+            // fails the supported stream combination rules.
+            Map<UseCase, ConfigPair> configs = getConfigs(cameraUseCasesToAttach,
+                    mCameraConfig.getUseCaseConfigFactory(), mUseCaseConfigFactory);
+
+            Map<UseCase, StreamSpec> suggestedStreamSpecMap;
+            try {
+                suggestedStreamSpecMap = calculateSuggestedStreamSpecs(
+                        mCameraInternal.getCameraInfoInternal(), cameraUseCasesToAttach,
+                        cameraUseCasesToKeep, configs);
+                // TODO(b/265704882): enable stream sharing for LEVEL_3 and high preview
+                //  resolution. Throw exception here if (applyStreamSharing == false), both video
+                //  and preview are used and preview resolution is lower than user configuration.
+            } catch (IllegalArgumentException exception) {
+                if (!applyStreamSharing && mStreamSharingEnabled && hasNoExtension()) {
+                    // Try again and see if StreamSharing resolves the issue.
+                    updateUseCases(appUseCases, /*applyStreamSharing*/true);
+                    return;
+                } else {
+                    // If StreamSharing already on or not enabled, throw exception.
+                    throw exception;
                 }
             }
+
+            // Update properties.
+            updateViewPort(suggestedStreamSpecMap, cameraUseCases);
+            updateEffects(mEffects, appUseCases);
+
+            // Detach unused UseCases.
+            for (UseCase useCase : cameraUseCasesToDetach) {
+                useCase.unbindFromCamera(mCameraInternal);
+            }
+            mCameraInternal.detachUseCases(cameraUseCasesToDetach);
+
+            // Attach new UseCases.
+            for (UseCase useCase : cameraUseCasesToAttach) {
+                ConfigPair configPair = requireNonNull(configs.get(useCase));
+                useCase.bindToCamera(mCameraInternal, configPair.mExtendedConfig,
+                        configPair.mCameraConfig);
+                useCase.updateSuggestedStreamSpec(
+                        Preconditions.checkNotNull(suggestedStreamSpecMap.get(useCase)));
+            }
+            if (mAttached) {
+                mCameraInternal.attachUseCases(cameraUseCasesToAttach);
+            }
+
+            // Once UseCases are detached/attached, notify the camera.
+            for (UseCase useCase : cameraUseCasesToAttach) {
+                useCase.notifyState();
+            }
+
+            // The changes are successful. Update the states of this class.
+            mUseCases.clear();
+            mUseCases.addAll(appUseCases);
+            mCameraUseCases.clear();
+            mCameraUseCases.addAll(cameraUseCases);
+            mPlaceholderForExtensions = placeholderForExtensions;
+            mStreamSharing = streamSharing;
         }
     }
 
+    private boolean hasNoExtension() {
+        synchronized (mLock) {
+            return mCameraConfig == CameraConfigs.emptyConfig();
+        }
+    }
+
+    /**
+     * Returns {@link UseCase}s qualified for {@link StreamSharing}.
+     */
+    @NonNull
+    private Set<UseCase> getStreamSharingChildren(@NonNull Set<UseCase> appUseCases) {
+        Set<UseCase> useCases = new HashSet<>();
+        for (UseCase useCase : appUseCases) {
+            checkArgument(!isStreamSharing(useCase), "Only support one level of sharing for now.");
+            if (isPrivateInputFormat(useCase)) {
+                // Add UseCase if the input format is PRIVATE(Preview and VideoCapture).
+                useCases.add(useCase);
+            }
+        }
+        return useCases;
+    }
+
+    /**
+     * Creates a new {@link StreamSharing} or returns the existing one.
+     *
+     * <p> Returns the existing {@link StreamSharing} if the children have not changed.
+     * Otherwise, create a new {@link StreamSharing} and return.
+     *
+     * <p> Currently, only {@link UseCase} with {@link ImageFormat#PRIVATE} can be
+     * {@link StreamSharing} children({@link Preview} and VideoCapture).
+     */
+    @Nullable
+    private StreamSharing createOrReuseStreamSharing(@NonNull Set<UseCase> appUseCases) {
+        synchronized (mLock) {
+            Set<UseCase> newChildren = getStreamSharingChildren(appUseCases);
+            if (newChildren.size() < 2) {
+                // No need to share the stream for 1 or less children.
+                return null;
+            }
+            if (mStreamSharing != null && mStreamSharing.getChildren().equals(newChildren)) {
+                // Returns the current instance if the new children equals the old.
+                return requireNonNull(mStreamSharing);
+            }
+            return new StreamSharing(mCameraInternal, newChildren, mUseCaseConfigFactory);
+        }
+    }
+
+    /**
+     * Returns {@link UseCase} that connects to the camera.
+     */
+    static Set<UseCase> calculateCameraUseCases(@NonNull Set<UseCase> appUseCases,
+            @Nullable UseCase placeholderForExtensions,
+            @Nullable StreamSharing streamSharing) {
+        Set<UseCase> useCases = new HashSet<>(appUseCases);
+        if (placeholderForExtensions != null) {
+            useCases.add(placeholderForExtensions);
+        }
+        if (streamSharing != null) {
+            useCases.add(streamSharing);
+            useCases.removeAll(streamSharing.getChildren());
+        }
+        return useCases;
+    }
+
     /**
      * Returns the UseCases currently associated with the adapter.
      *
@@ -316,6 +401,14 @@
         }
     }
 
+    @VisibleForTesting
+    @NonNull
+    Set<UseCase> getCameraUseCases() {
+        synchronized (mLock) {
+            return new HashSet<>(mCameraUseCases);
+        }
+    }
+
     /**
      * Attach the UseCases to the {@link CameraInternal} camera so that the UseCases can receive
      * data if they are active.
@@ -393,14 +486,14 @@
         }
     }
 
-    private Map<UseCase, Size> calculateSuggestedResolutions(
+    private Map<UseCase, StreamSpec> calculateSuggestedStreamSpecs(
             @NonNull CameraInfoInternal cameraInfoInternal,
-            @NonNull List<UseCase> newUseCases,
-            @NonNull List<UseCase> currentUseCases,
+            @NonNull Collection<UseCase> newUseCases,
+            @NonNull Collection<UseCase> currentUseCases,
             @NonNull Map<UseCase, ConfigPair> configPairMap) {
         List<AttachedSurfaceInfo> existingSurfaces = new ArrayList<>();
         String cameraId = cameraInfoInternal.getCameraId();
-        Map<UseCase, Size> suggestedResolutions = new HashMap<>();
+        Map<UseCase, StreamSpec> suggestedStreamSpecs = new HashMap<>();
 
         // Get resolution for current use cases.
         for (UseCase useCase : currentUseCases) {
@@ -411,7 +504,7 @@
             existingSurfaces.add(AttachedSurfaceInfo.create(surfaceConfig,
                     useCase.getImageFormat(), useCase.getAttachedSurfaceResolution(),
                     useCase.getCurrentConfig().getTargetFramerate(null)));
-            suggestedResolutions.put(useCase, useCase.getAttachedSurfaceResolution());
+            suggestedStreamSpecs.put(useCase, useCase.getAttachedStreamSpec());
         }
 
         // Calculate resolution for new use cases.
@@ -426,17 +519,17 @@
                 configToUseCaseMap.put(combinedUseCaseConfig, useCase);
             }
 
-            // Get suggested resolutions and update the use case session configuration
-            Map<UseCaseConfig<?>, Size> useCaseConfigSizeMap = mCameraDeviceSurfaceManager
-                    .getSuggestedResolutions(cameraId, existingSurfaces,
+            // Get suggested stream specifications and update the use case session configuration
+            Map<UseCaseConfig<?>, StreamSpec> useCaseConfigStreamSpecMap =
+                    mCameraDeviceSurfaceManager.getSuggestedStreamSpecs(cameraId, existingSurfaces,
                             new ArrayList<>(configToUseCaseMap.keySet()));
 
             for (Map.Entry<UseCaseConfig<?>, UseCase> entry : configToUseCaseMap.entrySet()) {
-                suggestedResolutions.put(entry.getValue(),
-                        useCaseConfigSizeMap.get(entry.getKey()));
+                suggestedStreamSpecs.put(entry.getValue(),
+                        useCaseConfigStreamSpecMap.get(entry.getKey()));
             }
         }
-        return suggestedResolutions;
+        return suggestedStreamSpecs;
     }
 
     @VisibleForTesting
@@ -466,7 +559,7 @@
         }
     }
 
-    private void updateViewPort(@NonNull Map<UseCase, Size> suggestedResolutionsMap,
+    private void updateViewPort(@NonNull Map<UseCase, StreamSpec> suggestedStreamSpecMap,
             @NonNull Collection<UseCase> useCases) {
         synchronized (mLock) {
             if (mViewPort != null) {
@@ -490,14 +583,15 @@
                                 mViewPort.getRotation()),
                         mViewPort.getScaleType(),
                         mViewPort.getLayoutDirection(),
-                        suggestedResolutionsMap);
+                        suggestedStreamSpecMap);
                 for (UseCase useCase : useCases) {
                     useCase.setViewPortCropRect(
                             Preconditions.checkNotNull(cropRectMap.get(useCase)));
                     useCase.setSensorToBufferTransformMatrix(
                             calculateSensorToBufferTransformMatrix(
                                     mCameraInternal.getCameraControlInternal().getSensorRect(),
-                                    suggestedResolutionsMap.get(useCase)));
+                                    Preconditions.checkNotNull(
+                                            suggestedStreamSpecMap.get(useCase)).getResolution()));
                 }
             }
         }
@@ -507,7 +601,7 @@
     private static Matrix calculateSensorToBufferTransformMatrix(
             @NonNull Rect fullSensorRect,
             @NonNull Size useCaseSize) {
-        Preconditions.checkArgument(
+        checkArgument(
                 fullSensorRect.width() > 0 && fullSensorRect.height() > 0,
                 "Cannot compute viewport crop rects zero sized sensor rect.");
         RectF fullSensorRectF = new RectF(fullSensorRect);
@@ -533,7 +627,7 @@
     }
 
     // Get a map of the configs for the use cases from the respective factories
-    private Map<UseCase, ConfigPair> getConfigs(List<UseCase> useCases,
+    private Map<UseCase, ConfigPair> getConfigs(Collection<UseCase> useCases,
             UseCaseConfigFactory extendedFactory, UseCaseConfigFactory cameraFactory) {
         Map<UseCase, ConfigPair> configs = new HashMap<>();
         for (UseCase useCase : useCases) {
@@ -647,7 +741,7 @@
             try {
                 Map<UseCase, ConfigPair> configs = getConfigs(Arrays.asList(useCases),
                         mCameraConfig.getUseCaseConfigFactory(), mUseCaseConfigFactory);
-                calculateSuggestedResolutions(mCameraInternal.getCameraInfoInternal(),
+                calculateSuggestedStreamSpecs(mCameraInternal.getCameraInfoInternal(),
                         Arrays.asList(useCases), emptyList(), configs);
             } catch (IllegalArgumentException e) {
                 return false;
@@ -658,64 +752,30 @@
     }
 
     /**
-     * Calculates the new required extra use cases according to the use cases bound by the
-     * application and the existing extra use cases.
+     * Calculate the internal created placeholder UseCase for Extensions.
      *
-     * @param boundUseCases The use cases bound by the application.
-     * @param extraUseCases The originally existing extra use cases.
-     * @return new required extra use cases
+     * @param appUseCases UseCase provided by the app.
      */
-    @NonNull
-    private List<UseCase> calculateRequiredExtraUseCases(@NonNull List<UseCase> boundUseCases,
-            @NonNull List<UseCase> extraUseCases) {
-        List<UseCase> requiredExtraUseCases = new ArrayList<>(extraUseCases);
-        boolean isExtraPreviewRequired = isExtraPreviewRequired(boundUseCases);
-        boolean isExtraImageCaptureRequired = isExtraImageCaptureRequired(
-                boundUseCases);
-        UseCase existingExtraPreview = null;
-        UseCase existingExtraImageCapture = null;
-
-        for (UseCase useCase : extraUseCases) {
-            if (isPreview(useCase)) {
-                existingExtraPreview = useCase;
-            } else if (isImageCapture(useCase)) {
-                existingExtraImageCapture = useCase;
-            }
-        }
-
-        if (isExtraPreviewRequired && existingExtraPreview == null) {
-            requiredExtraUseCases.add(createExtraPreview());
-        } else if (!isExtraPreviewRequired && existingExtraPreview != null) {
-            requiredExtraUseCases.remove(existingExtraPreview);
-        }
-
-        if (isExtraImageCaptureRequired && existingExtraImageCapture == null) {
-            requiredExtraUseCases.add(createExtraImageCapture());
-        } else if (!isExtraImageCaptureRequired && existingExtraImageCapture != null) {
-            requiredExtraUseCases.remove(existingExtraImageCapture);
-        }
-
-        return requiredExtraUseCases;
-    }
-
-    /**
-     * Detaches unnecessary use cases from camera.
-     */
-    private void detachUnnecessaryUseCases(@NonNull List<UseCase> unnecessaryUseCases) {
+    @Nullable
+    UseCase calculatePlaceholderForExtensions(@NonNull Set<UseCase> appUseCases) {
         synchronized (mLock) {
-            if (!unnecessaryUseCases.isEmpty()) {
-                mCameraInternal.detachUseCases(unnecessaryUseCases);
-
-                for (UseCase useCase : unnecessaryUseCases) {
-                    if (mUseCases.contains(useCase)) {
-                        useCase.unbindFromCamera(mCameraInternal);
+            UseCase placeholder = null;
+            if (isCoexistingPreviewImageCaptureRequired()) {
+                if (isExtraPreviewRequired(appUseCases)) {
+                    if (isPreview(mPlaceholderForExtensions)) {
+                        placeholder = mPlaceholderForExtensions;
                     } else {
-                        Logger.e(TAG, "Attempting to detach non-attached UseCase: " + useCase);
+                        placeholder = createExtraPreview();
+                    }
+                } else if (isExtraImageCaptureRequired(appUseCases)) {
+                    if (isImageCapture(mPlaceholderForExtensions)) {
+                        placeholder = mPlaceholderForExtensions;
+                    } else {
+                        placeholder = createExtraImageCapture();
                     }
                 }
-
-                mUseCases.removeAll(unnecessaryUseCases);
             }
+            return placeholder;
         }
     }
 
@@ -730,7 +790,7 @@
      * Returns true if the input use case list contains a {@link ImageCapture} but does not
      * contain an {@link Preview}.
      */
-    private boolean isExtraPreviewRequired(@NonNull List<UseCase> useCases) {
+    private boolean isExtraPreviewRequired(@NonNull Collection<UseCase> useCases) {
         boolean hasPreview = false;
         boolean hasImageCapture = false;
 
@@ -749,7 +809,7 @@
      * Returns true if the input use case list contains a {@link Preview} but does not contain an
      * {@link ImageCapture}.
      */
-    private boolean isExtraImageCaptureRequired(@NonNull List<UseCase> useCases) {
+    private boolean isExtraImageCaptureRequired(@NonNull Collection<UseCase> useCases) {
         boolean hasPreview = false;
         boolean hasImageCapture = false;
 
@@ -764,14 +824,25 @@
         return hasPreview && !hasImageCapture;
     }
 
-    private boolean isPreview(UseCase useCase) {
+    private static boolean isStreamSharing(@Nullable UseCase useCase) {
+        return useCase instanceof StreamSharing;
+    }
+
+    private static boolean isPreview(@Nullable UseCase useCase) {
         return useCase instanceof Preview;
     }
 
-    private boolean isImageCapture(UseCase useCase) {
+    private static boolean isImageCapture(@Nullable UseCase useCase) {
         return useCase instanceof ImageCapture;
     }
 
+    private boolean isPrivateInputFormat(@NonNull UseCase useCase) {
+        UseCaseConfig<?> mergedConfig = useCase.mergeConfigs(
+                mCameraInternal.getCameraInfoInternal(), null,
+                useCase.getDefaultConfig(true, mUseCaseConfigFactory));
+        return mergedConfig.getInputFormat() == INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+    }
+
     private Preview createExtraPreview() {
         Preview preview = new Preview.Builder().setTargetName("Preview-Extra").build();
 
@@ -796,4 +867,9 @@
     private ImageCapture createExtraImageCapture() {
         return new ImageCapture.Builder().setTargetName("ImageCapture-Extra").build();
     }
+
+    @VisibleForTesting
+    void setStreamSharingEnabled(boolean streamSharingEnabled) {
+        mStreamSharingEnabled = streamSharingEnabled;
+    }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/ViewPorts.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/ViewPorts.java
index 8f39855..e20f0ed 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/ViewPorts.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/ViewPorts.java
@@ -22,13 +22,13 @@
 import android.graphics.RectF;
 import android.util.LayoutDirection;
 import android.util.Rational;
-import android.util.Size;
 
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.ViewPort;
+import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.internal.utils.ImageUtil;
 import androidx.core.util.Preconditions;
 
@@ -57,7 +57,7 @@
      *                              rotation.
      * @param scaleType             The scale type to calculate
      * @param layoutDirection       The direction of layout.
-     * @param useCaseSizes          The resolutions of the UseCases
+     * @param useCaseStreamSpecs    The stream specifications of the UseCases
      * @return The set of Viewports that should be set for each UseCase
      */
     @NonNull
@@ -68,7 +68,7 @@
             @IntRange(from = 0, to = 359) int outputRotationDegrees,
             @ViewPort.ScaleType int scaleType,
             @ViewPort.LayoutDirection int layoutDirection,
-            @NonNull Map<UseCase, Size> useCaseSizes) {
+            @NonNull Map<UseCase, StreamSpec> useCaseStreamSpecs) {
         Preconditions.checkArgument(
                 fullSensorRect.width() > 0 && fullSensorRect.height() > 0,
                 "Cannot compute viewport crop rects zero sized sensor rect.");
@@ -82,11 +82,11 @@
         RectF fullSensorRectF = new RectF(fullSensorRect);
         Map<UseCase, Matrix> useCaseToSensorTransformations = new HashMap<>();
         RectF sensorIntersectionRect = new RectF(fullSensorRect);
-        for (Map.Entry<UseCase, Size> entry : useCaseSizes.entrySet()) {
+        for (Map.Entry<UseCase, StreamSpec> entry : useCaseStreamSpecs.entrySet()) {
             // Calculate the transformation from UseCase to sensor.
             Matrix useCaseToSensorTransformation = new Matrix();
-            RectF srcRect = new RectF(0, 0, entry.getValue().getWidth(),
-                    entry.getValue().getHeight());
+            RectF srcRect = new RectF(0, 0, entry.getValue().getResolution().getWidth(),
+                    entry.getValue().getResolution().getHeight());
             useCaseToSensorTransformation.setRectToRect(srcRect, fullSensorRectF,
                     Matrix.ScaleToFit.CENTER);
             useCaseToSensorTransformations.put(entry.getKey(), useCaseToSensorTransformation);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java
index 57d3ffe..61190dc 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java
@@ -30,6 +30,7 @@
 import androidx.camera.core.SurfaceRequest;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.core.util.Supplier;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
@@ -65,7 +66,7 @@
     private int mInputSurfaceCount = 0;
 
     /** Constructs {@link DefaultSurfaceProcessor} with default shaders. */
-    public DefaultSurfaceProcessor() {
+    DefaultSurfaceProcessor() {
         this(ShaderProvider.DEFAULT);
     }
 
@@ -75,7 +76,7 @@
      * @param shaderProvider custom shader provider for OpenGL rendering.
      * @throws IllegalArgumentException if the shaderProvider provides invalid shader.
      */
-    public DefaultSurfaceProcessor(@NonNull ShaderProvider shaderProvider) {
+    DefaultSurfaceProcessor(@NonNull ShaderProvider shaderProvider) {
         mGlThread = new HandlerThread("GL Thread");
         mGlThread.start();
         mGlHandler = new Handler(mGlThread.getLooper());
@@ -208,4 +209,32 @@
             }
         }
     }
+
+    /**
+     * Factory class that produces {@link DefaultSurfaceProcessor}.
+     *
+     * <p> This is for working around the limit that OpenGL cannot be initialized in unit tests.
+     */
+    public static class Factory {
+        private Factory() {
+        }
+
+        private static Supplier<SurfaceProcessorInternal> sSupplier = DefaultSurfaceProcessor::new;
+
+        /**
+         * Creates a new {@link DefaultSurfaceProcessor} with no-op shader.
+         */
+        @NonNull
+        public static SurfaceProcessorInternal newInstance() {
+            return sSupplier.get();
+        }
+
+        /**
+         * Overrides the {@link DefaultSurfaceProcessor} supplier for testing.
+         */
+        @VisibleForTesting
+        public static void setSupplier(@NonNull Supplier<SurfaceProcessorInternal> supplier) {
+            sSupplier = supplier;
+        }
+    }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
index e1951631..ca3c2df 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
@@ -50,6 +50,7 @@
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.DeferrableSurface;
 import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.utils.futures.Futures;
 import androidx.concurrent.futures.CallbackToFutureAdapter;
 
@@ -101,7 +102,7 @@
     private final boolean mMirroring;
     @CameraEffect.Targets
     private final int mTargets;
-    private final Size mSize;
+    private final StreamSpec mStreamSpec;
     // Guarded by main thread.
     private int mRotationDegrees;
 
@@ -128,20 +129,20 @@
      */
     public SurfaceEdge(
             @CameraEffect.Targets int targets,
-            @NonNull Size size,
+            @NonNull StreamSpec streamSpec,
             @NonNull Matrix sensorToBufferTransform,
             boolean hasCameraTransform,
             @NonNull Rect cropRect,
             int rotationDegrees,
             boolean mirroring) {
         mTargets = targets;
-        mSize = size;
+        mStreamSpec = streamSpec;
         mSensorToBufferTransform = sensorToBufferTransform;
         mHasCameraTransform = hasCameraTransform;
         mCropRect = cropRect;
         mRotationDegrees = rotationDegrees;
         mMirroring = mirroring;
-        mSettableSurface = new SettableSurface(size);
+        mSettableSurface = new SettableSurface(streamSpec.getResolution());
     }
 
     /**
@@ -248,8 +249,9 @@
             @Nullable Range<Integer> expectedFpsRange) {
         checkMainThread();
         // TODO(b/238230154) figure out how to support HDR.
-        SurfaceRequest surfaceRequest = new SurfaceRequest(getSize(), cameraInternal,
-                expectedFpsRange, () -> mainThreadExecutor().execute(this::invalidate));
+        SurfaceRequest surfaceRequest = new SurfaceRequest(mStreamSpec.getResolution(),
+                cameraInternal, expectedFpsRange,
+                () -> mainThreadExecutor().execute(this::invalidate));
         try {
             DeferrableSurface deferrableSurface = surfaceRequest.getDeferrableSurface();
             if (mSettableSurface.setProvider(deferrableSurface)) {
@@ -308,8 +310,8 @@
                         return immediateFailedFuture(e);
                     }
                     SurfaceOutputImpl surfaceOutputImpl = new SurfaceOutputImpl(surface,
-                            getTargets(), getSize(), inputSize, cropRect, rotationDegrees,
-                            mirroring);
+                            getTargets(), mStreamSpec.getResolution(), inputSize, cropRect,
+                            rotationDegrees, mirroring);
                     surfaceOutputImpl.getCloseFuture().addListener(
                             settableSurface::decrementUseCount,
                             directExecutor());
@@ -337,7 +339,7 @@
         checkMainThread();
         close();
         mHasConsumer = false;
-        mSettableSurface = new SettableSurface(mSize);
+        mSettableSurface = new SettableSurface(mStreamSpec.getResolution());
         for (Runnable onInvalidated : mOnInvalidatedListeners) {
             onInvalidated.run();
         }
@@ -374,14 +376,6 @@
     }
 
     /**
-     * The allocated size of the {@link Surface}.
-     */
-    @NonNull
-    public Size getSize() {
-        return mSize;
-    }
-
-    /**
      * Gets the {@link Matrix} represents the transformation from camera sensor to the current
      * {@link Surface}.
      *
@@ -475,6 +469,14 @@
     }
 
     /**
+     * Returns {@link StreamSpec} associated with this edge.
+     */
+    @NonNull
+    public StreamSpec getStreamSpec() {
+        return mStreamSpec;
+    }
+
+    /**
      * A {@link DeferrableSurface} that sets another {@link DeferrableSurface} as the source.
      *
      * <p>This class provides mechanisms to link an {@link DeferrableSurface}, and propagates
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
index dfb71ec..286ddca 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
@@ -40,6 +40,7 @@
 import androidx.camera.core.SurfaceRequest;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.impl.CameraInternal;
+import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.utils.Threads;
 import androidx.camera.core.impl.utils.futures.FutureCallback;
 import androidx.camera.core.impl.utils.futures.Futures;
@@ -121,17 +122,16 @@
     @NonNull
     private SurfaceEdge transformSingleOutput(@NonNull SurfaceEdge input,
             @NonNull OutConfig outConfig) {
-
         SurfaceEdge outputSurface;
-        Size inputSize = input.getSize();
         Rect cropRect = outConfig.getCropRect();
         int rotationDegrees = input.getRotationDegrees();
-        boolean mirroring = input.getMirroring();
+        boolean mirroring = outConfig.getMirroring();
 
         // Calculate sensorToBufferTransform
         android.graphics.Matrix sensorToBufferTransform =
                 new android.graphics.Matrix(input.getSensorToBufferTransform());
-        android.graphics.Matrix imageTransform = getRectToRect(sizeToRectF(inputSize),
+        android.graphics.Matrix imageTransform = getRectToRect(
+                sizeToRectF(input.getStreamSpec().getResolution()),
                 sizeToRectF(outConfig.getSize()), rotationDegrees, mirroring);
         sensorToBufferTransform.postConcat(imageTransform);
 
@@ -140,16 +140,20 @@
         Size rotatedCropSize = getRotatedSize(outConfig.getCropRect(), rotationDegrees);
         checkArgument(isAspectRatioMatchingWithRoundingError(rotatedCropSize, outConfig.getSize()));
 
+        StreamSpec streamSpec = StreamSpec.builder(outConfig.getSize())
+                .setExpectedFrameRateRange(input.getStreamSpec().getExpectedFrameRateRange())
+                .build();
+
         outputSurface = new SurfaceEdge(
                 outConfig.getTargets(),
-                outConfig.getSize(),
+                streamSpec,
                 sensorToBufferTransform,
                 // The Surface transform cannot be carried over during buffer copy.
                 /*hasCameraTransform=*/false,
                 // Crop rect is always the full size.
                 sizeToRect(outConfig.getSize()),
                 /*rotationDegrees=*/0,
-                /*mirroring=*/false);
+                /*mirroring=*/input.getMirroring() != mirroring);
 
         return outputSurface;
     }
@@ -163,7 +167,6 @@
         setUpRotationUpdates(
                 surfaceRequest,
                 outputs,
-                input.getMirroring(),
                 input.getRotationDegrees());
         try {
             mSurfaceProcessor.onInputSurface(surfaceRequest);
@@ -191,10 +194,10 @@
     private void createAndSendSurfaceOutput(@NonNull SurfaceEdge input,
             Map.Entry<OutConfig, SurfaceEdge> output) {
         ListenableFuture<SurfaceOutput> future = output.getValue().createSurfaceOutputFuture(
-                input.getSize(),
+                input.getStreamSpec().getResolution(),
                 output.getKey().getCropRect(),
                 input.getRotationDegrees(),
-                input.getMirroring());
+                output.getKey().getMirroring());
         Futures.addCallback(future, new FutureCallback<SurfaceOutput>() {
             @Override
             public void onSuccess(@Nullable SurfaceOutput output) {
@@ -226,23 +229,23 @@
      *
      * @param inputSurfaceRequest {@link SurfaceRequest} of the input edge.
      * @param outputs             the output edges.
-     * @param mirrored            whether the node mirrors the buffer.
      * @param rotatedDegrees      how much the node rotates the buffer.
      */
     void setUpRotationUpdates(
             @NonNull SurfaceRequest inputSurfaceRequest,
             @NonNull Collection<SurfaceEdge> outputs,
-            boolean mirrored,
             int rotatedDegrees) {
         inputSurfaceRequest.setTransformationInfoListener(mainThreadExecutor(), info -> {
-            // To obtain the rotation degrees delta, the rotation performed by the node must be
-            // eliminated.
-            int rotationDegrees = info.getRotationDegrees() - rotatedDegrees;
-            if (mirrored) {
-                rotationDegrees = -rotationDegrees;
-            }
-            rotationDegrees = within360(rotationDegrees);
             for (SurfaceEdge output : outputs) {
+                // To obtain the rotation degrees delta, the rotation performed by the node must be
+                // eliminated.
+                int rotationDegrees = info.getRotationDegrees() - rotatedDegrees;
+                if (output.getMirroring()) {
+                    // The order of transformation is cropping -> rotation -> mirroring. To
+                    // change the rotation, one must consider the mirroring.
+                    rotationDegrees = -rotationDegrees;
+                }
+                rotationDegrees = within360(rotationDegrees);
                 output.setRotationDegrees(rotationDegrees);
             }
         });
@@ -339,6 +342,11 @@
         abstract Size getSize();
 
         /**
+         * The whether the stream should be mirrored.
+         */
+        abstract boolean getMirroring();
+
+        /**
          * Creates an {@link OutConfig} instance from the input edge.
          *
          * <p>The result is an output edge with the input's transformation applied.
@@ -347,15 +355,17 @@
         public static OutConfig of(@NonNull SurfaceEdge surface) {
             return of(surface.getTargets(),
                     surface.getCropRect(),
-                    getRotatedSize(surface.getCropRect(), surface.getRotationDegrees()));
+                    getRotatedSize(surface.getCropRect(), surface.getRotationDegrees()),
+                    surface.getMirroring());
         }
 
         /**
          * Creates an {@link OutConfig} instance with custom transformations.
          */
         @NonNull
-        public static OutConfig of(int targets, @NonNull Rect cropRect, @NonNull Size size) {
-            return new AutoValue_SurfaceProcessorNode_OutConfig(targets, cropRect, size);
+        public static OutConfig of(int targets, @NonNull Rect cropRect, @NonNull Size size,
+                boolean mirroring) {
+            return new AutoValue_SurfaceProcessorNode_OutConfig(targets, cropRect, size, mirroring);
         }
     }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/ResolutionUtils.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/ResolutionUtils.java
new file mode 100644
index 0000000..7b93c0b
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/ResolutionUtils.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.streamsharing;
+
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_CUSTOM_ORDERED_RESOLUTIONS;
+
+import android.os.Build;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.UseCaseConfig;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Utility methods for calculating resolutions.
+ */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+class ResolutionUtils {
+
+    private ResolutionUtils() {
+    }
+
+    /**
+     * Returns a list of {@link Surface} resolution sorted by priority.
+     *
+     * <p> This method calculates the resolution for the parent {@link StreamSharing} based on 1)
+     * the supported PRIV resolutions, 2) the sensor size and 3) the children's configs.
+     */
+    static List<Size> getMergedResolutions(
+            @NonNull List<Size> supportedResolutions,
+            @NonNull Size sensorSize,
+            @NonNull Set<UseCaseConfig<?>> useCaseConfigs) {
+        // TODO(b/264936115): This is a temporary placeholder solution that returns the config of
+        //  VideoCapture if it exists. Later we will update it to actually merge the children's
+        //  configs.
+        for (UseCaseConfig<?> useCaseConfig : useCaseConfigs) {
+            List<Size> customOrderedResolutions =
+                    useCaseConfig.retrieveOption(OPTION_CUSTOM_ORDERED_RESOLUTIONS, null);
+            if (customOrderedResolutions != null) {
+                return customOrderedResolutions;
+            }
+        }
+        return supportedResolutions;
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
new file mode 100644
index 0000000..f1a5619
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.streamsharing;
+
+import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_FORMAT;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.CameraInfoInternal;
+import androidx.camera.core.impl.CameraInternal;
+import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.ImageFormatConstants;
+import androidx.camera.core.impl.MutableConfig;
+import androidx.camera.core.impl.MutableOptionsBundle;
+import androidx.camera.core.impl.OptionsBundle;
+import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.core.impl.UseCaseConfigFactory;
+
+import java.util.Set;
+
+/**
+ * A {@link UseCase} that shares one PRIV stream to multiple children {@link UseCase}s.
+ */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+public class StreamSharing extends UseCase {
+
+    private static final StreamSharingConfig DEFAULT_CONFIG;
+
+    @SuppressWarnings("UnusedVariable")
+    private final VirtualCamera mVirtualCamera;
+
+    static {
+        MutableConfig mutableConfig = new StreamSharingBuilder().getMutableConfig();
+        mutableConfig.insertOption(OPTION_INPUT_FORMAT,
+                ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE);
+        DEFAULT_CONFIG = new StreamSharingConfig(OptionsBundle.from(mutableConfig));
+    }
+
+    /**
+     * Constructs a {@link StreamSharing} with a parent {@link CameraInternal}, children
+     * {@link UseCase}s, and a {@link UseCaseConfigFactory} for getting default {@link UseCase}
+     * configurations.
+     */
+    public StreamSharing(@NonNull CameraInternal parentCamera,
+            @NonNull Set<UseCase> children,
+            @NonNull UseCaseConfigFactory useCaseConfigFactory) {
+        this(new VirtualCamera(parentCamera, children, useCaseConfigFactory));
+    }
+
+    StreamSharing(@NonNull VirtualCamera virtualCamera) {
+        super(DEFAULT_CONFIG);
+        mVirtualCamera = virtualCamera;
+    }
+
+    @Nullable
+    @Override
+    public UseCaseConfig<?> getDefaultConfig(boolean applyDefaultConfig,
+            @NonNull UseCaseConfigFactory factory) {
+        // The shared stream optimizes for VideoCapture.
+        Config captureConfig = factory.getConfig(
+                UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
+                ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY);
+
+        if (applyDefaultConfig) {
+            captureConfig = Config.mergeConfigs(captureConfig, DEFAULT_CONFIG.getConfig());
+        }
+        return captureConfig == null ? null :
+                getUseCaseConfigBuilder(captureConfig).getUseCaseConfig();
+    }
+
+    @NonNull
+    @Override
+    public UseCaseConfig.Builder<?, ?, ?> getUseCaseConfigBuilder(@NonNull Config config) {
+        return new StreamSharingBuilder(MutableOptionsBundle.from(config));
+    }
+
+    @NonNull
+    @Override
+    protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
+            @NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
+        mVirtualCamera.mergeChildrenConfigs(builder.getMutableConfig());
+        return builder.getUseCaseConfig();
+    }
+
+    @Override
+    public void onBind() {
+        super.onBind();
+        mVirtualCamera.bindChildren();
+    }
+
+    @Override
+    public void onUnbind() {
+        super.onUnbind();
+        mVirtualCamera.unbindChildren();
+    }
+
+    @Override
+    public void onStateAttached() {
+        super.onStateAttached();
+        mVirtualCamera.notifyStateAttached();
+    }
+
+    @Override
+    public void onStateDetached() {
+        super.onStateDetached();
+        mVirtualCamera.notifyStateDetached();
+    }
+
+    @NonNull
+    public Set<UseCase> getChildren() {
+        return mVirtualCamera.getChildren();
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharingBuilder.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharingBuilder.java
new file mode 100644
index 0000000..0ae8daa
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharingBuilder.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.streamsharing;
+
+import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_CLASS;
+import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_NAME;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.CaptureConfig;
+import androidx.camera.core.impl.MutableConfig;
+import androidx.camera.core.impl.MutableOptionsBundle;
+import androidx.camera.core.impl.OptionsBundle;
+import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.core.internal.TargetConfig;
+
+import java.util.UUID;
+
+/**
+ * A empty builder for {@link StreamSharing}.
+ *
+ * <p> {@link StreamSharing} does not need a public builder. Instead, the caller should call the
+ * constructor directly. This class exists because the {@link UseCase#getUseCaseConfigBuilder}
+ * method requires a builder for each {@link UseCase}.
+ */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+class StreamSharingBuilder implements
+        UseCaseConfig.Builder<StreamSharing, StreamSharingConfig, StreamSharingBuilder> {
+
+    private static final String UNSUPPORTED_MESSAGE =
+            "Operation not supported by StreamSharingBuilder.";
+
+    private final MutableOptionsBundle mMutableConfig;
+
+    StreamSharingBuilder() {
+        this(MutableOptionsBundle.create());
+    }
+
+    StreamSharingBuilder(@NonNull MutableOptionsBundle mutableConfig) {
+        mMutableConfig = mutableConfig;
+        Class<?> oldConfigClass =
+                mutableConfig.retrieveOption(TargetConfig.OPTION_TARGET_CLASS, null);
+        if (oldConfigClass != null && !oldConfigClass.equals(StreamSharing.class)) {
+            throw new IllegalArgumentException(
+                    "Invalid target class configuration for "
+                            + StreamSharingBuilder.this
+                            + ": "
+                            + oldConfigClass);
+        }
+        setTargetClass(StreamSharing.class);
+    }
+
+    @NonNull
+    @Override
+    public MutableConfig getMutableConfig() {
+        return mMutableConfig;
+    }
+
+    @NonNull
+    @Override
+    public StreamSharing build() {
+        throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
+    }
+
+    @NonNull
+    @Override
+    public StreamSharingBuilder setDefaultSessionConfig(@NonNull SessionConfig sessionConfig) {
+        throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
+    }
+
+    @NonNull
+    @Override
+    public StreamSharingBuilder setDefaultCaptureConfig(@NonNull CaptureConfig captureConfig) {
+        throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
+    }
+
+    @NonNull
+    @Override
+    public StreamSharingBuilder setSessionOptionUnpacker(
+            @NonNull SessionConfig.OptionUnpacker optionUnpacker) {
+        throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
+    }
+
+    @NonNull
+    @Override
+    public StreamSharingBuilder setCaptureOptionUnpacker(
+            @NonNull CaptureConfig.OptionUnpacker optionUnpacker) {
+        throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
+    }
+
+    @NonNull
+    @Override
+    public StreamSharingBuilder setSurfaceOccupancyPriority(int priority) {
+        throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
+    }
+
+    @NonNull
+    @Override
+    public StreamSharingBuilder setCameraSelector(@NonNull CameraSelector cameraSelector) {
+        throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
+    }
+
+    @NonNull
+    @Override
+    public StreamSharingBuilder setZslDisabled(boolean disabled) {
+        throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
+    }
+
+    @NonNull
+    @Override
+    public StreamSharingBuilder setHighResolutionDisabled(boolean disabled) {
+        throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
+    }
+
+    @NonNull
+    @Override
+    public StreamSharingConfig getUseCaseConfig() {
+        return new StreamSharingConfig(OptionsBundle.from(mMutableConfig));
+    }
+
+    @NonNull
+    @Override
+    public StreamSharingBuilder setTargetClass(@NonNull Class<StreamSharing> targetClass) {
+        getMutableConfig().insertOption(OPTION_TARGET_CLASS, targetClass);
+        // If no name is set yet, then generate a unique name
+        if (null == getMutableConfig().retrieveOption(OPTION_TARGET_NAME, null)) {
+            String targetName = targetClass.getCanonicalName() + "-" + UUID.randomUUID();
+            setTargetName(targetName);
+        }
+        return this;
+    }
+
+    @NonNull
+    @Override
+    public StreamSharingBuilder setTargetName(@NonNull String targetName) {
+        getMutableConfig().insertOption(OPTION_TARGET_NAME, targetName);
+        return this;
+    }
+
+    @NonNull
+    @Override
+    public StreamSharingBuilder setUseCaseEventCallback(
+            @NonNull UseCase.EventCallback eventCallback) {
+        throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharingConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharingConfig.java
new file mode 100644
index 0000000..adf1207
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharingConfig.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.streamsharing;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.MutableConfig;
+import androidx.camera.core.impl.OptionsBundle;
+import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.core.internal.ThreadConfig;
+
+/**
+ * Configuration for a {@link StreamSharing} use case.
+ *
+ * <p> This class is a thin wrapper of the underlying {@link OptionsBundle}. Instead of adding
+ * getters and setters to this class, one should modify the config using
+ * {@link MutableConfig#insertOption} and {@link MutableConfig#retrieveOption} directly.
+ */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+class StreamSharingConfig implements UseCaseConfig<StreamSharing>,
+        ImageOutputConfig,
+        ThreadConfig {
+
+    private final OptionsBundle mConfig;
+
+    /** Creates a new configuration instance. */
+    StreamSharingConfig(@NonNull OptionsBundle config) {
+        mConfig = config;
+    }
+
+    @NonNull
+    @Override
+    public Config getConfig() {
+        return mConfig;
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
new file mode 100644
index 0000000..1a5ec39
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.streamsharing;
+
+import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_CUSTOM_ORDERED_RESOLUTIONS;
+import static androidx.camera.core.impl.utils.TransformUtils.rectToSize;
+import static androidx.camera.core.streamsharing.ResolutionUtils.getMergedResolutions;
+
+import android.os.Build;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.CameraControlInternal;
+import androidx.camera.core.impl.CameraInfoInternal;
+import androidx.camera.core.impl.CameraInternal;
+import androidx.camera.core.impl.MutableConfig;
+import androidx.camera.core.impl.Observable;
+import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.core.impl.UseCaseConfigFactory;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A virtual implementation of {@link CameraInternal}.
+ *
+ * <p> This class manages children {@link UseCase} and connects/disconnects them to the
+ * parent {@link StreamSharing}. It also forwards parent camera properties/events to the children.
+ */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+class VirtualCamera implements CameraInternal {
+
+    private static final String UNSUPPORTED_MESSAGE = "Operation not supported by VirtualCamera.";
+
+    @NonNull
+    private final Set<UseCase> mChildren;
+    @NonNull
+    private final UseCaseConfigFactory mUseCaseConfigFactory;
+    @NonNull
+    private final CameraInternal mParentCamera;
+
+    /**
+     * @param parentCamera         the parent {@link CameraInternal} instance. For example, the
+     *                             real camera.
+     * @param children             the children {@link UseCase}.
+     * @param useCaseConfigFactory the factory for configuring children {@link UseCase}.
+     */
+    VirtualCamera(@NonNull CameraInternal parentCamera,
+            @NonNull Set<UseCase> children,
+            @NonNull UseCaseConfigFactory useCaseConfigFactory) {
+        mParentCamera = parentCamera;
+        mUseCaseConfigFactory = useCaseConfigFactory;
+        mChildren = children;
+    }
+
+    // --- API for StreamSharing ---
+
+    void mergeChildrenConfigs(@NonNull MutableConfig mutableConfig) {
+        Set<UseCaseConfig<?>> childrenConfigs = new HashSet<>();
+        for (UseCase useCase : mChildren) {
+            childrenConfigs.add(useCase.mergeConfigs(mParentCamera.getCameraInfoInternal(),
+                    null,
+                    useCase.getDefaultConfig(true, mUseCaseConfigFactory)));
+        }
+        // Merge resolution configs.
+        List<Size> supportedResolutions =
+                new ArrayList<>(mParentCamera.getCameraInfoInternal().getSupportedResolutions(
+                        INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE));
+        Size sensorSize = rectToSize(mParentCamera.getCameraControlInternal().getSensorRect());
+        mutableConfig.insertOption(OPTION_CUSTOM_ORDERED_RESOLUTIONS,
+                getMergedResolutions(supportedResolutions, sensorSize,
+                        childrenConfigs));
+    }
+
+    void bindChildren() {
+        for (UseCase useCase : mChildren) {
+            useCase.bindToCamera(this, null,
+                    useCase.getDefaultConfig(true, mUseCaseConfigFactory));
+        }
+    }
+
+    void unbindChildren() {
+        for (UseCase useCase : mChildren) {
+            useCase.unbindFromCamera(this);
+        }
+    }
+
+    void notifyStateAttached() {
+        for (UseCase useCase : mChildren) {
+            useCase.onStateAttached();
+        }
+    }
+
+    void notifyStateDetached() {
+        for (UseCase useCase : mChildren) {
+            useCase.onStateDetached();
+        }
+    }
+
+    @NonNull
+    Set<UseCase> getChildren() {
+        return mChildren;
+    }
+
+    // --- Handle children state change ---
+
+    // TODO(b/264936250): Handle children state changes.
+
+    @Override
+    public void onUseCaseActive(@NonNull UseCase useCase) {
+
+    }
+
+    @Override
+    public void onUseCaseInactive(@NonNull UseCase useCase) {
+
+    }
+
+    @Override
+    public void onUseCaseUpdated(@NonNull UseCase useCase) {
+
+    }
+
+    @Override
+    public void onUseCaseReset(@NonNull UseCase useCase) {
+
+    }
+
+    // --- Forward parent camera properties and events ---
+
+    @NonNull
+    @Override
+    public CameraControlInternal getCameraControlInternal() {
+        return mParentCamera.getCameraControlInternal();
+    }
+
+    @NonNull
+    @Override
+    public CameraInfoInternal getCameraInfoInternal() {
+        // TODO(264936250): replace this with a virtual camera info that returns a updated sensor
+        //  rotation degrees based on buffer transformation applied in StreamSharing.
+        return mParentCamera.getCameraInfoInternal();
+    }
+
+    @NonNull
+    @Override
+    public Observable<State> getCameraState() {
+        return mParentCamera.getCameraState();
+    }
+
+    // --- Unused overrides ---
+
+    @Override
+    public void open() {
+        throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
+    }
+
+    @Override
+    public void close() {
+        throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Void> release() {
+        throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
+    }
+
+    @Override
+    public void attachUseCases(@NonNull Collection<UseCase> useCases) {
+        throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
+    }
+
+    @Override
+    public void detachUseCases(@NonNull Collection<UseCase> useCases) {
+        throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/package-info.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/package-info.java
new file mode 100644
index 0000000..fd92612
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.camera.core.streamsharing;
+
+import androidx.annotation.RestrictTo;
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index f7fff53..d1dae4c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -32,6 +32,7 @@
 import androidx.camera.core.impl.OptionsBundle
 import androidx.camera.core.impl.PreviewConfig
 import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.StreamSpec
 import androidx.camera.core.impl.UseCaseConfig
 import androidx.camera.core.impl.UseCaseConfigFactory
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
@@ -354,6 +355,39 @@
     }
 
     @Test
+    fun setNoCameraTransform_propagatesToCameraEdge() {
+        // Act: create preview with hasCameraTransform == false
+        val preview = createPreview(
+            FakeSurfaceProcessorInternal(mainThreadExecutor()),
+            hasCameraTransform = false
+        )
+        // Assert
+        assertThat(preview.cameraEdge.hasCameraTransform()).isFalse()
+        assertThat(preview.cameraEdge.mirroring).isFalse()
+    }
+
+    @Test
+    fun frontCameraWithoutCameraTransform_noMirroring() {
+        // Act: create preview with hasCameraTransform == false
+        val preview = createPreview(
+            FakeSurfaceProcessorInternal(mainThreadExecutor()),
+            frontCamera,
+            hasCameraTransform = false
+        )
+        // Assert
+        assertThat(preview.cameraEdge.mirroring).isFalse()
+    }
+
+    @Test
+    fun cameraEdgeHasTransformByDefault() {
+        assertThat(
+            createPreview(
+                FakeSurfaceProcessorInternal(mainThreadExecutor())
+            ).cameraEdge.hasCameraTransform()
+        ).isTrue()
+    }
+
+    @Test
     fun bindAndUnbindPreview_surfacesPropagated() {
         // Arrange.
         val processor = FakeSurfaceProcessorInternal(
@@ -605,11 +639,13 @@
 
     private fun createPreview(
         surfaceProcessor: SurfaceProcessorInternal? = null,
-        camera: FakeCamera = backCamera
+        camera: FakeCamera = backCamera,
+        hasCameraTransform: Boolean = true
     ): Preview {
         previewToDetach = Preview.Builder()
             .setTargetRotation(Surface.ROTATION_0)
             .build()
+        previewToDetach.setHasCameraTransform(hasCameraTransform)
         previewToDetach.processor = surfaceProcessor
         previewToDetach.setSurfaceProvider(CameraXExecutors.directExecutor()) {}
         val previewConfig = PreviewConfig(
@@ -620,7 +656,8 @@
         )
         previewToDetach.bindToCamera(camera, null, previewConfig)
 
-        previewToDetach.onSuggestedResolutionUpdated(Size(640, 480))
+        val streamSpec = StreamSpec.builder(Size(640, 480)).build()
+        previewToDetach.onSuggestedStreamSpecUpdated(streamSpec)
         return previewToDetach
     }
 }
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/StreamSpecTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/StreamSpecTest.kt
new file mode 100644
index 0000000..6142527
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/StreamSpecTest.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl
+
+import android.os.Build
+import android.util.Range
+import android.util.Size
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class StreamSpecTest {
+
+    @Test
+    fun canRetrieveResolution() {
+        val streamSpec = StreamSpec.builder(TEST_RESOLUTION).build()
+
+        assertThat(streamSpec.resolution).isEqualTo(TEST_RESOLUTION)
+    }
+
+    @Test
+    fun defaultExpectedFrameRateRangeIsUnspecified() {
+        val streamSpec = StreamSpec.builder(TEST_RESOLUTION).build()
+
+        assertThat(streamSpec.expectedFrameRateRange)
+            .isEqualTo(StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED)
+    }
+
+    @Test
+    fun canRetrieveExpectedFrameRateRange() {
+        val streamSpec = StreamSpec.builder(TEST_RESOLUTION)
+            .setExpectedFrameRateRange(TEST_EXPECTED_FRAME_RATE_RANGE)
+            .build()
+
+        assertThat(streamSpec.expectedFrameRateRange).isEqualTo(TEST_EXPECTED_FRAME_RATE_RANGE)
+    }
+
+    companion object {
+        private val TEST_RESOLUTION = Size(640, 480)
+        private val TEST_EXPECTED_FRAME_RATE_RANGE = Range(30, 30)
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
index f34f0c1..6680e50 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.core.internal
 
+import android.graphics.ImageFormat.JPEG
 import android.graphics.Matrix
 import android.graphics.Rect
 import android.os.Build
@@ -23,6 +24,7 @@
 import android.util.Size
 import android.view.Surface
 import androidx.camera.core.CameraEffect
+import androidx.camera.core.ImageAnalysis
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.Preview
 import androidx.camera.core.UseCase
@@ -31,11 +33,15 @@
 import androidx.camera.core.impl.CameraInternal
 import androidx.camera.core.impl.Config
 import androidx.camera.core.impl.Identifier
+import androidx.camera.core.impl.ImageFormatConstants
 import androidx.camera.core.impl.MutableOptionsBundle
 import androidx.camera.core.impl.OptionsBundle
+import androidx.camera.core.impl.StreamSpec
 import androidx.camera.core.impl.UseCaseConfigFactory
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
+import androidx.camera.core.internal.CameraUseCaseAdapter.CameraException
 import androidx.camera.core.processing.SurfaceProcessorWithExecutor
+import androidx.camera.core.streamsharing.StreamSharing
 import androidx.camera.testing.fakes.FakeCamera
 import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager
 import androidx.camera.testing.fakes.FakePreviewEffect
@@ -82,6 +88,11 @@
     private lateinit var useCaseConfigFactory: UseCaseConfigFactory
     private val fakeCameraSet = LinkedHashSet<CameraInternal>()
     private val imageEffect = GrayscaleImageEffect()
+    private val preview = Preview.Builder().build()
+    private val video = createFakeVideoCapture()
+    private val image = ImageCapture.Builder().build()
+    private val analysis = ImageAnalysis.Builder().build()
+    private lateinit var adapter: CameraUseCaseAdapter
 
     @Before
     fun setUp() {
@@ -92,6 +103,11 @@
         surfaceProcessor = FakeSurfaceProcessor(mainThreadExecutor())
         executor = Executors.newSingleThreadExecutor()
         effects = listOf(FakePreviewEffect(executor, surfaceProcessor), imageEffect)
+        adapter = CameraUseCaseAdapter(
+            fakeCameraSet,
+            fakeCameraDeviceSurfaceManager,
+            useCaseConfigFactory
+        )
     }
 
     @After
@@ -100,6 +116,153 @@
         executor.shutdown()
     }
 
+    // TODO(b/264936606): test UseCases bind()/unbind() are called correctly.
+
+    @Test(expected = CameraException::class)
+    fun addStreamSharing_throwsException() {
+        // Arrange.
+        adapter.setStreamSharingEnabled(true)
+        val streamSharing = StreamSharing(fakeCamera, setOf(preview, video), useCaseConfigFactory)
+        // Act: add use cases that can only be supported with StreamSharing
+        adapter.addUseCases(setOf(streamSharing, video, image))
+    }
+
+    @Test
+    fun invalidUseCaseCombo_streamSharingOn() {
+        // Arrange.
+        adapter.setStreamSharingEnabled(true)
+        // Act: add use cases that can only be supported with StreamSharing
+        adapter.addUseCases(setOf(preview, video, image))
+        // Assert: StreamSharing is connected to camera.
+        adapter.cameraUseCases.hasExactTypes(
+            StreamSharing::class.java,
+            ImageCapture::class.java
+        )
+        // Assert: StreamSharing children are bound
+        assertThat(preview.camera).isNotNull()
+        assertThat(video.camera).isNotNull()
+    }
+
+    @Test
+    fun validUseCaseCombo_streamSharingOff() {
+        // Arrange.
+        adapter.setStreamSharingEnabled(true)
+        // Act: add use cases that do not need StreamSharing
+        adapter.addUseCases(setOf(preview, video))
+        // Assert: the app UseCase are connected to camera.
+        adapter.cameraUseCases.hasExactTypes(
+            Preview::class.java,
+            FakeUseCase::class.java
+        )
+    }
+
+    @Test(expected = CameraException::class)
+    fun invalidUseCaseComboCantBeFixedByStreamSharing_throwsException() {
+        // Arrange: create a camera that only support one JPEG stream.
+        adapter.setStreamSharingEnabled(true)
+        fakeCameraDeviceSurfaceManager.setValidSurfaceCombos(setOf(listOf(JPEG)))
+        // Act: add PRIV and JPEG streams.
+        adapter.addUseCases(setOf(preview, image))
+    }
+
+    @Test
+    fun addChildThatRequiresStreamSharing_streamSharingOn() {
+        // Arrange.
+        adapter.setStreamSharingEnabled(true)
+        // Act: add UseCase that do not need StreamSharing
+        adapter.addUseCases(setOf(video, image))
+        // Assert.
+        adapter.cameraUseCases.hasExactTypes(
+            FakeUseCase::class.java,
+            ImageCapture::class.java
+        )
+        // Act: add a new UseCase that needs StreamSharing
+        adapter.addUseCases(setOf(preview))
+        // Assert: StreamSharing is created.
+        adapter.cameraUseCases.hasExactTypes(
+            StreamSharing::class.java,
+            ImageCapture::class.java
+        )
+        // Assert: StreamSharing children are bound
+        assertThat(preview.camera).isNotNull()
+        assertThat(video.camera).isNotNull()
+        assertThat(image.camera).isNotNull()
+    }
+
+    @Test
+    fun removeChildThatRequiresStreamSharing_streamSharingOff() {
+        // Arrange.
+        adapter.setStreamSharingEnabled(true)
+        // Act: add UseCases that need StreamSharing.
+        adapter.addUseCases(setOf(preview, video, image))
+        // Assert: StreamSharing exists and bound.
+        adapter.cameraUseCases.hasExactTypes(
+            StreamSharing::class.java,
+            ImageCapture::class.java
+        )
+        val streamSharing =
+            adapter.cameraUseCases.filterIsInstance(StreamSharing::class.java).single()
+        assertThat(streamSharing.camera).isNotNull()
+        // Act: remove UseCase so that StreamSharing is no longer needed
+        adapter.removeUseCases(setOf(video))
+        // Assert: StreamSharing removed and unbound.
+        adapter.cameraUseCases.hasExactTypes(
+            Preview::class.java,
+            ImageCapture::class.java
+        )
+        assertThat(streamSharing.camera).isNull()
+    }
+
+    @Test(expected = CameraException::class)
+    fun extensionEnabled_streamSharingOffAndThrowsException() {
+        // Arrange: enable extensions
+        adapter.setExtendedConfig(createCoexistingRequiredRuleCameraConfig())
+        adapter.setStreamSharingEnabled(true)
+        // Act: add UseCases that require StreamSharing
+        adapter.addUseCases(setOf(preview, video, image))
+    }
+
+    @Test
+    fun addAdditionalUseCase_streamSharingReused() {
+        // Arrange.
+        adapter.setStreamSharingEnabled(true)
+        // Act: add UseCases that require StreamSharing
+        adapter.addUseCases(setOf(preview, video, image))
+        // Assert: StreamSharing is used.
+        val streamSharing = adapter.cameraUseCases.filterIsInstance<StreamSharing>().single()
+        adapter.cameraUseCases.hasExactTypes(
+            StreamSharing::class.java,
+            ImageCapture::class.java
+        )
+        // Act: add another UseCase
+        adapter.addUseCases(setOf(analysis))
+        // Assert: the same StreamSharing instance is kept.
+        assertThat(
+            adapter.cameraUseCases.filterIsInstance<StreamSharing>().single()
+        ).isSameInstanceAs(streamSharing)
+        adapter.cameraUseCases.hasExactTypes(
+            StreamSharing::class.java,
+            ImageCapture::class.java,
+            ImageAnalysis::class.java
+        )
+    }
+
+    private fun createFakeVideoCapture(): FakeUseCase {
+        val fakeUseCaseConfig = FakeUseCaseConfig.Builder()
+            .setBufferFormat(ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE)
+        return FakeUseCase(
+            fakeUseCaseConfig.useCaseConfig,
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE
+        )
+    }
+
+    private fun Collection<UseCase>.hasExactTypes(vararg classTypes: Any) {
+        assertThat(classTypes.size).isEqualTo(size)
+        classTypes.forEach {
+            assertThat(filterIsInstance(it as Class<*>)).hasSize(1)
+        }
+    }
+
     @Test
     fun detachUseCases() {
         val cameraUseCaseAdapter = CameraUseCaseAdapter(
@@ -323,10 +486,10 @@
 
         // Arrange: set up adapter with aspect ratio 1.
         // The sensor size is 4032x3024 defined in FakeCameraDeviceSurfaceManager
-        fakeCameraDeviceSurfaceManager.setSuggestedResolution(
+        fakeCameraDeviceSurfaceManager.setSuggestedStreamSpec(
             CAMERA_ID,
             FakeUseCaseConfig::class.java,
-            Size(4032, 3022)
+            StreamSpec.builder(Size(4032, 3022)).build()
         )
         /*         Sensor to Buffer                 Crop on Buffer
          *        0               4032
@@ -445,7 +608,7 @@
         cameraUseCaseAdapter.addUseCases(listOf(preview))
 
         // Checks whether an extra ImageCapture is added.
-        assertThat(containsImageCapture(cameraUseCaseAdapter.useCases)).isTrue()
+        assertThat(containsImageCapture(cameraUseCaseAdapter.cameraUseCases)).isTrue()
     }
 
     @Test
@@ -462,7 +625,7 @@
         cameraUseCaseAdapter.addUseCases(listOf(preview))
 
         // Checks whether an extra ImageCapture is added.
-        assertThat(containsImageCapture(cameraUseCaseAdapter.useCases))
+        assertThat(containsImageCapture(cameraUseCaseAdapter.cameraUseCases))
         val imageCapture = ImageCapture.Builder().build()
 
         // Adds an ImageCapture
@@ -496,7 +659,7 @@
         cameraUseCaseAdapter.removeUseCases(listOf(imageCapture))
 
         // Checks whether an extra ImageCapture is added.
-        assertThat(containsImageCapture(cameraUseCaseAdapter.useCases)).isTrue()
+        assertThat(containsImageCapture(cameraUseCaseAdapter.cameraUseCases)).isTrue()
     }
 
     @Test
@@ -513,7 +676,7 @@
         cameraUseCaseAdapter.addUseCases(listOf(imageCapture))
 
         // Checks whether an extra Preview is added.
-        assertThat(containsPreview(cameraUseCaseAdapter.useCases)).isTrue()
+        assertThat(containsPreview(cameraUseCaseAdapter.cameraUseCases)).isTrue()
     }
 
     @Test
@@ -530,7 +693,7 @@
         cameraUseCaseAdapter.addUseCases(listOf(imageCapture))
 
         // Checks whether an extra Preview is added.
-        assertThat(containsPreview(cameraUseCaseAdapter.useCases))
+        assertThat(containsPreview(cameraUseCaseAdapter.cameraUseCases))
         val preview = Preview.Builder().build()
 
         // Adds an Preview
@@ -563,7 +726,7 @@
         cameraUseCaseAdapter.removeUseCases(listOf(preview))
 
         // Checks whether an extra Preview is added.
-        assertThat(containsPreview(cameraUseCaseAdapter.useCases)).isTrue()
+        assertThat(containsPreview(cameraUseCaseAdapter.cameraUseCases)).isTrue()
     }
 
     @Test
@@ -671,7 +834,7 @@
         }
     }
 
-    private fun containsPreview(useCases: List<UseCase>): Boolean {
+    private fun containsPreview(useCases: Collection<UseCase>): Boolean {
         for (useCase in useCases) {
             if (useCase is Preview) {
                 return true
@@ -680,7 +843,7 @@
         return false
     }
 
-    private fun containsImageCapture(useCases: List<UseCase>): Boolean {
+    private fun containsImageCapture(useCases: Collection<UseCase>): Boolean {
         for (useCase in useCases) {
             if (useCase is ImageCapture) {
                 return true
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt
new file mode 100644
index 0000000..2bc0793
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.processing
+
+import android.os.Build
+import androidx.camera.core.SurfaceOutput
+import androidx.camera.core.SurfaceRequest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+/**
+ * Unit tests for [DefaultSurfaceProcessor].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class DefaultSurfaceProcessorTest {
+
+    @Test
+    fun setFactorySupplier_factoryProvidesInstance() {
+        // Arrange: create a no-op processor and set it on the factory.
+        val noOpProcessor = object : SurfaceProcessorInternal {
+            override fun onInputSurface(request: SurfaceRequest) {
+            }
+
+            override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
+            }
+
+            override fun release() {
+            }
+        }
+        DefaultSurfaceProcessor.Factory.setSupplier { noOpProcessor }
+        // Assert: new instance returns the no-op processor.
+        assertThat(DefaultSurfaceProcessor.Factory.newInstance()).isSameInstanceAs(noOpProcessor)
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEdgeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEdgeTest.kt
index 4fa3664..60e066b 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEdgeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEdgeTest.kt
@@ -22,9 +22,11 @@
 import android.graphics.SurfaceTexture
 import android.os.Build
 import android.os.Looper.getMainLooper
+import android.util.Range
 import android.util.Size
 import android.view.Surface
 import androidx.camera.core.CameraEffect
+import androidx.camera.core.CameraEffect.PREVIEW
 import androidx.camera.core.SurfaceOutput
 import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.SurfaceRequest.Result.RESULT_REQUEST_CANCELLED
@@ -32,6 +34,7 @@
 import androidx.camera.core.impl.DeferrableSurface
 import androidx.camera.core.impl.DeferrableSurface.SurfaceClosedException
 import androidx.camera.core.impl.DeferrableSurface.SurfaceUnavailableException
+import androidx.camera.core.impl.StreamSpec
 import androidx.camera.core.impl.utils.TransformUtils.sizeToRect
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.core.impl.utils.futures.FutureCallback
@@ -60,6 +63,9 @@
 
     companion object {
         private val INPUT_SIZE = Size(640, 480)
+        private val FRAME_RATE = Range.create(30, 30)
+        private val FRAME_SPEC =
+            StreamSpec.builder(INPUT_SIZE).setExpectedFrameRateRange(FRAME_RATE).build()
     }
 
     private lateinit var surfaceEdge: SurfaceEdge
@@ -70,7 +76,7 @@
     @Before
     fun setUp() {
         surfaceEdge = SurfaceEdge(
-            CameraEffect.PREVIEW, INPUT_SIZE,
+            CameraEffect.PREVIEW, StreamSpec.builder(INPUT_SIZE).build(),
             Matrix(), true, Rect(), 0, false
         )
         fakeSurfaceTexture = SurfaceTexture(0)
@@ -86,6 +92,14 @@
         fakeSurface.release()
     }
 
+    @Test
+    fun createWithStreamSpec_canGetStreamSpec() {
+        val edge = SurfaceEdge(
+            PREVIEW, FRAME_SPEC, Matrix(), true, Rect(), 0, false
+        )
+        assertThat(edge.streamSpec).isEqualTo(FRAME_SPEC)
+    }
+
     @Test(expected = IllegalStateException::class)
     fun setProviderOnClosedEdge_throwsException() {
         surfaceEdge.close()
@@ -253,7 +267,13 @@
     private fun getSurfaceRequestHasTransform(hasCameraTransform: Boolean): Boolean {
         // Arrange.
         val surface = SurfaceEdge(
-            CameraEffect.PREVIEW, Size(640, 480), Matrix(), hasCameraTransform, Rect(), 0, false
+            CameraEffect.PREVIEW,
+            StreamSpec.builder(Size(640, 480)).build(),
+            Matrix(),
+            hasCameraTransform,
+            Rect(),
+            0,
+            false
         )
         var transformationInfo: TransformationInfo? = null
 
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
index 6dfefa5..5628d5f 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
@@ -20,12 +20,14 @@
 import android.graphics.SurfaceTexture
 import android.os.Build
 import android.os.Looper.getMainLooper
+import android.util.Range
 import android.util.Size
 import android.view.Surface
 import androidx.camera.core.CameraEffect.PREVIEW
 import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
 import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.SurfaceRequest.TransformationInfo
+import androidx.camera.core.impl.StreamSpec
 import androidx.camera.core.impl.utils.TransformUtils.is90or270
 import androidx.camera.core.impl.utils.TransformUtils.rectToSize
 import androidx.camera.core.impl.utils.TransformUtils.rotateSize
@@ -141,13 +143,15 @@
             // Assert: with transformation, the output size is cropped/rotated and the rotation
             // degrees is reset.
             val previewOutput = nodeOutput[previewOutConfig]!!
-            assertThat(previewOutput.size).isEqualTo(rectToSize(expectedCropRect))
+            assertThat(previewOutput.streamSpec.resolution).isEqualTo(rectToSize(expectedCropRect))
             assertThat(previewOutput.cropRect).isEqualTo(expectedCropRect)
             assertThat(previewOutput.rotationDegrees).isEqualTo(0)
+            assertThat(previewOutput.mirroring).isFalse()
             val videoOutput = nodeOutput[videoOutConfig]!!
-            assertThat(videoOutput.size).isEqualTo(videoOutputSize)
+            assertThat(videoOutput.streamSpec.resolution).isEqualTo(videoOutputSize)
             assertThat(videoOutput.cropRect).isEqualTo(sizeToRect(videoOutputSize))
             assertThat(videoOutput.rotationDegrees).isEqualTo(0)
+            assertThat(videoOutput.mirroring).isTrue()
 
             // Clean up.
             nodeInput.surfaceEdge.close()
@@ -166,23 +170,20 @@
     }
 
     @Test
-    fun transformInput_applyCropRotateAndMirroring_outputHasNoMirroring() {
-        for (mirroring in arrayOf(false, true)) {
-            // Arrange.
-            createSurfaceProcessorNode()
-            createInputEdge(mirroring = mirroring)
-
-            // Act.
-            val nodeOutput = node.transform(nodeInput)
-
-            // Assert: the mirroring of output is always false.
-            assertThat(nodeOutput[previewOutConfig]!!.mirroring).isFalse()
-            assertThat(nodeOutput[videoOutConfig]!!.mirroring).isFalse()
-
-            // Clean up.
-            nodeInput.surfaceEdge.close()
-            node.release()
-        }
+    fun transformInputWithFrameRate_propagatesToChildren() {
+        // Arrange: create input edge with frame rate.
+        val frameRateRange = Range.create(30, 30)
+        createSurfaceProcessorNode()
+        createInputEdge(
+            frameRateRange = frameRateRange
+        )
+        // Act.
+        val nodeOutput = node.transform(nodeInput)
+        // Assert: all outputs have the same frame rate.
+        assertThat(nodeOutput[previewOutConfig]!!.streamSpec.expectedFrameRateRange)
+            .isEqualTo(frameRateRange)
+        assertThat(nodeOutput[videoOutConfig]!!.streamSpec.expectedFrameRateRange)
+            .isEqualTo(frameRateRange)
     }
 
     @Test
@@ -206,6 +207,7 @@
         assertThat(previewTransformInfo.cropRect).isEqualTo(Rect(0, 0, 400, 600))
         assertThat(previewTransformInfo.rotationDegrees).isEqualTo(0)
         assertThat(previewSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
+        assertThat(previewSurfaceOutput.mirroring).isFalse()
 
         val videoSurfaceOutput =
             surfaceProcessorInternal.surfaceOutputs[VIDEO_CAPTURE]!! as SurfaceOutputImpl
@@ -215,6 +217,7 @@
         assertThat(videoTransformInfo.cropRect).isEqualTo(sizeToRect(VIDEO_SIZE))
         assertThat(videoTransformInfo.rotationDegrees).isEqualTo(0)
         assertThat(videoSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
+        assertThat(videoSurfaceOutput.mirroring).isTrue()
     }
 
     @Test
@@ -227,7 +230,7 @@
         provideSurfaces(nodeOutput)
         shadowOf(getMainLooper()).idle()
 
-        // Act.
+        // Act: update rotation degrees
         inputSurface.rotationDegrees = 270
         shadowOf(getMainLooper()).idle()
 
@@ -238,11 +241,20 @@
         assertThat(previewSurfaceOutput.rotationDegrees).isEqualTo(90)
         assertThat(previewTransformInfo.rotationDegrees).isEqualTo(180)
         assertThat(previewSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
+        assertThat(previewSurfaceOutput.mirroring).isFalse()
         val videoSurfaceOutput =
             surfaceProcessorInternal.surfaceOutputs[VIDEO_CAPTURE]!! as SurfaceOutputImpl
         assertThat(videoSurfaceOutput.rotationDegrees).isEqualTo(90)
         assertThat(videoTransformInfo.rotationDegrees).isEqualTo(180)
         assertThat(videoSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
+        assertThat(videoSurfaceOutput.mirroring).isTrue()
+
+        // Act: update rotation degrees
+        inputSurface.rotationDegrees = 180
+        shadowOf(getMainLooper()).idle()
+        // Assert: video rotation degrees is opposite of preview because it's not mirrored.
+        assertThat(previewTransformInfo.rotationDegrees).isEqualTo(90)
+        assertThat(videoTransformInfo.rotationDegrees).isEqualTo(270)
     }
 
     @Test
@@ -291,25 +303,27 @@
         previewCropRect: Rect = PREVIEW_CROP_RECT,
         previewRotationDegrees: Int = ROTATION_DEGREES,
         mirroring: Boolean = MIRRORING,
-        videoOutputSize: Size = VIDEO_SIZE
+        videoOutputSize: Size = VIDEO_SIZE,
+        frameRateRange: Range<Int> = StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED
     ) {
-        val surface = SurfaceEdge(
+        val inputEdge = SurfaceEdge(
             previewTarget,
-            previewSize,
+            StreamSpec.builder(previewSize).setExpectedFrameRateRange(frameRateRange).build(),
             sensorToBufferTransform,
             hasCameraTransform,
             previewCropRect,
             previewRotationDegrees,
-            mirroring
+            mirroring,
         )
         videoOutConfig = OutConfig.of(
             VIDEO_CAPTURE,
             VIDEO_CROP_RECT,
-            videoOutputSize
+            videoOutputSize,
+            true
         )
-        previewOutConfig = OutConfig.of(surface)
+        previewOutConfig = OutConfig.of(inputEdge)
         nodeInput = SurfaceProcessorNode.In.of(
-            surface,
+            inputEdge,
             listOf(previewOutConfig, videoOutConfig)
         )
     }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
new file mode 100644
index 0000000..46c8367
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.streamsharing
+
+import android.os.Build
+import androidx.camera.core.impl.UseCaseConfigFactory
+import androidx.camera.core.internal.TargetConfig.OPTION_TARGET_CLASS
+import androidx.camera.core.internal.TargetConfig.OPTION_TARGET_NAME
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeUseCase
+import androidx.camera.testing.fakes.FakeUseCaseConfigFactory
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+/**
+ * Unit tests for [StreamSharing].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class StreamSharingTest {
+
+    private val parentCamera = FakeCamera()
+    private val child1 = FakeUseCase()
+    private val child2 = FakeUseCase()
+    private val useCaseConfigFactory = FakeUseCaseConfigFactory()
+    private val camera = FakeCamera()
+    private lateinit var streamSharing: StreamSharing
+
+    @Before
+    fun setUp() {
+        streamSharing = StreamSharing(parentCamera, setOf(child1, child2), useCaseConfigFactory)
+    }
+
+    @Test
+    fun bindAndUnbindParent_propagatesToChildren() {
+        // Assert: children not bound to camera by default.
+        assertThat(child1.camera).isNull()
+        assertThat(child2.camera).isNull()
+        // Act: bind to camera.
+        streamSharing.bindToCamera(camera, null, null)
+        // Assert: children bound to the virtual camera.
+        assertThat(child1.camera).isInstanceOf(VirtualCamera::class.java)
+        assertThat(child1.mergedConfigRetrieved).isTrue()
+        assertThat(child2.camera).isInstanceOf(VirtualCamera::class.java)
+        assertThat(child2.mergedConfigRetrieved).isTrue()
+        // Act: unbind.
+        streamSharing.unbindFromCamera(camera)
+        // Assert: children not bound.
+        assertThat(child1.camera).isNull()
+        assertThat(child2.camera).isNull()
+    }
+
+    @Test
+    fun attachAndDetachParent_propagatesToChildren() {
+        // Assert: children not attached by default.
+        assertThat(child1.stateAttachedCount).isEqualTo(0)
+        assertThat(child2.stateAttachedCount).isEqualTo(0)
+        // Act: attach.
+        streamSharing.onStateAttached()
+        // Assert: children attached.
+        assertThat(child1.stateAttachedCount).isEqualTo(1)
+        assertThat(child2.stateAttachedCount).isEqualTo(1)
+        // Act: detach.
+        streamSharing.onStateDetached()
+        // Assert: children not attached.
+        assertThat(child1.stateAttachedCount).isEqualTo(0)
+        assertThat(child2.stateAttachedCount).isEqualTo(0)
+    }
+
+    @Test
+    fun getDefaultConfig_usesVideoCaptureType() {
+        val config = streamSharing.getDefaultConfig(true, useCaseConfigFactory)!!
+
+        assertThat(useCaseConfigFactory.lastRequestedCaptureType)
+            .isEqualTo(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE)
+        assertThat(
+            config.retrieveOption(
+                OPTION_TARGET_CLASS,
+                null
+            )
+        ).isEqualTo(StreamSharing::class.java)
+        assertThat(
+            config.retrieveOption(
+                OPTION_TARGET_NAME,
+                null
+            )
+        ).startsWith("androidx.camera.core.streamsharing.StreamSharing-")
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraTest.kt
new file mode 100644
index 0000000..901a70a
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraTest.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.streamsharing
+
+import android.os.Build
+import androidx.camera.core.Preview
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeUseCase
+import androidx.camera.testing.fakes.FakeUseCaseConfigFactory
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+/**
+ * Unit tests for [VirtualCamera].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class VirtualCameraTest {
+
+    private val parentCamera = FakeCamera()
+    private val preview = Preview.Builder().build()
+    private val video = FakeUseCase()
+    private val useCaseConfigFactory = FakeUseCaseConfigFactory()
+    private lateinit var virtualCamera: VirtualCamera
+
+    @Before
+    fun setUp() {
+        virtualCamera = VirtualCamera(parentCamera, setOf(preview, video), useCaseConfigFactory)
+    }
+
+    @Test
+    fun virtualCameraInheritsParentProperties() {
+        assertThat(virtualCamera.cameraState).isEqualTo(parentCamera.cameraState)
+        assertThat(virtualCamera.cameraInfo).isEqualTo(parentCamera.cameraInfo)
+        assertThat(virtualCamera.cameraControl).isEqualTo(parentCamera.cameraControl)
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-extensions-stub/camera-extensions-stub.jar b/camera/camera-extensions-stub/camera-extensions-stub.jar
deleted file mode 100644
index fe03193..0000000
--- a/camera/camera-extensions-stub/camera-extensions-stub.jar
+++ /dev/null
Binary files differ
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/Camera2OutputConfigImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/Camera2OutputConfigImpl.java
index 68de01b..61b01ef 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/Camera2OutputConfigImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/Camera2OutputConfigImpl.java
@@ -23,6 +23,8 @@
 /**
  * A config representing a {@link android.hardware.camera2.params.OutputConfiguration} where
  * Surface will be created by the information in this config.
+ *
+ * @since 1.2
  */
 @SuppressLint("UnknownNullness")
 public interface Camera2OutputConfigImpl {
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/Camera2SessionConfigImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/Camera2SessionConfigImpl.java
index d121717..b470063 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/Camera2SessionConfigImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/Camera2SessionConfigImpl.java
@@ -24,6 +24,8 @@
 
 /**
  * A config representing a {@link android.hardware.camera2.params.SessionConfiguration}
+ *
+ * @since 1.2
  */
 @SuppressLint("UnknownNullness")
 public interface Camera2SessionConfigImpl {
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/ImageProcessorImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/ImageProcessorImpl.java
index ce17c4f..6209e0c 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/ImageProcessorImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/ImageProcessorImpl.java
@@ -22,6 +22,8 @@
  * A interface to receive and process the upcoming next available Image.
  *
  * <p>Implemented by OEM.
+ *
+ * @since 1.2
  */
 @SuppressLint("UnknownNullness")
 public interface ImageProcessorImpl {
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/ImageReaderOutputConfigImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/ImageReaderOutputConfigImpl.java
index ca4dcaf..a58f8c4 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/ImageReaderOutputConfigImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/ImageReaderOutputConfigImpl.java
@@ -21,6 +21,8 @@
 
 /**
  * Surface will be created by constructing a ImageReader.
+ *
+ * @since 1.2
  */
 @SuppressLint("UnknownNullness")
 public interface ImageReaderOutputConfigImpl extends Camera2OutputConfigImpl {
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/ImageReferenceImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/ImageReferenceImpl.java
index 95f2c3b..aafba7d 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/ImageReferenceImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/ImageReferenceImpl.java
@@ -25,6 +25,8 @@
  * reaches 0.
  *
  * <p>Implemented by Camera2/CameraX.
+ *
+ * @since 1.2
  */
 @SuppressLint("UnknownNullness")
 public interface ImageReferenceImpl {
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/MultiResolutionImageReaderOutputConfigImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/MultiResolutionImageReaderOutputConfigImpl.java
index c3ad61b..ccc229d 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/MultiResolutionImageReaderOutputConfigImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/MultiResolutionImageReaderOutputConfigImpl.java
@@ -18,6 +18,8 @@
 
 /**
  * Surface will be created by constructing a MultiResolutionImageReader.
+ *
+ * @since 1.2
  */
 public interface MultiResolutionImageReaderOutputConfigImpl extends Camera2OutputConfigImpl {
     /**
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/OutputSurfaceImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/OutputSurfaceImpl.java
index f692029..fe93f85 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/OutputSurfaceImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/OutputSurfaceImpl.java
@@ -22,6 +22,8 @@
 
 /**
  * For specifying output surface of the extension.
+ *
+ * @since 1.2
  */
 @SuppressLint("UnknownNullness")
 public interface OutputSurfaceImpl {
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/RequestProcessorImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/RequestProcessorImpl.java
index 5185333..003f105 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/RequestProcessorImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/RequestProcessorImpl.java
@@ -27,6 +27,8 @@
 
 /**
  * An Interface to execute Camera2 capture requests.
+ *
+ * @since 1.2
  */
 @SuppressLint("UnknownNullness")
 public interface RequestProcessorImpl {
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java
index fabfc2b..b87f4bd 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java
@@ -56,6 +56,8 @@
  *
  * (6) {@link #deInitSession}: called when CameraCaptureSession is closed.
  * </pre>
+ *
+ * @since 1.2
  */
 @SuppressLint("UnknownNullness")
 public interface SessionProcessorImpl {
@@ -274,6 +276,8 @@
          *                             as part of this callback. Both Camera2 and CameraX guarantee
          *                             that those two settings and results are always supported and
          *                             applied by the corresponding framework.
+         *
+         * @since 1.3
          */
         void onCaptureCompleted(long timestamp, int captureSequenceId,
                 Map<CaptureResult.Key, Object> result);
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/SurfaceOutputConfigImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/SurfaceOutputConfigImpl.java
index 7b8d83c..1a23ce0 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/SurfaceOutputConfigImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/SurfaceOutputConfigImpl.java
@@ -21,6 +21,8 @@
 
 /**
  * Use Surface directly to create the OutputConfiguration.
+ *
+ * @since 1.2
  */
 @SuppressLint("UnknownNullness")
 public interface SurfaceOutputConfigImpl extends Camera2OutputConfigImpl {
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..e44b6e7 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;
@@ -23,12 +30,16 @@
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.impl.AttachedSurfaceInfo;
 import androidx.camera.core.impl.CameraDeviceSurfaceManager;
+import androidx.camera.core.impl.StreamSpec;
 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
@@ -36,23 +47,25 @@
 
     public static final Size MAX_OUTPUT_SIZE = new Size(4032, 3024); // 12.2 MP
 
-    private final Map<String, Map<Class<? extends UseCaseConfig<?>>, Size>> mDefinedResolutions =
-            new HashMap<>();
+    private final Map<String, Map<Class<? extends UseCaseConfig<?>>, StreamSpec>>
+            mDefinedStreamSpecs = new HashMap<>();
+
+    private Set<List<Integer>> mValidSurfaceCombos = createDefaultValidSurfaceCombos();
 
     /**
-     * Sets the given suggested resolutions for the specified camera Id and use case type.
+     * Sets the given suggested stream specs for the specified camera Id and use case type.
      */
-    public void setSuggestedResolution(@NonNull String cameraId,
+    public void setSuggestedStreamSpec(@NonNull String cameraId,
             @NonNull Class<? extends UseCaseConfig<?>> type,
-            @NonNull Size size) {
-        Map<Class<? extends UseCaseConfig<?>>, Size> useCaseConfigTypeToSizeMap =
-                mDefinedResolutions.get(cameraId);
-        if (useCaseConfigTypeToSizeMap == null) {
-            useCaseConfigTypeToSizeMap = new HashMap<>();
-            mDefinedResolutions.put(cameraId, useCaseConfigTypeToSizeMap);
+            @NonNull StreamSpec streamSpec) {
+        Map<Class<? extends UseCaseConfig<?>>, StreamSpec> useCaseConfigTypeToStreamSpecMap =
+                mDefinedStreamSpecs.get(cameraId);
+        if (useCaseConfigTypeToStreamSpecMap == null) {
+            useCaseConfigTypeToStreamSpecMap = new HashMap<>();
+            mDefinedStreamSpecs.put(cameraId, useCaseConfigTypeToStreamSpecMap);
         }
 
-        useCaseConfigTypeToSizeMap.put(type, size);
+        useCaseConfigTypeToStreamSpecMap.put(type, streamSpec);
     }
 
     @Override
@@ -73,25 +86,80 @@
 
     @Override
     @NonNull
-    public Map<UseCaseConfig<?>, Size> getSuggestedResolutions(
+    public Map<UseCaseConfig<?>, StreamSpec> getSuggestedStreamSpecs(
             @NonNull String cameraId,
             @NonNull List<AttachedSurfaceInfo> existingSurfaces,
             @NonNull List<UseCaseConfig<?>> newUseCaseConfigs) {
-        Map<UseCaseConfig<?>, Size> suggestedSizes = new HashMap<>();
+        checkSurfaceCombo(existingSurfaces, newUseCaseConfigs);
+        Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecs = new HashMap<>();
         for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
-            Size resolution = MAX_OUTPUT_SIZE;
-            Map<Class<? extends UseCaseConfig<?>>, Size> definedResolutions =
-                    mDefinedResolutions.get(cameraId);
-            if (definedResolutions != null) {
-                Size definedResolution = definedResolutions.get(useCaseConfig.getClass());
-                if (definedResolution != null) {
-                    resolution = definedResolution;
+            StreamSpec streamSpec = StreamSpec.builder(MAX_OUTPUT_SIZE).build();
+            Map<Class<? extends UseCaseConfig<?>>, StreamSpec> definedStreamSpecs =
+                    mDefinedStreamSpecs.get(cameraId);
+            if (definedStreamSpecs != null) {
+                StreamSpec definedStreamSpec = definedStreamSpecs.get(useCaseConfig.getClass());
+                if (definedStreamSpec != null) {
+                    streamSpec = definedStreamSpec;
                 }
             }
 
-            suggestedSizes.put(useCaseConfig, resolution);
+            suggestedStreamSpecs.put(useCaseConfig, streamSpec);
         }
 
-        return suggestedSizes;
+        return suggestedStreamSpecs;
+    }
+
+    /**
+     * 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;
+    }
+
+    public void setValidSurfaceCombos(@NonNull Set<List<Integer>> validSurfaceCombos) {
+        mValidSurfaceCombos = validSurfaceCombos;
     }
 }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.java
index 4b87e5b..e102bc6 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.java
@@ -16,15 +16,15 @@
 
 package androidx.camera.testing.fakes;
 
-import android.util.Size;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
 import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType;
@@ -39,6 +39,7 @@
     private volatile boolean mIsDetached = false;
     private final AtomicInteger mStateAttachedCount = new AtomicInteger(0);
     private final CaptureType mCaptureType;
+    private boolean mMergedConfigRetrieved = false;
 
     /**
      * Creates a new instance of a {@link FakeUseCase} with a given configuration and capture type.
@@ -72,7 +73,8 @@
     @Override
     public UseCaseConfig.Builder<?, ?, ?> getUseCaseConfigBuilder(@NonNull Config config) {
         return new FakeUseCaseConfig.Builder(config)
-                .setSessionOptionUnpacker((useCaseConfig, sessionConfigBuilder) -> { });
+                .setSessionOptionUnpacker((useCaseConfig, sessionConfigBuilder) -> {
+                });
     }
 
     /**
@@ -91,6 +93,14 @@
         return config == null ? null : getUseCaseConfigBuilder(config).getUseCaseConfig();
     }
 
+    @NonNull
+    @Override
+    protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
+            @NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
+        mMergedConfigRetrieved = true;
+        return builder.getUseCaseConfig();
+    }
+
     @Override
     public void onUnbind() {
         super.onUnbind();
@@ -104,9 +114,15 @@
     }
 
     @Override
+    public void onStateDetached() {
+        super.onStateDetached();
+        mStateAttachedCount.decrementAndGet();
+    }
+
+    @Override
     @NonNull
-    protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
-        return suggestedResolution;
+    protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
+        return suggestedStreamSpec;
     }
 
     /**
@@ -122,4 +138,11 @@
     public int getStateAttachedCount() {
         return mStateAttachedCount.get();
     }
+
+    /**
+     * Returns true if {@link #mergeConfigs} have been invoked.
+     */
+    public boolean getMergedConfigRetrieved() {
+        return mMergedConfigRetrieved;
+    }
 }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfigFactory.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfigFactory.java
index c6d68fe9..8cae469 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfigFactory.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfigFactory.java
@@ -36,6 +36,10 @@
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public final class FakeUseCaseConfigFactory implements UseCaseConfigFactory {
+
+    @NonNull
+    private CaptureType mLastRequestedCaptureType;
+
     /**
      * Returns the configuration for the given capture type, or <code>null</code> if the
      * configuration cannot be produced.
@@ -45,6 +49,7 @@
     public Config getConfig(
             @NonNull CaptureType captureType,
             @CaptureMode int captureMode) {
+        mLastRequestedCaptureType = captureType;
         MutableOptionsBundle mutableConfig = MutableOptionsBundle.create();
 
         mutableConfig.insertOption(OPTION_CAPTURE_CONFIG_UNPACKER, (config, builder) -> {});
@@ -52,4 +57,9 @@
 
         return OptionsBundle.from(mutableConfig);
     }
+
+    @NonNull
+    public CaptureType getLastRequestedCaptureType() {
+        return mLastRequestedCaptureType;
+    }
 }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/MockConsumer.java b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/MockConsumer.java
index a05d407..b3f57afc 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/MockConsumer.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/MockConsumer.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import androidx.annotation.GuardedBy;
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -47,9 +48,12 @@
 
     private static final long NO_TIMEOUT = 0;
 
+    @GuardedBy("mLock")
     private CountDownLatch mLatch;
-
+    private final Object mLock = new Object();
     private final List<T> mEventList = new ArrayList<>();
+    @NonNull
+    private List<T> mVerifyingEventList = new ArrayList<>();
     private final Map<Integer, Boolean> mIsEventVerifiedByIndex = new HashMap<>();
     private int mIndexLastVerifiedInOrder = -1;
 
@@ -58,10 +62,14 @@
     private boolean mInOrder = false;
 
     private int getMatchingEventCount() {
+        return getMatchingEventCount(mVerifyingEventList);
+    }
+
+    private int getMatchingEventCount(@NonNull List<T> eventList) {
         int count = 0;
         int startIndex = mInOrder ? mIndexLastVerifiedInOrder + 1 : 0;
-        for (int i = startIndex; i < mEventList.size(); i++) {
-            if (mClassTypeToVerify.isInstance(mEventList.get(i))) {
+        for (int i = startIndex; i < eventList.size(); i++) {
+            if (mClassTypeToVerify.isInstance(eventList.get(i))) {
                 count++;
             }
         }
@@ -70,8 +78,8 @@
 
     private int getLastVerifiedEventInOrder() {
         int count = 0;
-        for (int i = mIndexLastVerifiedInOrder + 1; i < mEventList.size(); i++) {
-            if (mClassTypeToVerify.isInstance(mEventList.get(i))) {
+        for (int i = mIndexLastVerifiedInOrder + 1; i < mVerifyingEventList.size(); i++) {
+            if (mClassTypeToVerify.isInstance(mVerifyingEventList.get(i))) {
                 count++;
             }
 
@@ -88,15 +96,19 @@
             return;
         }
 
-        for (int i = 0; i < mEventList.size(); i++) {
-            if (mClassTypeToVerify.isInstance(mEventList.get(i))) {
+        for (int i = 0; i < mVerifyingEventList.size(); i++) {
+            if (mClassTypeToVerify.isInstance(mVerifyingEventList.get(i))) {
                 mIsEventVerifiedByIndex.put(i, true);
             }
         }
     }
 
     private boolean isVerified() {
-        return mCallTimes.isSatisfied(getMatchingEventCount());
+        return isVerified(mVerifyingEventList);
+    }
+
+    private boolean isVerified(@NonNull List<T> eventList) {
+        return mCallTimes.isSatisfied(getMatchingEventCount(eventList));
     }
 
     /**
@@ -171,20 +183,30 @@
         mClassTypeToVerify = classType;
         mCallTimes = callTimes;
         mInOrder = inOrder;
+        snapshotVerifyingEventList();
 
-        if (!isVerified()) {
-            if (timeoutInMillis != NO_TIMEOUT) {
-                mLatch = new CountDownLatch(1);
-                try {
-                    assertWithMessage(
-                            "Test failed for a timeout of " + timeoutInMillis + " ms"
-                    ).that(mLatch.await(timeoutInMillis, TimeUnit.MILLISECONDS)).isTrue();
-                } catch (InterruptedException e) {
-                    throw new RuntimeException(e);
-                }
-                mLatch = null;
+        CountDownLatch latch = null;
+        boolean isVerified;
+        synchronized (mLock) {
+            isVerified = isVerified();
+            if (!isVerified && timeoutInMillis != NO_TIMEOUT) {
+                latch = mLatch = new CountDownLatch(1);
             }
+        }
+        if (latch != null) {
+            try {
+                assertWithMessage("Test failed for a timeout of " + timeoutInMillis + " ms")
+                        .that(latch.await(timeoutInMillis, TimeUnit.MILLISECONDS)).isTrue();
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } finally {
+                synchronized (mLock) {
+                    mLatch = null;
+                }
+            }
+        }
 
+        if (!isVerified) {
             assertWithMessage(
                     "accept() called " + getMatchingEventCount() + " time(s) with "
                             + classType.getSimpleName()
@@ -201,7 +223,7 @@
         }
 
         if (captor != null) {
-            captor.setArguments(mEventList);
+            captor.setArguments(new ArrayList<>(mVerifyingEventList));
         }
     }
 
@@ -212,16 +234,17 @@
      *                verification, {@code false} otherwise.
      */
     public void verifyNoMoreAcceptCalls(boolean inOrder) {
+        snapshotVerifyingEventList();
         if (inOrder) {
             assertWithMessage(
                     "There are extra accept() calls after the last in-order verification"
-            ).that(mIndexLastVerifiedInOrder).isEqualTo(mEventList.size() - 1);
+            ).that(mIndexLastVerifiedInOrder).isEqualTo(mVerifyingEventList.size() - 1);
         } else {
-            for (int i = 0; i < mEventList.size(); i++) {
+            for (int i = 0; i < mVerifyingEventList.size(); i++) {
                 assertWithMessage(
                         "There are extra accept() calls after the last verification"
                                 + "\nFirst such call is with "
-                                + mEventList.get(i).getClass().getSimpleName() + " event"
+                                + mVerifyingEventList.get(i).getClass().getSimpleName() + " event"
                 ).that(mIsEventVerifiedByIndex.get(i)).isTrue();
             }
         }
@@ -232,6 +255,7 @@
      */
     public void clearAcceptCalls() {
         mEventList.clear();
+        mVerifyingEventList.clear();
         mIsEventVerifiedByIndex.clear();
         mIndexLastVerifiedInOrder = -1;
     }
@@ -240,8 +264,16 @@
     public void accept(T event) {
         mEventList.add(event);
 
-        if (mLatch != null && isVerified()) {
-            mLatch.countDown();
+        synchronized (mLock) {
+            if (mLatch != null && isVerified(mEventList)) {
+                snapshotVerifyingEventList();
+                mLatch.countDown();
+                mLatch = null;
+            }
         }
     }
+
+    private void snapshotVerifyingEventList() {
+        mVerifyingEventList = new ArrayList<>(mEventList);
+    }
 }
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..794cd97 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,25 @@
 
 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.StreamSpec;
+import androidx.camera.core.impl.SurfaceConfig;
 import androidx.camera.core.impl.UseCaseConfig;
 
 import org.junit.Before;
@@ -36,6 +48,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,30 +74,61 @@
     @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));
+        mFakeCameraDeviceSurfaceManager.setSuggestedStreamSpec(FAKE_CAMERA_ID0,
+                mFakeUseCaseConfig.getClass(),
+                StreamSpec.builder(new Size(FAKE_WIDTH0, FAKE_HEIGHT0)).build());
+        mFakeCameraDeviceSurfaceManager.setSuggestedStreamSpec(FAKE_CAMERA_ID1,
+                mFakeUseCaseConfig.getClass(),
+                StreamSpec.builder(new Size(FAKE_WIDTH1, FAKE_HEIGHT1)).build());
 
-        mUseCaseConfigList = Collections.singletonList((UseCaseConfig<?>) mFakeUseCaseConfig);
+        mUseCaseConfigList = singletonList(mFakeUseCaseConfig);
     }
 
     @Test
-    public void canRetrieveInsertedSuggestedResolutions() {
-        Map<UseCaseConfig<?>, Size> suggestedSizesCamera0 =
-                mFakeCameraDeviceSurfaceManager.getSuggestedResolutions(FAKE_CAMERA_ID0,
+    public void validSurfaceCombination_noException() {
+        UseCaseConfig<?> preview = new FakeUseCaseConfig.Builder().getUseCaseConfig();
+        UseCaseConfig<?> analysis = new ImageAnalysis.Builder().getUseCaseConfig();
+        mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(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.getSuggestedStreamSpecs(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.getSuggestedStreamSpecs(FAKE_CAMERA_ID0,
+                Collections.emptyList(), asList(preview, video, analysis));
+    }
+
+    @Test
+    public void canRetrieveInsertedSuggestedStreamSpecs() {
+        Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecsCamera0 =
+                mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(FAKE_CAMERA_ID0,
                         Collections.emptyList(), mUseCaseConfigList);
-        Map<UseCaseConfig<?>, Size> suggestedSizesCamera1 =
-                mFakeCameraDeviceSurfaceManager.getSuggestedResolutions(FAKE_CAMERA_ID1,
+        Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecCamera1 =
+                mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(FAKE_CAMERA_ID1,
                         Collections.emptyList(), mUseCaseConfigList);
 
-        assertThat(suggestedSizesCamera0.get(mFakeUseCaseConfig)).isEqualTo(
-                new Size(FAKE_WIDTH0, FAKE_HEIGHT0));
-        assertThat(suggestedSizesCamera1.get(mFakeUseCaseConfig)).isEqualTo(
-                new Size(FAKE_WIDTH1, FAKE_HEIGHT1));
-
+        assertThat(suggestedStreamSpecsCamera0.get(mFakeUseCaseConfig)).isEqualTo(
+                StreamSpec.builder(new Size(FAKE_WIDTH0, FAKE_HEIGHT0)).build());
+        assertThat(suggestedStreamSpecCamera1.get(mFakeUseCaseConfig)).isEqualTo(
+                StreamSpec.builder(new Size(FAKE_WIDTH1, FAKE_HEIGHT1)).build());
     }
 
 }
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt
index 50b36e4..66b2c8e 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt
@@ -205,10 +205,12 @@
                     // Make sure the recording proceed for a while.
                     latchForRecordingStatus.countDown()
                 }
+
                 is VideoRecordEvent.Finalize -> {
                     finalizedEvent = event
                     latchForRecordingFinalized.countDown()
                 }
+
                 else -> {
                     // Ignore other events.
                 }
@@ -238,7 +240,7 @@
     }
 
     private fun createSurfaceProcessor(): SurfaceProcessorInternal =
-        DefaultSurfaceProcessor().apply { surfaceProcessorsToRelease.add(this) }
+        DefaultSurfaceProcessor.Factory.newInstance().apply { surfaceProcessorsToRelease.add(this) }
 
     /** Skips tests which will enable surface processing and encounter device specific issues. */
     private fun assumeSuccessfulSurfaceProcessing() {
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingFrameDropTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingFrameDropTest.kt
index 42f9ba0..3815854 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingFrameDropTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingFrameDropTest.kt
@@ -19,12 +19,10 @@
 import android.Manifest
 import android.content.Context
 import android.hardware.camera2.CameraCaptureSession
-import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.CaptureRequest
 import android.os.Build
 import android.view.Surface
 import androidx.camera.camera2.Camera2Config
-import androidx.camera.camera2.interop.Camera2CameraInfo
 import androidx.camera.camera2.interop.Camera2Interop
 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.AspectRatio
@@ -242,13 +240,14 @@
             droppedFrameFlow.asSharedFlow().collect { droppedFrames.add(it) }
         }
 
-        val camInfo = cameraSelector.filter(cameraProvider.availableCameraInfos).first()
-            .let { Camera2CameraInfo.from(it) }
         val aspectRatio = AspectRatio.RATIO_16_9
 
         // Create video capture with a recorder
-        val videoCapture = VideoCapture.withOutput(Recorder.Builder().setQualitySelector(
-            QualitySelector.from(Quality.HIGHEST)).build())
+        val videoCapture = VideoCapture.withOutput(
+            Recorder.Builder().setQualitySelector(
+                QualitySelector.from(Quality.HIGHEST)
+            ).build()
+        )
 
         // Add Preview to ensure the preview stream does not drop frames during/after recordings
         val preview = Preview.Builder()
@@ -256,31 +255,10 @@
             .apply { Camera2Interop.Extender(this).setSessionCaptureCallback(captureCallback) }
             .build()
 
-        val useCaseGroup = UseCaseGroup.Builder()
-            .addUseCase(videoCapture)
-            .addUseCase(preview)
-            .apply {
-                val hardwareLevel =
-                    camInfo.getCameraCharacteristic(
-                        CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
-                    )
-
-                if (hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY ||
-                    isSpecificDeviceOnlySupport2UseCases()
-                ) {
-                    Logger.d(
-                        TAG, "Skipping ImageCapture use case, because this device" +
-                            " doesn't support 3 use case combination" +
-                            " (Preview, Video, ImageCapture)."
-                    )
-                } else {
-                    val imageCapture = ImageCapture.Builder()
-                        .setTargetAspectRatio(aspectRatio)
-                        .setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY)
-                        .build()
-                    addUseCase(imageCapture)
-                }
-            }.build()
+        val imageCapture = ImageCapture.Builder()
+            .setTargetAspectRatio(aspectRatio)
+            .setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY)
+            .build()
 
         withContext(Dispatchers.Main) {
             val lifecycleOwner = FakeLifecycleOwner()
@@ -288,6 +266,28 @@
             preview.setSurfaceProvider(
                 SurfaceTextureProvider.createAutoDrainingSurfaceTextureProvider()
             )
+            val camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector)
+
+            val isImageCaptureSupportedAs3rdUseCase = camera.isUseCasesCombinationSupported(
+                preview,
+                videoCapture,
+                imageCapture
+            )
+            val useCaseGroup = UseCaseGroup.Builder()
+                .addUseCase(videoCapture)
+                .addUseCase(preview)
+                .apply {
+                    if (isImageCaptureSupportedAs3rdUseCase) {
+                        addUseCase(imageCapture)
+                    } else {
+                        Logger.d(
+                            TAG, "Skipping ImageCapture use case, because this device" +
+                                " doesn't support 3 use case combination" +
+                                " (Preview, Video, ImageCapture)."
+                        )
+                    }
+                }.build()
+
             cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCaseGroup)
 
             val files = mutableListOf<File>()
@@ -404,10 +404,4 @@
         val recording = start(CameraXExecutors.directExecutor(), eventListener)
         recording.use { it.apply { block(eventFlow) } }
     }
-
-    private fun isSpecificDeviceOnlySupport2UseCases(): Boolean {
-        // skip for b/263431891
-        return Build.BRAND.equals("samsung", true) &&
-            Build.MODEL.equals("SM-G930T", true)
-    }
 }
\ No newline at end of file
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/AudioSourceTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/AudioSourceTest.kt
index 5c7b847..01f6b54 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/AudioSourceTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/AudioSourceTest.kt
@@ -24,6 +24,7 @@
 import androidx.camera.video.internal.encoder.noInvocation
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
+import androidx.test.filters.RequiresDevice
 import androidx.test.filters.SdkSuppress
 import androidx.test.rule.GrantPermissionRule
 import java.util.concurrent.Callable
@@ -88,6 +89,7 @@
         }
     }
 
+    @RequiresDevice // b/264902324
     @Test
     fun canRestartAudioSource() {
         for (i in 0..2) {
@@ -106,6 +108,7 @@
         }
     }
 
+    @RequiresDevice // b/264902324
     @Test
     fun bufferProviderStateChange_acquireBufferOrNot() {
         // Arrange.
@@ -127,6 +130,7 @@
         }
     }
 
+    @RequiresDevice // b/264902324
     @Test
     fun canResetBufferProvider_beforeStarting() {
         // Arrange
@@ -146,6 +150,7 @@
         verify(localBufferFactoryInvocations, timeout(10000L).atLeast(3)).call()
     }
 
+    @RequiresDevice // b/264902324
     @Test
     fun canResetBufferProvider_afterStarting() {
         // Arrange
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 474246a..1d5fad6 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -90,6 +90,7 @@
 import androidx.camera.core.impl.Observable.Observer;
 import androidx.camera.core.impl.OptionsBundle;
 import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.Timebase;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
@@ -280,14 +281,14 @@
     @Override
     public void onStateAttached() {
         super.onStateAttached();
-        Preconditions.checkNotNull(getAttachedSurfaceResolution(), "The suggested resolution "
-                + "should be already updated and shouldn't be null.");
+        Preconditions.checkNotNull(getAttachedStreamSpec(), "The suggested stream "
+                + "specification should be already updated and shouldn't be null.");
         Preconditions.checkState(mSurfaceRequest == null, "The surface request should be null "
                 + "when VideoCapture is attached.");
         mStreamInfo = fetchObservableValue(getOutput().getStreamInfo(),
                 StreamInfo.STREAM_INFO_ANY_INACTIVE);
         mSessionConfigBuilder = createPipeline(getCameraId(),
-                (VideoCaptureConfig<T>) getCurrentConfig(), getAttachedSurfaceResolution());
+                (VideoCaptureConfig<T>) getCurrentConfig(), getAttachedStreamSpec());
         applyStreamInfoToSessionConfigBuilder(mSessionConfigBuilder, mStreamInfo);
         updateSessionConfig(mSessionConfigBuilder.build());
         // VideoCapture has to be active to apply SessionConfig's template type.
@@ -414,7 +415,7 @@
             } else {
                 surfaceRequest.updateTransformationInfo(
                         SurfaceRequest.TransformationInfo.of(cropRect, relativeRotation,
-                                targetRotation, /*hasCameraTransform=*/true));
+                                targetRotation, getHasCameraTransform()));
             }
         }
     }
@@ -452,9 +453,10 @@
     @NonNull
     private SessionConfig.Builder createPipeline(@NonNull String cameraId,
             @NonNull VideoCaptureConfig<T> config,
-            @NonNull Size resolution) {
+            @NonNull StreamSpec streamSpec) {
         Threads.checkMainThread();
         CameraInternal camera = Preconditions.checkNotNull(getCamera());
+        Size resolution = streamSpec.getResolution();
 
         // Currently, VideoCapture uses StreamInfo to handle requests for surface, so
         // handleInvalidate() is not used. But if a different approach is asked in the future,
@@ -479,9 +481,9 @@
             timebase = camera.getCameraInfoInternal().getTimebase();
             SurfaceEdge cameraEdge = new SurfaceEdge(
                     VIDEO_CAPTURE,
-                    resolution,
+                    streamSpec,
                     getSensorToBufferTransformMatrix(),
-                    /*hasCameraTransform=*/true,
+                    getHasCameraTransform(),
                     mCropRect,
                     getRelativeRotation(camera),
                     /*mirroring=*/false);
@@ -526,7 +528,7 @@
 
         SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
         sessionConfigBuilder.addErrorListener(
-                (sessionConfig, error) -> resetPipeline(cameraId, config, resolution));
+                (sessionConfig, error) -> resetPipeline(cameraId, config, streamSpec));
         if (USE_TEMPLATE_PREVIEW_BY_QUIRK) {
             sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
         }
@@ -572,7 +574,7 @@
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     void resetPipeline(@NonNull String cameraId,
             @NonNull VideoCaptureConfig<T> config,
-            @NonNull Size resolution) {
+            @NonNull StreamSpec streamSpec) {
         clearPipeline();
 
         // Ensure the attached camera has not changed before resetting.
@@ -580,7 +582,7 @@
         //  to this use case so we don't need to do this check.
         if (isCurrentCamera(cameraId)) {
             // Only reset the pipeline when the bound camera is the same.
-            mSessionConfigBuilder = createPipeline(cameraId, config, resolution);
+            mSessionConfigBuilder = createPipeline(cameraId, config, streamSpec);
             applyStreamInfoToSessionConfigBuilder(mSessionConfigBuilder, mStreamInfo);
             updateSessionConfig(mSessionConfigBuilder.build());
             notifyReset();
@@ -588,6 +590,15 @@
     }
 
     /**
+     * @hide
+     */
+    @Nullable
+    @RestrictTo(Scope.TESTS)
+    SurfaceEdge getCameraEdge() {
+        return mCameraEdge;
+    }
+
+    /**
      * Provides a base static default configuration for the VideoCapture
      *
      * <p>These values may be overridden by the implementation. They only provide a minimum set of
@@ -660,7 +671,7 @@
                 // Reset pipeline if the stream ids are different, which means there's a new
                 // surface ready to be requested.
                 resetPipeline(getCameraId(), (VideoCaptureConfig<T>) getCurrentConfig(),
-                        Preconditions.checkNotNull(getAttachedSurfaceResolution()));
+                        Preconditions.checkNotNull(getAttachedStreamSpec()));
             } else if ((currentStreamInfo.getId() != STREAM_ID_ERROR
                     && streamInfo.getId() == STREAM_ID_ERROR)
                     || (currentStreamInfo.getId() == STREAM_ID_ERROR
@@ -711,7 +722,8 @@
         if (mSurfaceProcessor != null || ENABLE_SURFACE_PROCESSING_BY_QUIRK || isCropNeeded) {
             Logger.d(TAG, "Surface processing is enabled.");
             return new SurfaceProcessorNode(requireNonNull(getCamera()),
-                    mSurfaceProcessor != null ? mSurfaceProcessor : new DefaultSurfaceProcessor());
+                    mSurfaceProcessor != null ? mSurfaceProcessor :
+                            DefaultSurfaceProcessor.Factory.newInstance());
         }
         return null;
     }
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
index 8ffbdf9..945dd35 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -39,6 +39,7 @@
 import androidx.camera.core.impl.ImageOutputConfig
 import androidx.camera.core.impl.MutableStateObservable
 import androidx.camera.core.impl.Observable
+import androidx.camera.core.impl.StreamSpec
 import androidx.camera.core.impl.Timebase
 import androidx.camera.core.impl.utils.CompareSizesByArea
 import androidx.camera.core.impl.utils.TransformUtils.rectToSize
@@ -141,6 +142,33 @@
     }
 
     @Test
+    fun setNoCameraTransform_propagatesToCameraEdge() {
+        // Arrange.
+        setupCamera()
+        createCameraUseCaseAdapter()
+        val processor = createFakeSurfaceProcessor()
+        val videoCapture = createVideoCapture(createVideoOutput(), processor = processor)
+        // Act.
+        videoCapture.setHasCameraTransform(false)
+        addAndAttachUseCases(videoCapture)
+        // Assert.
+        assertThat(videoCapture.cameraEdge!!.hasCameraTransform()).isFalse()
+    }
+
+    @Test
+    fun cameraEdgeHasTransformByDefault() {
+        // Arrange.
+        setupCamera()
+        createCameraUseCaseAdapter()
+        val processor = createFakeSurfaceProcessor()
+        val videoCapture = createVideoCapture(createVideoOutput(), processor = processor)
+        // Act.
+        addAndAttachUseCases(videoCapture)
+        // Assert.
+        assertThat(videoCapture.cameraEdge!!.hasCameraTransform()).isTrue()
+    }
+
+    @Test
     fun setTargetResolution_throwsException() {
         assertThrows(UnsupportedOperationException::class.java) {
             createVideoCapture(targetResolution = ANY_SIZE)
@@ -273,7 +301,7 @@
             Surface.ROTATION_270
         ).forEach { targetRotation ->
             // Arrange.
-            setSuggestedResolution(quality)
+            setSuggestedStreamSpec(quality)
             var surfaceRequest: SurfaceRequest? = null
             val videoOutput = createVideoOutput(
                 mediaSpec = MediaSpec.builder().configureVideo {
@@ -377,7 +405,7 @@
 
         // Camera 0 support 2160P(UHD) and 720P(HD)
         arrayOf(UHD, HD, HIGHEST, LOWEST).forEach { quality ->
-            setSuggestedResolution(quality)
+            setSuggestedStreamSpec(quality)
 
             val videoOutput = createVideoOutput(
                 mediaSpec = MediaSpec.builder().configureVideo {
@@ -412,7 +440,7 @@
             )
         )
         createCameraUseCaseAdapter()
-        setSuggestedResolution(RESOLUTION_480P)
+        setSuggestedStreamSpec(StreamSpec.builder(RESOLUTION_480P).build())
 
         val videoOutput = createVideoOutput(
             mediaSpec = MediaSpec.builder().configureVideo {
@@ -525,7 +553,7 @@
         // Arrange.
         setupCamera()
         createCameraUseCaseAdapter()
-        setSuggestedResolution(Size(639, 479))
+        setSuggestedStreamSpec(StreamSpec.builder(Size(639, 479)).build())
 
         val videoOutput = createVideoOutput()
         val videoCapture = createVideoCapture(
@@ -857,7 +885,7 @@
         // Arrange.
         setupCamera()
         createCameraUseCaseAdapter()
-        setSuggestedResolution(quality)
+        setSuggestedStreamSpec(quality)
         var surfaceRequest: SurfaceRequest? = null
         val videoOutput = createVideoOutput(
             mediaSpec = MediaSpec.builder().configureVideo {
@@ -963,6 +991,7 @@
 
     private fun createVideoCapture(
         videoOutput: VideoOutput = createVideoOutput(),
+        hasCameraTransform: Boolean = true,
         targetRotation: Int? = null,
         targetResolution: Size? = null,
         processor: SurfaceProcessorInternal? = null,
@@ -976,19 +1005,20 @@
             setVideoEncoderInfoFinder(videoEncoderInfoFinder)
         }.build().apply {
             setProcessor(processor)
+            setHasCameraTransform(hasCameraTransform)
         }
 
     private fun createFakeSurfaceProcessor() = FakeSurfaceProcessorInternal(mainThreadExecutor())
 
-    private fun setSuggestedResolution(quality: Quality) {
-        setSuggestedResolution(CAMERA_0_QUALITY_SIZE[quality]!!)
+    private fun setSuggestedStreamSpec(quality: Quality) {
+        setSuggestedStreamSpec(StreamSpec.builder(CAMERA_0_QUALITY_SIZE[quality]!!).build())
     }
 
-    private fun setSuggestedResolution(resolution: Size) {
-        surfaceManager.setSuggestedResolution(
+    private fun setSuggestedStreamSpec(streamSpec: StreamSpec) {
+        surfaceManager.setSuggestedStreamSpec(
             CAMERA_ID_0,
             VideoCaptureConfig::class.java,
-            resolution
+            streamSpec
         )
     }
 
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
index 1c2f921..a63882a 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
@@ -435,6 +435,10 @@
     @Test
     @SdkSuppress(minSdkVersion = 21, maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun canRecordToFile_whenPauseAndResumeInTheMiddle() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         val pauseTimes = 1
         val resumeTimes = 1
 
diff --git a/camera/camera-viewfinder/api/1.1.0-beta04.txt b/camera/camera-viewfinder/api/1.1.0-beta04.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/camera/camera-viewfinder/api/1.1.0-beta04.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/camera/camera-viewfinder/api/1.2.0-beta01.txt b/camera/camera-viewfinder/api/1.2.0-beta01.txt
deleted file mode 100644
index d452929..0000000
--- a/camera/camera-viewfinder/api/1.2.0-beta01.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-// Signature format: 4.0
-package androidx.camera.viewfinder {
-
-  @RequiresApi(21) public final class CameraViewfinder extends android.widget.FrameLayout {
-    ctor @UiThread public CameraViewfinder(android.content.Context);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int, int);
-    method @UiThread public android.graphics.Bitmap? getBitmap();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ScaleType getScaleType();
-    method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
-    method @UiThread public void setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode);
-    method @UiThread public void setScaleType(androidx.camera.viewfinder.CameraViewfinder.ScaleType);
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ScaleType {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_START;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_START;
-  }
-
-  @RequiresApi(21) public class ViewfinderSurfaceRequest {
-    ctor public ViewfinderSurfaceRequest(android.util.Size, android.hardware.camera2.CameraCharacteristics);
-    method public android.util.Size getResolution();
-    method public int getSensorOrientation();
-    method public boolean isFrontCamera();
-    method public boolean isLegacyDevice();
-    method public void markSurfaceSafeToRelease();
-  }
-
-}
-
diff --git a/camera/camera-viewfinder/api/1.2.0-beta02.txt b/camera/camera-viewfinder/api/1.2.0-beta02.txt
deleted file mode 100644
index d452929..0000000
--- a/camera/camera-viewfinder/api/1.2.0-beta02.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-// Signature format: 4.0
-package androidx.camera.viewfinder {
-
-  @RequiresApi(21) public final class CameraViewfinder extends android.widget.FrameLayout {
-    ctor @UiThread public CameraViewfinder(android.content.Context);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int, int);
-    method @UiThread public android.graphics.Bitmap? getBitmap();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ScaleType getScaleType();
-    method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
-    method @UiThread public void setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode);
-    method @UiThread public void setScaleType(androidx.camera.viewfinder.CameraViewfinder.ScaleType);
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ScaleType {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_START;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_START;
-  }
-
-  @RequiresApi(21) public class ViewfinderSurfaceRequest {
-    ctor public ViewfinderSurfaceRequest(android.util.Size, android.hardware.camera2.CameraCharacteristics);
-    method public android.util.Size getResolution();
-    method public int getSensorOrientation();
-    method public boolean isFrontCamera();
-    method public boolean isLegacyDevice();
-    method public void markSurfaceSafeToRelease();
-  }
-
-}
-
diff --git a/camera/camera-viewfinder/api/1.2.0-beta03.txt b/camera/camera-viewfinder/api/1.2.0-beta03.txt
index d452929..04cecbd 100644
--- a/camera/camera-viewfinder/api/1.2.0-beta03.txt
+++ b/camera/camera-viewfinder/api/1.2.0-beta03.txt
@@ -1,41 +1,2 @@
 // Signature format: 4.0
-package androidx.camera.viewfinder {
-
-  @RequiresApi(21) public final class CameraViewfinder extends android.widget.FrameLayout {
-    ctor @UiThread public CameraViewfinder(android.content.Context);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int, int);
-    method @UiThread public android.graphics.Bitmap? getBitmap();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ScaleType getScaleType();
-    method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
-    method @UiThread public void setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode);
-    method @UiThread public void setScaleType(androidx.camera.viewfinder.CameraViewfinder.ScaleType);
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ScaleType {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_START;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_START;
-  }
-
-  @RequiresApi(21) public class ViewfinderSurfaceRequest {
-    ctor public ViewfinderSurfaceRequest(android.util.Size, android.hardware.camera2.CameraCharacteristics);
-    method public android.util.Size getResolution();
-    method public int getSensorOrientation();
-    method public boolean isFrontCamera();
-    method public boolean isLegacyDevice();
-    method public void markSurfaceSafeToRelease();
-  }
-
-}
 
diff --git a/camera/camera-viewfinder/api/current.txt b/camera/camera-viewfinder/api/current.txt
index d452929..48504db 100644
--- a/camera/camera-viewfinder/api/current.txt
+++ b/camera/camera-viewfinder/api/current.txt
@@ -10,7 +10,6 @@
     method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
     method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ScaleType getScaleType();
     method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
-    method @UiThread public void setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode);
     method @UiThread public void setScaleType(androidx.camera.viewfinder.CameraViewfinder.ScaleType);
   }
 
@@ -28,14 +27,30 @@
     enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_START;
   }
 
+  @RequiresApi(21) public final class CameraViewfinderExt {
+    method public suspend Object? requestSurface(androidx.camera.viewfinder.CameraViewfinder, androidx.camera.viewfinder.ViewfinderSurfaceRequest viewfinderSurfaceRequest, kotlin.coroutines.Continuation<? super android.view.Surface>);
+    field public static final androidx.camera.viewfinder.CameraViewfinderExt INSTANCE;
+  }
+
   @RequiresApi(21) public class ViewfinderSurfaceRequest {
-    ctor public ViewfinderSurfaceRequest(android.util.Size, android.hardware.camera2.CameraCharacteristics);
+    method public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode? getImplementationMode();
+    method public int getLensFacing();
     method public android.util.Size getResolution();
     method public int getSensorOrientation();
-    method public boolean isFrontCamera();
-    method public boolean isLegacyDevice();
     method public void markSurfaceSafeToRelease();
   }
 
+  public static final class ViewfinderSurfaceRequest.Builder {
+    ctor public ViewfinderSurfaceRequest.Builder(android.util.Size);
+    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest build();
+    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode);
+    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setLensFacing(int);
+    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setSensorOrientation(int);
+  }
+
+  public final class ViewfinderSurfaceRequestUtil {
+    method @RequiresApi(21) public static androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder populateFromCharacteristics(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder, android.hardware.camera2.CameraCharacteristics cameraCharacteristics);
+  }
+
 }
 
diff --git a/camera/camera-viewfinder/api/public_plus_experimental_1.1.0-beta03.txt b/camera/camera-viewfinder/api/public_plus_experimental_1.1.0-beta03.txt
deleted file mode 100644
index 69b776e..0000000
--- a/camera/camera-viewfinder/api/public_plus_experimental_1.1.0-beta03.txt
+++ /dev/null
@@ -1,44 +0,0 @@
-// Signature format: 4.0
-package @androidx.camera.viewfinder.ExperimentalViewfinder androidx.camera.viewfinder {
-
-  @RequiresApi(21) public final class CameraViewfinder extends android.widget.FrameLayout {
-    ctor @UiThread public CameraViewfinder(android.content.Context);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int, int);
-    method @UiThread public android.graphics.Bitmap? getBitmap();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ScaleType getScaleType();
-    method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
-    method @UiThread public void setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode);
-    method @UiThread public void setScaleType(androidx.camera.viewfinder.CameraViewfinder.ScaleType);
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ScaleType {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_START;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_START;
-  }
-
-  @RequiresOptIn(level=androidx.annotation.RequiresOptIn.Level.ERROR) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalViewfinder {
-  }
-
-  @RequiresApi(21) public class ViewfinderSurfaceRequest {
-    ctor public ViewfinderSurfaceRequest(android.util.Size, android.hardware.camera2.CameraCharacteristics);
-    method public android.util.Size getResolution();
-    method public int getSensorOrientation();
-    method public boolean isFrontCamera();
-    method public boolean isLegacyDevice();
-    method public void markSurfaceSafeToRelease();
-  }
-
-}
-
diff --git a/camera/camera-viewfinder/api/public_plus_experimental_1.1.0-beta04.txt b/camera/camera-viewfinder/api/public_plus_experimental_1.1.0-beta04.txt
deleted file mode 100644
index 69b776e..0000000
--- a/camera/camera-viewfinder/api/public_plus_experimental_1.1.0-beta04.txt
+++ /dev/null
@@ -1,44 +0,0 @@
-// Signature format: 4.0
-package @androidx.camera.viewfinder.ExperimentalViewfinder androidx.camera.viewfinder {
-
-  @RequiresApi(21) public final class CameraViewfinder extends android.widget.FrameLayout {
-    ctor @UiThread public CameraViewfinder(android.content.Context);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int, int);
-    method @UiThread public android.graphics.Bitmap? getBitmap();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ScaleType getScaleType();
-    method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
-    method @UiThread public void setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode);
-    method @UiThread public void setScaleType(androidx.camera.viewfinder.CameraViewfinder.ScaleType);
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ScaleType {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_START;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_START;
-  }
-
-  @RequiresOptIn(level=androidx.annotation.RequiresOptIn.Level.ERROR) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalViewfinder {
-  }
-
-  @RequiresApi(21) public class ViewfinderSurfaceRequest {
-    ctor public ViewfinderSurfaceRequest(android.util.Size, android.hardware.camera2.CameraCharacteristics);
-    method public android.util.Size getResolution();
-    method public int getSensorOrientation();
-    method public boolean isFrontCamera();
-    method public boolean isLegacyDevice();
-    method public void markSurfaceSafeToRelease();
-  }
-
-}
-
diff --git a/camera/camera-viewfinder/api/public_plus_experimental_1.2.0-beta01.txt b/camera/camera-viewfinder/api/public_plus_experimental_1.2.0-beta01.txt
deleted file mode 100644
index d452929..0000000
--- a/camera/camera-viewfinder/api/public_plus_experimental_1.2.0-beta01.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-// Signature format: 4.0
-package androidx.camera.viewfinder {
-
-  @RequiresApi(21) public final class CameraViewfinder extends android.widget.FrameLayout {
-    ctor @UiThread public CameraViewfinder(android.content.Context);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int, int);
-    method @UiThread public android.graphics.Bitmap? getBitmap();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ScaleType getScaleType();
-    method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
-    method @UiThread public void setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode);
-    method @UiThread public void setScaleType(androidx.camera.viewfinder.CameraViewfinder.ScaleType);
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ScaleType {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_START;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_START;
-  }
-
-  @RequiresApi(21) public class ViewfinderSurfaceRequest {
-    ctor public ViewfinderSurfaceRequest(android.util.Size, android.hardware.camera2.CameraCharacteristics);
-    method public android.util.Size getResolution();
-    method public int getSensorOrientation();
-    method public boolean isFrontCamera();
-    method public boolean isLegacyDevice();
-    method public void markSurfaceSafeToRelease();
-  }
-
-}
-
diff --git a/camera/camera-viewfinder/api/public_plus_experimental_1.2.0-beta02.txt b/camera/camera-viewfinder/api/public_plus_experimental_1.2.0-beta02.txt
deleted file mode 100644
index d452929..0000000
--- a/camera/camera-viewfinder/api/public_plus_experimental_1.2.0-beta02.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-// Signature format: 4.0
-package androidx.camera.viewfinder {
-
-  @RequiresApi(21) public final class CameraViewfinder extends android.widget.FrameLayout {
-    ctor @UiThread public CameraViewfinder(android.content.Context);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int, int);
-    method @UiThread public android.graphics.Bitmap? getBitmap();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ScaleType getScaleType();
-    method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
-    method @UiThread public void setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode);
-    method @UiThread public void setScaleType(androidx.camera.viewfinder.CameraViewfinder.ScaleType);
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ScaleType {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_START;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_START;
-  }
-
-  @RequiresApi(21) public class ViewfinderSurfaceRequest {
-    ctor public ViewfinderSurfaceRequest(android.util.Size, android.hardware.camera2.CameraCharacteristics);
-    method public android.util.Size getResolution();
-    method public int getSensorOrientation();
-    method public boolean isFrontCamera();
-    method public boolean isLegacyDevice();
-    method public void markSurfaceSafeToRelease();
-  }
-
-}
-
diff --git a/camera/camera-viewfinder/api/public_plus_experimental_1.2.0-beta03.txt b/camera/camera-viewfinder/api/public_plus_experimental_1.2.0-beta03.txt
index d452929..04cecbd 100644
--- a/camera/camera-viewfinder/api/public_plus_experimental_1.2.0-beta03.txt
+++ b/camera/camera-viewfinder/api/public_plus_experimental_1.2.0-beta03.txt
@@ -1,41 +1,2 @@
 // Signature format: 4.0
-package androidx.camera.viewfinder {
-
-  @RequiresApi(21) public final class CameraViewfinder extends android.widget.FrameLayout {
-    ctor @UiThread public CameraViewfinder(android.content.Context);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int, int);
-    method @UiThread public android.graphics.Bitmap? getBitmap();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ScaleType getScaleType();
-    method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
-    method @UiThread public void setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode);
-    method @UiThread public void setScaleType(androidx.camera.viewfinder.CameraViewfinder.ScaleType);
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ScaleType {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_START;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_START;
-  }
-
-  @RequiresApi(21) public class ViewfinderSurfaceRequest {
-    ctor public ViewfinderSurfaceRequest(android.util.Size, android.hardware.camera2.CameraCharacteristics);
-    method public android.util.Size getResolution();
-    method public int getSensorOrientation();
-    method public boolean isFrontCamera();
-    method public boolean isLegacyDevice();
-    method public void markSurfaceSafeToRelease();
-  }
-
-}
 
diff --git a/camera/camera-viewfinder/api/public_plus_experimental_current.txt b/camera/camera-viewfinder/api/public_plus_experimental_current.txt
index d452929..48504db 100644
--- a/camera/camera-viewfinder/api/public_plus_experimental_current.txt
+++ b/camera/camera-viewfinder/api/public_plus_experimental_current.txt
@@ -10,7 +10,6 @@
     method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
     method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ScaleType getScaleType();
     method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
-    method @UiThread public void setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode);
     method @UiThread public void setScaleType(androidx.camera.viewfinder.CameraViewfinder.ScaleType);
   }
 
@@ -28,14 +27,30 @@
     enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_START;
   }
 
+  @RequiresApi(21) public final class CameraViewfinderExt {
+    method public suspend Object? requestSurface(androidx.camera.viewfinder.CameraViewfinder, androidx.camera.viewfinder.ViewfinderSurfaceRequest viewfinderSurfaceRequest, kotlin.coroutines.Continuation<? super android.view.Surface>);
+    field public static final androidx.camera.viewfinder.CameraViewfinderExt INSTANCE;
+  }
+
   @RequiresApi(21) public class ViewfinderSurfaceRequest {
-    ctor public ViewfinderSurfaceRequest(android.util.Size, android.hardware.camera2.CameraCharacteristics);
+    method public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode? getImplementationMode();
+    method public int getLensFacing();
     method public android.util.Size getResolution();
     method public int getSensorOrientation();
-    method public boolean isFrontCamera();
-    method public boolean isLegacyDevice();
     method public void markSurfaceSafeToRelease();
   }
 
+  public static final class ViewfinderSurfaceRequest.Builder {
+    ctor public ViewfinderSurfaceRequest.Builder(android.util.Size);
+    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest build();
+    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode);
+    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setLensFacing(int);
+    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setSensorOrientation(int);
+  }
+
+  public final class ViewfinderSurfaceRequestUtil {
+    method @RequiresApi(21) public static androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder populateFromCharacteristics(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder, android.hardware.camera2.CameraCharacteristics cameraCharacteristics);
+  }
+
 }
 
diff --git a/camera/camera-viewfinder/api/res-1.1.0-beta04.txt b/camera/camera-viewfinder/api/res-1.1.0-beta04.txt
deleted file mode 100644
index e69de29..0000000
--- a/camera/camera-viewfinder/api/res-1.1.0-beta04.txt
+++ /dev/null
diff --git a/camera/camera-viewfinder/api/restricted_1.1.0-beta03.txt b/camera/camera-viewfinder/api/restricted_1.1.0-beta03.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/camera/camera-viewfinder/api/restricted_1.1.0-beta03.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/camera/camera-viewfinder/api/restricted_1.1.0-beta04.txt b/camera/camera-viewfinder/api/restricted_1.1.0-beta04.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/camera/camera-viewfinder/api/restricted_1.1.0-beta04.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/camera/camera-viewfinder/api/restricted_1.2.0-beta01.txt b/camera/camera-viewfinder/api/restricted_1.2.0-beta01.txt
deleted file mode 100644
index d452929..0000000
--- a/camera/camera-viewfinder/api/restricted_1.2.0-beta01.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-// Signature format: 4.0
-package androidx.camera.viewfinder {
-
-  @RequiresApi(21) public final class CameraViewfinder extends android.widget.FrameLayout {
-    ctor @UiThread public CameraViewfinder(android.content.Context);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int, int);
-    method @UiThread public android.graphics.Bitmap? getBitmap();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ScaleType getScaleType();
-    method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
-    method @UiThread public void setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode);
-    method @UiThread public void setScaleType(androidx.camera.viewfinder.CameraViewfinder.ScaleType);
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ScaleType {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_START;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_START;
-  }
-
-  @RequiresApi(21) public class ViewfinderSurfaceRequest {
-    ctor public ViewfinderSurfaceRequest(android.util.Size, android.hardware.camera2.CameraCharacteristics);
-    method public android.util.Size getResolution();
-    method public int getSensorOrientation();
-    method public boolean isFrontCamera();
-    method public boolean isLegacyDevice();
-    method public void markSurfaceSafeToRelease();
-  }
-
-}
-
diff --git a/camera/camera-viewfinder/api/restricted_1.2.0-beta02.txt b/camera/camera-viewfinder/api/restricted_1.2.0-beta02.txt
deleted file mode 100644
index d452929..0000000
--- a/camera/camera-viewfinder/api/restricted_1.2.0-beta02.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-// Signature format: 4.0
-package androidx.camera.viewfinder {
-
-  @RequiresApi(21) public final class CameraViewfinder extends android.widget.FrameLayout {
-    ctor @UiThread public CameraViewfinder(android.content.Context);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int, int);
-    method @UiThread public android.graphics.Bitmap? getBitmap();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ScaleType getScaleType();
-    method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
-    method @UiThread public void setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode);
-    method @UiThread public void setScaleType(androidx.camera.viewfinder.CameraViewfinder.ScaleType);
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ScaleType {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_START;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_START;
-  }
-
-  @RequiresApi(21) public class ViewfinderSurfaceRequest {
-    ctor public ViewfinderSurfaceRequest(android.util.Size, android.hardware.camera2.CameraCharacteristics);
-    method public android.util.Size getResolution();
-    method public int getSensorOrientation();
-    method public boolean isFrontCamera();
-    method public boolean isLegacyDevice();
-    method public void markSurfaceSafeToRelease();
-  }
-
-}
-
diff --git a/camera/camera-viewfinder/api/restricted_1.2.0-beta03.txt b/camera/camera-viewfinder/api/restricted_1.2.0-beta03.txt
index d452929..04cecbd 100644
--- a/camera/camera-viewfinder/api/restricted_1.2.0-beta03.txt
+++ b/camera/camera-viewfinder/api/restricted_1.2.0-beta03.txt
@@ -1,41 +1,2 @@
 // Signature format: 4.0
-package androidx.camera.viewfinder {
-
-  @RequiresApi(21) public final class CameraViewfinder extends android.widget.FrameLayout {
-    ctor @UiThread public CameraViewfinder(android.content.Context);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int);
-    ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int, int);
-    method @UiThread public android.graphics.Bitmap? getBitmap();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ScaleType getScaleType();
-    method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
-    method @UiThread public void setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode);
-    method @UiThread public void setScaleType(androidx.camera.viewfinder.CameraViewfinder.ScaleType);
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
-  }
-
-  @RequiresApi(21) public enum CameraViewfinder.ScaleType {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FILL_START;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_CENTER;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_END;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_START;
-  }
-
-  @RequiresApi(21) public class ViewfinderSurfaceRequest {
-    ctor public ViewfinderSurfaceRequest(android.util.Size, android.hardware.camera2.CameraCharacteristics);
-    method public android.util.Size getResolution();
-    method public int getSensorOrientation();
-    method public boolean isFrontCamera();
-    method public boolean isLegacyDevice();
-    method public void markSurfaceSafeToRelease();
-  }
-
-}
 
diff --git a/camera/camera-viewfinder/api/restricted_current.txt b/camera/camera-viewfinder/api/restricted_current.txt
index d452929..48504db 100644
--- a/camera/camera-viewfinder/api/restricted_current.txt
+++ b/camera/camera-viewfinder/api/restricted_current.txt
@@ -10,7 +10,6 @@
     method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
     method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ScaleType getScaleType();
     method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
-    method @UiThread public void setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode);
     method @UiThread public void setScaleType(androidx.camera.viewfinder.CameraViewfinder.ScaleType);
   }
 
@@ -28,14 +27,30 @@
     enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_START;
   }
 
+  @RequiresApi(21) public final class CameraViewfinderExt {
+    method public suspend Object? requestSurface(androidx.camera.viewfinder.CameraViewfinder, androidx.camera.viewfinder.ViewfinderSurfaceRequest viewfinderSurfaceRequest, kotlin.coroutines.Continuation<? super android.view.Surface>);
+    field public static final androidx.camera.viewfinder.CameraViewfinderExt INSTANCE;
+  }
+
   @RequiresApi(21) public class ViewfinderSurfaceRequest {
-    ctor public ViewfinderSurfaceRequest(android.util.Size, android.hardware.camera2.CameraCharacteristics);
+    method public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode? getImplementationMode();
+    method public int getLensFacing();
     method public android.util.Size getResolution();
     method public int getSensorOrientation();
-    method public boolean isFrontCamera();
-    method public boolean isLegacyDevice();
     method public void markSurfaceSafeToRelease();
   }
 
+  public static final class ViewfinderSurfaceRequest.Builder {
+    ctor public ViewfinderSurfaceRequest.Builder(android.util.Size);
+    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest build();
+    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode);
+    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setLensFacing(int);
+    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setSensorOrientation(int);
+  }
+
+  public final class ViewfinderSurfaceRequestUtil {
+    method @RequiresApi(21) public static androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder populateFromCharacteristics(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder, android.hardware.camera2.CameraCharacteristics cameraCharacteristics);
+  }
+
 }
 
diff --git a/camera/camera-viewfinder/build.gradle b/camera/camera-viewfinder/build.gradle
index 449b256..ea7bfde 100644
--- a/camera/camera-viewfinder/build.gradle
+++ b/camera/camera-viewfinder/build.gradle
@@ -29,6 +29,7 @@
     implementation(libs.guavaListenableFuture)
     implementation("androidx.core:core:1.3.2")
     implementation("androidx.concurrent:concurrent-futures:1.0.0")
+    implementation(project(":concurrent:concurrent-futures-ktx"))
     implementation(libs.autoValueAnnotations)
     implementation("androidx.appcompat:appcompat:1.1.0")
     implementation("androidx.test.espresso:espresso-idling-resource:3.1.0")
diff --git a/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/CameraViewfinderBitmapTest.kt b/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/CameraViewfinderBitmapTest.kt
index 67ea991..25b8313 100644
--- a/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/CameraViewfinderBitmapTest.kt
+++ b/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/CameraViewfinderBitmapTest.kt
@@ -21,8 +21,6 @@
 import android.hardware.camera2.CameraManager
 import android.util.Size
 import android.view.Surface
-import androidx.camera.viewfinder.CameraViewfinder.ImplementationMode.COMPATIBLE
-import androidx.camera.viewfinder.CameraViewfinder.ImplementationMode.PERFORMANCE
 import androidx.camera.viewfinder.CameraViewfinder.ScaleType.FILL_CENTER
 import androidx.camera.viewfinder.internal.utils.futures.FutureCallback
 import androidx.camera.viewfinder.internal.utils.futures.Futures
@@ -79,7 +77,9 @@
         Assume.assumeTrue("No cameras found on device.", cameraIds.isNotEmpty())
         val cameraId = cameraIds[0]
         val characteristics = cameraManager.getCameraCharacteristics(cameraId)
-        mSurfaceRequest = ViewfinderSurfaceRequest(ANY_SIZE, characteristics)
+        mSurfaceRequest = ViewfinderSurfaceRequest.Builder(ANY_SIZE)
+            .populateFromCharacteristics(characteristics)
+            .build()
     }
 
     @After
@@ -90,7 +90,7 @@
     @Throws(Throwable::class)
     fun bitmapNotNull_whenViewfinderIsDisplaying_surfaceView() {
         // Arrange
-        val viewfinder: CameraViewfinder = setUpViewfinder(PERFORMANCE, FILL_CENTER)
+        val viewfinder: CameraViewfinder = setUpViewfinder(FILL_CENTER)
 
         // assert
         runOnMainThread(Runnable {
@@ -119,7 +119,7 @@
     @Throws(Throwable::class)
     fun bitmapNotNull_whenViewfinderIsDisplaying_textureView() {
         // Arrange
-        val viewfinder: CameraViewfinder = setUpViewfinder(COMPATIBLE, FILL_CENTER)
+        val viewfinder: CameraViewfinder = setUpViewfinder(FILL_CENTER)
 
         // assert
         runOnMainThread(Runnable {
@@ -145,7 +145,6 @@
     }
 
     private fun setUpViewfinder(
-        mode: CameraViewfinder.ImplementationMode,
         scaleType: CameraViewfinder.ScaleType
     ): CameraViewfinder {
         val viewfinderAtomicReference: AtomicReference<CameraViewfinder> =
@@ -153,7 +152,6 @@
         runOnMainThread {
             val viewfiner =
                 CameraViewfinder(ApplicationProvider.getApplicationContext<Context>())
-            viewfiner.setImplementationMode(mode)
             viewfiner.setScaleType(scaleType)
             mActivityRule.getScenario().onActivity(
                 ActivityAction<FakeActivity> { activity: FakeActivity ->
diff --git a/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/SurfaceViewImplementationTest.kt b/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/SurfaceViewImplementationTest.kt
index 16e57c2..7e0012c 100644
--- a/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/SurfaceViewImplementationTest.kt
+++ b/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/SurfaceViewImplementationTest.kt
@@ -72,7 +72,9 @@
         Assume.assumeTrue("No cameras found on device.", cameraIds.isNotEmpty())
         val cameraId = cameraIds[0]
         val characteristics = cameraManager.getCameraCharacteristics(cameraId)
-        mSurfaceRequest = ViewfinderSurfaceRequest(ANY_SIZE, characteristics)
+        mSurfaceRequest = ViewfinderSurfaceRequest.Builder(ANY_SIZE)
+            .populateFromCharacteristics(characteristics)
+            .build()
         mImplementation = SurfaceViewImplementation(mParent, ViewfinderTransformation())
     }
 
diff --git a/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/TextureViewImplementationTest.kt b/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/TextureViewImplementationTest.kt
index 3b6bae6..dbb8587 100644
--- a/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/TextureViewImplementationTest.kt
+++ b/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/TextureViewImplementationTest.kt
@@ -56,7 +56,9 @@
                 val cameraId = cameraIds[0]
                 val characteristics = cameraManager.getCameraCharacteristics(cameraId)
                 _surfaceRequest =
-                    ViewfinderSurfaceRequest(ANY_SIZE, characteristics)
+                    ViewfinderSurfaceRequest.Builder(ANY_SIZE)
+                        .populateFromCharacteristics(characteristics)
+                        .build()
             }
             return _surfaceRequest!!
         }
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinder.java b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinder.java
index 03c4488..9a438c0 100644
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinder.java
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinder.java
@@ -41,6 +41,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.UiThread;
+import androidx.annotation.VisibleForTesting;
 import androidx.camera.viewfinder.internal.quirk.DeviceQuirks;
 import androidx.camera.viewfinder.internal.quirk.SurfaceViewNotCroppedByParentQuirk;
 import androidx.camera.viewfinder.internal.quirk.SurfaceViewStretchedQuirk;
@@ -79,7 +80,7 @@
     @NonNull
     private final Looper mRequiredLooper = Looper.myLooper();
 
-    @NonNull ImplementationMode mImplementationMode = DEFAULT_IMPL_MODE;
+    @NonNull ImplementationMode mImplementationMode;
 
     // Synthetic access
     @SuppressWarnings("WeakerAccess")
@@ -116,8 +117,11 @@
             }
             Logger.d(TAG, "Surface requested by Viewfinder.");
 
-            mImplementation = shouldUseTextureView(
-                    surfaceRequest.isLegacyDevice(), mImplementationMode)
+            if (surfaceRequest.getImplementationMode() != null) {
+                mImplementationMode = surfaceRequest.getImplementationMode();
+            }
+
+            mImplementation = shouldUseTextureView(mImplementationMode)
                     ? new TextureViewImplementation(
                             CameraViewfinder.this, mViewfinderTransformation)
                     : new SurfaceViewImplementation(
@@ -130,10 +134,12 @@
                 mViewfinderTransformation.setTransformationInfo(
                         createTransformInfo(surfaceRequest.getResolution(),
                                 display,
-                                surfaceRequest.isFrontCamera(),
+                                surfaceRequest.getLensFacing()
+                                        == CameraCharacteristics.LENS_FACING_FRONT,
                                 surfaceRequest.getSensorOrientation()),
                         surfaceRequest.getResolution(),
-                        surfaceRequest.isFrontCamera());
+                        surfaceRequest.getLensFacing()
+                                == CameraCharacteristics.LENS_FACING_FRONT);
                 redrawViewfinder();
             }
         }
@@ -176,7 +182,7 @@
             int implementationModeId =
                     attributes.getInteger(R.styleable.Viewfinder_implementationMode,
                             DEFAULT_IMPL_MODE.getId());
-            setImplementationMode(ImplementationMode.fromId(implementationModeId));
+            mImplementationMode = ImplementationMode.fromId(implementationModeId);
         } finally {
             attributes.recycle();
         }
@@ -189,36 +195,15 @@
     }
 
     /**
-     * Sets the {@link ImplementationMode} for the {@link CameraViewfinder}.
-     *
-     * <p> This value can also be set in the layout XML file via the {@code app:implementationMode}
-     * attribute.
-     *
-     * <p> {@link CameraViewfinder} displays the viewfinder with a {@link TextureView} when the
-     * mode is {@link ImplementationMode#COMPATIBLE}, and tries to use a {@link SurfaceView} if
-     * it is {@link ImplementationMode#PERFORMANCE} when possible, which depends on the device's
-     * attributes (e.g. API level). If not set, the default mode is
-     * {@link ImplementationMode#PERFORMANCE}.
-     *
-     * <p> This method should be called after {@link CameraViewfinder} is inflated and before
-     * {@link CameraViewfinder#requestSurfaceAsync(ViewfinderSurfaceRequest)}. If a new
-     * {@link ImplementationMode} is set, the capture session needs to be recreated and new
-     * surface request needs to be sent to make it effective.
-     *
-     * @param implementationMode The {@link ImplementationMode} to apply to the viewfinder.
-     * @attr name app:implementationMode
-     */
-    @UiThread
-    public void setImplementationMode(@NonNull final ImplementationMode implementationMode) {
-        checkUiThread();
-        mImplementationMode = implementationMode;
-    }
-
-    /**
      * Returns the {@link ImplementationMode}.
      *
-     * <p> If nothing is set via {@link #setImplementationMode}, the default
-     * value is {@link ImplementationMode#PERFORMANCE}.
+     * <p> For each {@link ViewfinderSurfaceRequest} sent to {@link CameraViewfinder}, the
+     * {@link ImplementationMode} set in the {@link ViewfinderSurfaceRequest} will be used first.
+     * If it's not set, the {@code app:implementationMode} in the layout xml will be used. If
+     * it's not set in the layout xml, the default value {@link ImplementationMode#PERFORMANCE}
+     * will be used. Each {@link ViewfinderSurfaceRequest sent to {@link CameraViewfinder} can
+     * override the {@link ImplementationMode} once it has set the
+     * {@link ImplementationMode}.
      *
      * @return The {@link ImplementationMode} for {@link CameraViewfinder}.
      */
@@ -376,16 +361,15 @@
         stopListeningToDisplayChange();
     }
 
-    // Synthetic access
-    @SuppressWarnings("WeakerAccess")
-    static boolean shouldUseTextureView(
-            boolean isLegacyDevice,
-            @NonNull final ImplementationMode implementationMode) {
+    @VisibleForTesting
+    static boolean shouldUseTextureView(@NonNull final ImplementationMode implementationMode) {
         boolean hasSurfaceViewQuirk = DeviceQuirks.get(SurfaceViewStretchedQuirk.class) != null
                 ||  DeviceQuirks.get(SurfaceViewNotCroppedByParentQuirk.class) != null;
-        if (Build.VERSION.SDK_INT <= 24 || isLegacyDevice || hasSurfaceViewQuirk) {
+        if (Build.VERSION.SDK_INT <= 24 || hasSurfaceViewQuirk) {
             // Force to use TextureView when the device is running android 7.0 and below, legacy
             // level or SurfaceView has quirks.
+            Logger.d(TAG, "Implementation mode to set is not supported, forcing to use "
+                    + "TextureView, because transform APIs are not supported on these devices.");
             return true;
         }
         switch (implementationMode) {
@@ -624,7 +608,8 @@
                     mViewfinderTransformation.updateTransformInfo(
                             createTransformInfo(surfaceRequest.getResolution(),
                                     display,
-                                    surfaceRequest.isFrontCamera(),
+                                    surfaceRequest.getLensFacing()
+                                            == CameraCharacteristics.LENS_FACING_FRONT,
                                     surfaceRequest.getSensorOrientation()));
                     redrawViewfinder();
                 }
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinderExt.kt b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinderExt.kt
new file mode 100644
index 0000000..1193fc1
--- /dev/null
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinderExt.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.viewfinder
+
+import android.view.Surface
+import androidx.annotation.RequiresApi
+import androidx.concurrent.futures.await
+
+/**
+ * Provides a suspending function of [CameraViewfinder.requestSurfaceAsync] to request
+ * a [Surface] by sending a [ViewfinderSurfaceRequest].
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+object CameraViewfinderExt {
+    suspend fun CameraViewfinder.requestSurface(
+        viewfinderSurfaceRequest: ViewfinderSurfaceRequest
+    ): Surface = requestSurfaceAsync(viewfinderSurfaceRequest).await()
+}
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequest.java b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequest.java
index e0eb047..b23887a8 100644
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequest.java
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequest.java
@@ -16,6 +16,10 @@
 
 package androidx.camera.viewfinder;
 
+import static android.hardware.camera2.CameraMetadata.LENS_FACING_BACK;
+import static android.hardware.camera2.CameraMetadata.LENS_FACING_EXTERNAL;
+import static android.hardware.camera2.CameraMetadata.LENS_FACING_FRONT;
+
 import android.annotation.SuppressLint;
 import android.graphics.SurfaceTexture;
 import android.hardware.camera2.CameraCharacteristics;
@@ -31,6 +35,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.viewfinder.CameraViewfinder.ImplementationMode;
 import androidx.camera.viewfinder.internal.surface.ViewfinderSurface;
 import androidx.camera.viewfinder.internal.utils.Logger;
 import androidx.camera.viewfinder.internal.utils.executor.CameraExecutors;
@@ -68,62 +73,39 @@
 
     private static final String TAG = "ViewfinderSurfaceRequest";
 
-    private final boolean mIsLegacyDevice;
-    private final boolean mIsFrontCamera;
-    private final int mSensorOrientation;
     @NonNull private final Size mResolution;
     @NonNull private final ViewfinderSurface mInternalViewfinderSurface;
     @NonNull private final CallbackToFutureAdapter.Completer<Void> mRequestCancellationCompleter;
     @NonNull private final ListenableFuture<Void> mSessionStatusFuture;
     @NonNull private final CallbackToFutureAdapter.Completer<Surface> mSurfaceCompleter;
-
+    @LensFacingValue private int mLensFacing;
+    @SensorOrientationDegreesValue private int mSensorOrientation;
+    @Nullable
+    private ImplementationMode mImplementationMode;
     @SuppressWarnings("WeakerAccess") /*synthetic accessor */
     @NonNull
     final ListenableFuture<Surface> mSurfaceFuture;
 
     /**
-     * Creates a new surface request with surface resolution and camera characteristics.
-     *
-     * <p>The resolution given here will be the default resolution of the Surface returned by
-     * {@link CameraViewfinder#requestSurfaceAsync(ViewfinderSurfaceRequest)}, which can then be
-     * passed to the camera API to set the camera viewfinder resolution.
-     *
-     * @param resolution The requested surface resolution.
-     * @param cameraCharacteristics The {@link CameraCharacteristics} to get device information
-     *                              e.g. hardware level, lens facing, sensor orientation, etc,.
-     */
-    public ViewfinderSurfaceRequest(
-            @NonNull Size resolution,
-            @NonNull CameraCharacteristics cameraCharacteristics) {
-        this(resolution,
-                cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
-                        == CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
-                cameraCharacteristics.get(CameraCharacteristics.LENS_FACING)
-                        == CameraCharacteristics.LENS_FACING_FRONT,
-                cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION));
-    }
-
-    /**
-     * Creates a new surface request with surface resolution, view display and camera device
-     * information.
+     * Creates a new surface request with surface resolution, camera device, lens facing and
+     * sensor orientation information.
      *
      * @param resolution The requested surface resolution. It is the output surface size
      *                   the camera is configured with, instead of {@link CameraViewfinder}
      *                   view size.
-     * {@link CameraViewfinder} view
-     * @param isLegacyDevice The device hardware level is legacy or not.
-     * @param isFrontCamera The camera is front facing or not.
+     * @param lensFacing The camera lens facing.
      * @param sensorOrientation THe camera sensor orientation.
+     * @param implementationMode The {@link ImplementationMode} to apply to the viewfinder.
      */
-    private ViewfinderSurfaceRequest(
+    ViewfinderSurfaceRequest(
             @NonNull Size resolution,
-            boolean isLegacyDevice,
-            boolean isFrontCamera,
-            int sensorOrientation) {
+            @LensFacingValue int lensFacing,
+            @SensorOrientationDegreesValue int sensorOrientation,
+            @Nullable ImplementationMode implementationMode) {
         mResolution = resolution;
-        mIsLegacyDevice = isLegacyDevice;
-        mIsFrontCamera = isFrontCamera;
+        mLensFacing = lensFacing;
         mSensorOrientation = sensorOrientation;
+        mImplementationMode = implementationMode;
 
         // To ensure concurrency and ordering, operations are chained. Completion can only be
         // triggered externally by the top-level completer (mSurfaceCompleter). The other future
@@ -164,7 +146,7 @@
             }
 
             @Override
-            public void onFailure(Throwable t) {
+            public void onFailure(@NonNull Throwable t) {
                 if (t instanceof RequestCancelledException) {
                     // Cancellation occurred. Notify listeners.
                     Preconditions.checkState(requestCancellationFuture.cancel(false));
@@ -259,6 +241,8 @@
     /**
      * Returns the resolution of the requested {@link Surface}.
      *
+     * <p>The value is set by {@link Builder#Builder(Size)}.
+     *
      * The surface which fulfills this request must have the resolution specified here in
      * order to fulfill the resource requirements of the camera.
      *
@@ -273,28 +257,40 @@
     /**
      * Returns the sensor orientation.
      *
+     * <p>The value is set by {@link Builder#setSensorOrientation(int)}, which can be retrieved from
+     * {@link CameraCharacteristics} by key {@link CameraCharacteristics#SENSOR_ORIENTATION}.
+     *
      * @return The sensor orientation.
      */
+    @SensorOrientationDegreesValue
     public int getSensorOrientation() {
         return mSensorOrientation;
     }
 
     /**
-     * Returns the status of camera lens facing.
+     * Returns the camera lens facing.
      *
-     * @return True if front camera, otherwise false.
+     * <p>The value is set by {@link Builder#setLensFacing(int)}, which can be retrieved from
+     * {@link CameraCharacteristics} by key {@link CameraCharacteristics#LENS_FACING}.
+     *
+     * @return The lens facing.
      */
-    public boolean isFrontCamera() {
-        return mIsFrontCamera;
+    @LensFacingValue
+    public int getLensFacing() {
+        return mLensFacing;
     }
 
     /**
-     * Returns the status of camera hardware level.
+     * Returns the {@link ImplementationMode}.
      *
-     * @return True if legacy device, otherwise false.
+     * <p>The value is set by {@link Builder#setImplementationMode(ImplementationMode)}.
+     *
+     * @return {@link ImplementationMode}. The value will be null if it's not set via
+     * {@link Builder#setImplementationMode(ImplementationMode)}.
      */
-    public boolean isLegacyDevice() {
-        return mIsLegacyDevice;
+    @Nullable
+    public ImplementationMode getImplementationMode() {
+        return mImplementationMode;
     }
 
     /**
@@ -362,7 +358,7 @@
                 }
 
                 @Override
-                public void onFailure(Throwable t) {
+                public void onFailure(@NonNull Throwable t) {
                     Preconditions.checkState(t instanceof RequestCancelledException, "Camera "
                             + "surface session should only fail with request "
                             + "cancellation. Instead failed due to:\n" + t);
@@ -414,6 +410,114 @@
                         + "will not complete."));
     }
 
+    /**
+     * Builder for {@link ViewfinderSurfaceRequest}.
+     */
+    public static final class Builder {
+
+        @NonNull private final Size mResolution;
+        @LensFacingValue private int mLensFacing = LENS_FACING_BACK;
+        @SensorOrientationDegreesValue private int mSensorOrientation = 0;
+        @Nullable private ImplementationMode mImplementationMode;
+
+        public Builder(@NonNull Size resolution) {
+            mResolution = resolution;
+        }
+
+        /**
+         * Sets the {@link ImplementationMode}.
+         *
+         * <p><b>Possible values:</b></p>
+         * <ul>
+         *   <li>{@link ImplementationMode#PERFORMANCE PERFORMANCE}</li>
+         *   <li>{@link ImplementationMode#COMPATIBLE COMPATIBLE}</li>
+         * </ul>
+         *
+         * <p>If not set, the {@link ImplementationMode} set via {@code app:implementationMode} in
+         * layout xml will be used for {@link CameraViewfinder}. If not set in the layout xml,
+         * the default value {@link ImplementationMode#PERFORMANCE} will be used in
+         * {@link CameraViewfinder}.
+         *
+         * @param implementationMode The {@link ImplementationMode}.
+         * @return This builder.
+         */
+        @NonNull
+        public Builder setImplementationMode(@NonNull ImplementationMode implementationMode) {
+            mImplementationMode = implementationMode;
+            return this;
+        }
+
+        /**
+         * Sets the lens facing.
+         *
+         * <p><b>Possible values:</b></p>
+         * <ul>
+         *   <li>{@link CameraMetadata#LENS_FACING_FRONT FRONT}</li>
+         *   <li>{@link CameraMetadata#LENS_FACING_BACK BACK}</li>
+         *   <li>{@link CameraMetadata#LENS_FACING_EXTERNAL EXTERNAL}</li>
+         * </ul>
+         *
+         * <p>The value can be retrieved from {@link CameraCharacteristics} by key
+         * {@link CameraCharacteristics#LENS_FACING}. If not set,
+         * {@link CameraMetadata#LENS_FACING_BACK} will be used by default.
+         *
+         * @param lensFacing The lens facing.
+         * @return This builder.
+         */
+        @NonNull
+        public Builder setLensFacing(@LensFacingValue int lensFacing) {
+            mLensFacing = lensFacing;
+            return this;
+        }
+
+        /**
+         * Sets the sensor orientation.
+         *
+         * <p><b>Range of valid values:</b><br>
+         * 0, 90, 180, 270</p>
+         *
+         * <p>The value can be retrieved from {@link CameraCharacteristics} by key
+         * {@link CameraCharacteristics#SENSOR_ORIENTATION}. If it is not
+         * set, 0 will be used by default.
+         *
+         * @param sensorOrientation
+         * @return this builder.
+         */
+        @NonNull
+        public Builder setSensorOrientation(@SensorOrientationDegreesValue int sensorOrientation) {
+            mSensorOrientation = sensorOrientation;
+            return this;
+        }
+
+        /**
+         * Builds the {@link ViewfinderSurfaceRequest}.
+         * @return the instance of {@link ViewfinderSurfaceRequest}.
+         */
+        @NonNull
+        public ViewfinderSurfaceRequest build() {
+            if (mLensFacing != LENS_FACING_FRONT
+                    && mLensFacing != LENS_FACING_BACK
+                    && mLensFacing != LENS_FACING_EXTERNAL) {
+                throw new IllegalArgumentException("Lens facing value: " + mLensFacing + " is "
+                        + "invalid");
+            }
+
+            if (mSensorOrientation != 0
+                    && mSensorOrientation != 90
+                    && mSensorOrientation != 180
+                    && mSensorOrientation != 270) {
+                throw new IllegalArgumentException("Sensor orientation value: "
+                        + mSensorOrientation + " is invalid");
+            }
+
+            return new ViewfinderSurfaceRequest(
+                    mResolution,
+                    mLensFacing,
+                    mSensorOrientation,
+                    mImplementationMode);
+        }
+    }
+
     static final class RequestCancelledException extends RuntimeException {
         RequestCancelledException(@NonNull String message, @NonNull Throwable cause) {
             super(message, cause);
@@ -541,4 +645,20 @@
         Result() {
         }
     }
+
+    /**
+     * Valid integer sensor orientation degrees values.
+     */
+    @IntDef({0, 90, 180, 270})
+    @Retention(RetentionPolicy.SOURCE)
+    @interface SensorOrientationDegreesValue {
+    }
+
+    /**
+     * Valid integer sensor orientation degrees values.
+     */
+    @IntDef({LENS_FACING_FRONT, LENS_FACING_BACK, LENS_FACING_EXTERNAL})
+    @Retention(RetentionPolicy.SOURCE)
+    @interface LensFacingValue {
+    }
 }
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequestExt.kt b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequestExt.kt
new file mode 100644
index 0000000..4b8112e
--- /dev/null
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequestExt.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:JvmName("ViewfinderSurfaceRequestUtil")
+
+package androidx.camera.viewfinder
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraMetadata
+import androidx.annotation.RequiresApi
+import androidx.camera.viewfinder.CameraViewfinder.ImplementationMode
+
+/**
+ * Populates [ViewfinderSurfaceRequest.Builder] from [CameraCharacteristics].
+ *
+ * <p>The [CameraCharacteristics] will be used to populate information including lens facing,
+ * sensor orientation and [ImplementationMode]. If the hardware level is legacy,
+ * the [ImplementationMode] will be set to [ImplementationMode.COMPATIBLE].
+ */
+@SuppressLint("ClassVerificationFailure")
+@RequiresApi(21)
+fun ViewfinderSurfaceRequest.Builder.populateFromCharacteristics(
+    cameraCharacteristics: CameraCharacteristics
+): ViewfinderSurfaceRequest.Builder {
+    setLensFacing(cameraCharacteristics.get(CameraCharacteristics.LENS_FACING)!!)
+    setSensorOrientation(
+        cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!)
+    if (cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
+        == CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
+        setImplementationMode(ImplementationMode.COMPATIBLE)
+    }
+    return this
+}
\ No newline at end of file
diff --git a/camera/camera-viewfinder/src/test/java/androidx/camera/viewfinder/CameraViewfinderTest.java b/camera/camera-viewfinder/src/test/java/androidx/camera/viewfinder/CameraViewfinderTest.java
index 897fcde0..2528c8d 100644
--- a/camera/camera-viewfinder/src/test/java/androidx/camera/viewfinder/CameraViewfinderTest.java
+++ b/camera/camera-viewfinder/src/test/java/androidx/camera/viewfinder/CameraViewfinderTest.java
@@ -46,7 +46,6 @@
     public void surfaceViewNormal_useSurfaceView() {
         // Assert: SurfaceView is used.
         assertThat(CameraViewfinder.shouldUseTextureView(
-                /* isLegacyDevice = */ false,
                 CameraViewfinder.ImplementationMode.PERFORMANCE)).isFalse();
     }
 
@@ -57,7 +56,6 @@
 
         // Assert: TextureView is used even the SurfaceRequest is compatible with SurfaceView.
         assertThat(CameraViewfinder.shouldUseTextureView(
-                /* isLegacyDevice = */ false,
                 CameraViewfinder.ImplementationMode.PERFORMANCE)).isTrue();
     }
 
@@ -68,7 +66,6 @@
 
         // Assert: TextureView is used even the SurfaceRequest is compatible with SurfaceView.
         assertThat(CameraViewfinder.shouldUseTextureView(
-                /* isLegacyDevice = */ false,
                 CameraViewfinder.ImplementationMode.PERFORMANCE)).isTrue();
     }
 
@@ -79,7 +76,6 @@
 
         // Assert: TextureView is used even the SurfaceRequest is compatible with SurfaceView.
         assertThat(CameraViewfinder.shouldUseTextureView(
-                /* isLegacyDevice = */ true,
                 CameraViewfinder.ImplementationMode.COMPATIBLE)).isTrue();
     }
 }
diff --git a/camera/integration-tests/avsynctestapp/build.gradle b/camera/integration-tests/avsynctestapp/build.gradle
index 8612bb7..0f4683f 100644
--- a/camera/integration-tests/avsynctestapp/build.gradle
+++ b/camera/integration-tests/avsynctestapp/build.gradle
@@ -53,6 +53,7 @@
     implementation("androidx.compose.ui:ui:$compose_version")
     implementation("androidx.compose.ui:ui-tooling-preview:$compose_version")
     implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1")
+    implementation(project(":lifecycle:lifecycle-viewmodel"))
 
     compileOnly(libs.kotlinCompiler)
 
@@ -61,6 +62,7 @@
     testImplementation(libs.junit)
     testImplementation(libs.truth)
     androidTestImplementation(project(":camera:camera-testing"))
+    androidTestImplementation(project(":lifecycle:lifecycle-viewmodel"))
     androidTestImplementation(libs.kotlinCoroutinesTest)
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testRules)
diff --git a/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt
index 36b13db..88c8b26 100644
--- a/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt
+++ b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt
@@ -66,12 +66,12 @@
     private lateinit var viewModel: SignalGeneratorViewModel
     private lateinit var lifecycleOwner: FakeLifecycleOwner
     private val fakeViewModelStoreOwner = object : ViewModelStoreOwner {
-        private val viewModelStore = ViewModelStore()
+        private val vmStore = ViewModelStore()
 
-        override fun getViewModelStore() = viewModelStore
+        override val viewModelStore = vmStore
 
         fun clear() {
-            viewModelStore.clear()
+            vmStore.clear()
         }
     }
 
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt
index 2acc74e..a4daab7 100644
--- a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt
@@ -21,7 +21,9 @@
 import androidx.camera.integration.avsync.ui.widget.AdvancedFloatingActionButton
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.size
@@ -89,21 +91,23 @@
 ) {
     Box(modifier = Modifier.fillMaxSize()) {
         LightingScreen(isOn = isSignalActive)
-        SignalControl(
-            enabled = isGeneratorReady,
-            isStarted = isSignalStarted,
-            onStartClick = onSignalStartClick,
-            onStopClick = onSignalStopClick,
-        )
-        RecordingControl(
-            enabled = isRecorderReady,
-            isStarted = isRecording,
-            isPaused = isPaused,
-            onStartClick = onRecordingStartClick,
-            onStopClick = onRecordingStopClick,
-            onPauseClick = onRecordingPauseClick,
-            onResumeClick = onRecordingResumeClick,
-        )
+        ControlPanel {
+            SignalControl(
+                enabled = isGeneratorReady,
+                isStarted = isSignalStarted,
+                onStartClick = onSignalStartClick,
+                onStopClick = onSignalStopClick,
+            )
+            RecordingControl(
+                enabled = isRecorderReady,
+                isStarted = isRecording,
+                isPaused = isPaused,
+                onStartClick = onRecordingStartClick,
+                onStopClick = onRecordingStopClick,
+                onPauseClick = onRecordingPauseClick,
+                onResumeClick = onRecordingResumeClick,
+            )
+        }
     }
 }
 
@@ -116,6 +120,19 @@
 }
 
 @Composable
+private fun ControlPanel(
+    modifier: Modifier = Modifier,
+    content: @Composable () -> Unit
+) {
+    Column(modifier = modifier.fillMaxSize()) {
+        Spacer(modifier = Modifier.weight(2f))
+        Box(modifier = Modifier.weight(1f)) {
+            content()
+        }
+    }
+}
+
+@Composable
 private fun SignalControl(
     modifier: Modifier = Modifier,
     enabled: Boolean,
diff --git a/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/CameraPipeActivity.kt b/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/CameraPipeActivity.kt
index 16202a5..920268e 100644
--- a/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/CameraPipeActivity.kt
+++ b/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/CameraPipeActivity.kt
@@ -131,13 +131,19 @@
     }
 
     private suspend fun findNextCamera(lastCameraId: CameraId?): CameraId {
-        val cameras: List<CameraId> = cameraPipe.cameras().ids()
+        val cameras = cameraPipe.cameras().getCameraIds()
+        checkNotNull(cameras) { "Unable to load CameraIds from CameraPipe" }
+
         // By default, open the first back facing camera if no camera was previously configured.
         if (lastCameraId == null) {
-            return cameras.firstOrNull {
-                cameraPipe.cameras().getMetadata(it)[CameraCharacteristics.LENS_FACING] ==
-                    CameraCharacteristics.LENS_FACING_BACK
-            } ?: cameras.first()
+            for (id in cameras) {
+                val metadata = cameraPipe.cameras().getCameraMetadata(id)
+                if (metadata != null && metadata[CameraCharacteristics.LENS_FACING] ==
+                    CameraCharacteristics.LENS_FACING_BACK) {
+                    return id
+                }
+            }
+            return cameras.first()
         }
 
         // If a camera was previously opened and the operating mode is NORMAL, return the same
diff --git a/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/SimpleCamera.kt b/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/SimpleCamera.kt
index 3fd452d..e9a7863 100644
--- a/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/SimpleCamera.kt
+++ b/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/SimpleCamera.kt
@@ -83,7 +83,8 @@
 
             Log.i("CXCP-App", "Selected $cameraId to open.")
 
-            val cameraMetadata = cameraPipe.cameras().awaitMetadata(cameraId)
+            val cameraMetadata = cameraPipe.cameras().awaitCameraMetadata(cameraId)
+            checkNotNull(cameraMetadata) { "Failed to load CameraMetadata for $cameraId" }
 
             var yuvSizes =
                 cameraMetadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]!!
@@ -213,7 +214,9 @@
 
             Log.i("CXCP-App", "Selected $cameraId to open.")
 
-            val cameraMetadata = cameraPipe.cameras().awaitMetadata(cameraId)
+            val cameraMetadata = cameraPipe.cameras().awaitCameraMetadata(cameraId)
+            checkNotNull(cameraMetadata) { "Failed to load CameraMetadata for $cameraId" }
+
             var yuvSizes =
                 cameraMetadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]!!
                     .getOutputSizes(ImageFormat.YUV_420_888).toList()
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/CameraXInitTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXInitTest.kt
index 0d4c7e8..d8b0f40 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXInitTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXInitTest.kt
@@ -23,6 +23,7 @@
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.CameraXConfig
 import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.LabTestRule
 import androidx.concurrent.futures.await
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
@@ -58,6 +59,9 @@
     val permissionRule: GrantPermissionRule =
         GrantPermissionRule.grant(android.Manifest.permission.CAMERA)
 
+    @get:Rule
+    val labTest: LabTestRule = LabTestRule()
+
     private val context = ApplicationProvider.getApplicationContext<Context>()
     private val packageManager = context.packageManager
     private lateinit var cameraProvider: ProcessCameraProvider
@@ -88,6 +92,9 @@
         }
     }
 
+    // Only test on lab devices because emulator may not have correctly set the matching camera
+    // features and camera list.
+    @LabTestRule.LabTestOnly
     @Test
     fun initOnDevice_hasCamera() {
         ProcessCameraProvider.configureInstance(cameraXConfig)
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/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/compose/ComposeCameraAppTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/compose/ComposeCameraAppTest.kt
index 319a54b..4e031ec 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/compose/ComposeCameraAppTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/compose/ComposeCameraAppTest.kt
@@ -18,6 +18,8 @@
 
 import android.os.Build
 import androidx.camera.integration.uiwidgets.compose.ui.navigation.ComposeCameraScreen
+import androidx.camera.integration.uiwidgets.compose.ui.screen.imagecapture.DEFAULT_LENS_FACING
+import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.LabTestRule
 import androidx.camera.view.PreviewView
 import androidx.compose.ui.semantics.Role
@@ -64,6 +66,7 @@
             "Cuttlefish has MediaCodec dequeInput/Output buffer fails issue. Unable to test.",
             Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29
         )
+        Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(DEFAULT_LENS_FACING))
 
         // Recreate the activity as it might terminate in other tests
         androidComposeTestRule.activityRule.scenario.recreate()
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenState.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenState.kt
index cd0aa98..051fee8 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenState.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenState.kt
@@ -23,6 +23,7 @@
 import android.provider.MediaStore
 import android.util.Log
 import android.widget.Toast
+import androidx.annotation.VisibleForTesting
 import androidx.camera.core.Camera
 import androidx.camera.core.CameraControl.OperationCanceledException
 import androidx.camera.core.CameraSelector
@@ -63,7 +64,8 @@
 import kotlinx.coroutines.asExecutor
 import kotlinx.coroutines.launch
 
-private const val DEFAULT_LENS_FACING = CameraSelector.LENS_FACING_FRONT
+@VisibleForTesting
+internal const val DEFAULT_LENS_FACING = CameraSelector.LENS_FACING_FRONT
 private const val DEFAULT_FLASH_MODE = ImageCapture.FLASH_MODE_OFF
 
 // State Holder for ImageCaptureScreen
diff --git a/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt b/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt
index 22a0b7c..cc628ac 100644
--- a/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt
+++ b/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt
@@ -45,6 +45,7 @@
 import android.os.HandlerThread
 import android.provider.MediaStore
 import android.util.Log
+import android.util.Size
 import android.view.LayoutInflater
 import android.view.Menu
 import android.view.MenuInflater
@@ -52,13 +53,16 @@
 import android.view.Surface
 import android.view.View
 import android.view.ViewGroup
+import android.view.ViewTreeObserver
 import android.widget.Toast
 import androidx.appcompat.app.AlertDialog
 import androidx.camera.core.impl.utils.CompareSizesByArea
-import androidx.camera.core.impl.utils.futures.FutureCallback
-import androidx.camera.core.impl.utils.futures.Futures
 import androidx.camera.viewfinder.CameraViewfinder
+import androidx.camera.viewfinder.CameraViewfinder.ImplementationMode
+import androidx.camera.viewfinder.CameraViewfinder.ScaleType
+import androidx.camera.viewfinder.CameraViewfinderExt.requestSurface
 import androidx.camera.viewfinder.ViewfinderSurfaceRequest
+import androidx.camera.viewfinder.populateFromCharacteristics
 import androidx.core.app.ActivityCompat
 import androidx.core.content.ContextCompat
 import androidx.fragment.app.DialogFragment
@@ -70,18 +74,15 @@
 import androidx.window.layout.WindowInfoTracker
 import androidx.window.layout.WindowLayoutInfo
 import com.google.common.base.Objects
-import com.google.common.util.concurrent.ListenableFuture
 import java.io.Closeable
 import java.io.File
 import java.io.FileOutputStream
 import java.io.IOException
 import java.text.SimpleDateFormat
-import java.util.Arrays
 import java.util.Collections
 import java.util.Date
 import java.util.Locale
 import java.util.concurrent.ArrayBlockingQueue
-import java.util.concurrent.Semaphore
 import java.util.concurrent.TimeoutException
 import kotlin.coroutines.resume
 import kotlin.coroutines.resumeWithException
@@ -89,6 +90,7 @@
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.withContext
 
 /**
@@ -97,53 +99,55 @@
 class CameraViewfinderFoldableFragment : Fragment(), View.OnClickListener,
     ActivityCompat.OnRequestPermissionsResultCallback {
 
-    private lateinit var cameraThread: HandlerThread
-
-    private lateinit var cameraHandler: Handler
-
-    private lateinit var imageReaderThread: HandlerThread
-
-    private lateinit var imageReaderHandler: Handler
-
-    private val cameraOpenCloseLock = Semaphore(1)
+    private val cameraOpenCloseLock = Mutex()
 
     private val onImageAvailableListener = ImageReader.OnImageAvailableListener {
-        cameraHandler.post(
+        cameraHandler?.post(
             ImageSaver(
                 it.acquireNextImage(),
-                file
+                checkNotNull(file) { "file cannot be null when saving image" }
             )
         )
     }
 
-    private lateinit var camera: CameraDevice
-
-    private lateinit var characteristics: CameraCharacteristics
-
-    private lateinit var cameraId: String
-
     private lateinit var cameraManager: CameraManager
 
     private lateinit var cameraViewfinder: CameraViewfinder
 
-    private lateinit var file: File
-
-    private lateinit var imageReader: ImageReader
-
-    private lateinit var relativeOrientation: OrientationLiveData
-
-    private lateinit var session: CameraCaptureSession
-
-    private lateinit var surfaceListenableFuture: ListenableFuture<Surface>
-
     private lateinit var windowInfoTracker: WindowInfoTracker
 
+    private var cameraThread: HandlerThread? = null
+
+    private var cameraHandler: Handler? = null
+
+    private var imageReaderThread: HandlerThread? = null
+
+    private var imageReaderHandler: Handler? = null
+
+    private var camera: CameraDevice? = null
+
+    private var characteristics: CameraCharacteristics? = null
+
+    private var cameraId: String? = null
+
+    private var file: File? = null
+
+    private var imageReader: ImageReader? = null
+
+    private var relativeOrientation: OrientationLiveData? = null
+
+    private var session: CameraCaptureSession? = null
+
     private var activeWindowLayoutInfo: WindowLayoutInfo? = null
 
     private var isViewfinderInLeftTop = true
 
     private var viewfinderSurfaceRequest: ViewfinderSurfaceRequest? = null
 
+    private var resolution: Size? = null
+
+    private var layoutChangedListener: ViewTreeObserver.OnGlobalLayoutListener? = null
+
     @Deprecated("Deprecated in Java")
     @Suppress("DEPRECATION")
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -171,19 +175,22 @@
     override fun onOptionsItemSelected(item: MenuItem): Boolean {
         when (item.itemId) {
             R.id.implementationMode -> {
-                cameraViewfinder.implementationMode =
+                val implementationMode =
                     when (cameraViewfinder.implementationMode) {
-                        CameraViewfinder.ImplementationMode.PERFORMANCE ->
-                            CameraViewfinder.ImplementationMode.COMPATIBLE
-                        else -> CameraViewfinder.ImplementationMode.PERFORMANCE
+                        ImplementationMode.PERFORMANCE ->
+                            ImplementationMode.COMPATIBLE
+                        else -> ImplementationMode.PERFORMANCE
                     }
-                closeCamera()
-                sendSurfaceRequest(false)
+
+                lifecycleScope.launch {
+                    closeCamera()
+                    sendSurfaceRequest(implementationMode, false)
+                }
             }
-            R.id.fitCenter -> cameraViewfinder.scaleType = CameraViewfinder.ScaleType.FIT_CENTER
-            R.id.fillCenter -> cameraViewfinder.scaleType = CameraViewfinder.ScaleType.FILL_CENTER
-            R.id.fitStart -> cameraViewfinder.scaleType = CameraViewfinder.ScaleType.FIT_START
-            R.id.fitEnd -> cameraViewfinder.scaleType = CameraViewfinder.ScaleType.FIT_END
+            R.id.fitCenter -> cameraViewfinder.scaleType = ScaleType.FIT_CENTER
+            R.id.fillCenter -> cameraViewfinder.scaleType = ScaleType.FILL_CENTER
+            R.id.fitStart -> cameraViewfinder.scaleType = ScaleType.FIT_START
+            R.id.fitEnd -> cameraViewfinder.scaleType = ScaleType.FIT_END
         }
         return super.onOptionsItemSelected(item)
     }
@@ -209,9 +216,13 @@
     override fun onResume() {
         super.onResume()
         cameraThread = HandlerThread("CameraThread").apply { start() }
-        cameraHandler = Handler(cameraThread.looper)
+        cameraHandler = Handler(checkNotNull(cameraThread) {
+            "camera thread cannot be null"
+        }.looper)
         imageReaderThread = HandlerThread("ImageThread").apply { start() }
-        imageReaderHandler = Handler(imageReaderThread.looper)
+        imageReaderHandler = Handler(checkNotNull(imageReaderThread) {
+            "image reader thread cannot be null"
+        }.looper)
 
         // Request Permission
         val cameraPermission = activity?.let {
@@ -234,7 +245,13 @@
             }
         }
 
-        sendSurfaceRequest(false)
+        layoutChangedListener = ViewTreeObserver.OnGlobalLayoutListener {
+            cameraViewfinder.viewTreeObserver.removeOnGlobalLayoutListener(layoutChangedListener)
+            layoutChangedListener = null
+
+            sendSurfaceRequest(null, false)
+        }
+        cameraViewfinder.viewTreeObserver.addOnGlobalLayoutListener(layoutChangedListener)
 
         lifecycleScope.launch {
             windowInfoTracker.windowLayoutInfo(requireActivity())
@@ -247,10 +264,12 @@
     }
 
     override fun onPause() {
-        closeCamera()
-        cameraThread.quitSafely()
-        imageReaderThread.quitSafely()
-        viewfinderSurfaceRequest?.markSurfaceSafeToRelease()
+        lifecycleScope.launch {
+            closeCamera()
+            cameraThread?.quitSafely()
+            imageReaderThread?.quitSafely()
+            viewfinderSurfaceRequest?.markSurfaceSafeToRelease()
+        }
         super.onPause()
     }
 
@@ -302,28 +321,20 @@
     }
 
     // ------------- Create Capture Session --------------
-    private fun sendSurfaceRequest(toggleCamera: Boolean) {
-        cameraViewfinder.post {
-            if (isAdded && context != null) {
-                setUpCameraOutputs(toggleCamera)
-
-                val context = requireContext()
-                surfaceListenableFuture =
-                    cameraViewfinder.requestSurfaceAsync(viewfinderSurfaceRequest!!)
-
-                Futures.addCallback(surfaceListenableFuture, object : FutureCallback<Surface?> {
-                    override fun onSuccess(surface: Surface?) {
-                        Log.d(TAG, "request onSurfaceAvailable surface = $surface")
-                        if (surface != null) {
-                            initializeCamera(surface)
-                        }
-                    }
-
-                    override fun onFailure(t: Throwable) {
-                        Log.e(TAG, "request onSurfaceClosed")
-                    }
-                }, ContextCompat.getMainExecutor(context))
+    private fun sendSurfaceRequest(
+        implementationMode: ImplementationMode?,
+        toggleCamera: Boolean
+    ) = lifecycleScope.launch {
+        if (isAdded && context != null) {
+            setUpCameraOutputs(toggleCamera)
+            val builder = ViewfinderSurfaceRequest.Builder(resolution!!)
+                .populateFromCharacteristics(characteristics!!)
+            if (implementationMode != null) {
+                builder.setImplementationMode(implementationMode)
             }
+            viewfinderSurfaceRequest = builder.build()
+            val surface = cameraViewfinder.requestSurface(viewfinderSurfaceRequest!!)
+            initializeCamera(surface)
         }
     }
 
@@ -331,34 +342,44 @@
         try {
             for (cameraId in cameraManager.cameraIdList) {
                 characteristics = cameraManager.getCameraCharacteristics(cameraId)
-                relativeOrientation = OrientationLiveData(requireContext(), characteristics).apply {
+                relativeOrientation = OrientationLiveData(requireContext(),
+                    checkNotNull(characteristics) {
+                        "camera characteristics cannot be null"
+                    }).apply {
                     observe(viewLifecycleOwner, Observer { orientation ->
                         Log.d(TAG, "Orientation changed: $orientation")
                     })
                 }
 
-                val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
+                val facing = checkNotNull(characteristics) {
+                    "camera characteristics cannot be null"
+                }.get(CameraCharacteristics.LENS_FACING)
 
                 // Toggle the front and back camera
                 if (toggleCamera) {
-                    val currentFacing: Int? = cameraManager.getCameraCharacteristics(this.cameraId)
+                    val currentFacing: Int? = cameraManager.getCameraCharacteristics(
+                        checkNotNull(this.cameraId) {
+                            "camera id cannot be null"
+                        })
                         .get<Int>(CameraCharacteristics.LENS_FACING)
                     if (Objects.equal(currentFacing, facing)) {
                         continue
                     }
                 }
 
-                val map = characteristics.get(
+                val map = checkNotNull(characteristics) {
+                    "camera characteristics cannot be null"
+                }.get(
                     CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
                 ) ?: continue
 
                 // For still image captures, we use the largest available size.
-                val largest = Collections.max(
-                    /* coll = */ Arrays.asList(*map.getOutputSizes(ImageFormat.JPEG)),
+                resolution = Collections.max(
+                    /* coll = */ listOf(*map.getOutputSizes(ImageFormat.JPEG)),
                     /* comp = */ CompareSizesByArea()
                 )
                 imageReader = ImageReader.newInstance(
-                    largest.width, largest.height,
+                    resolution!!.width, resolution!!.height,
                     ImageFormat.JPEG, /*maxImages*/ 2
                 ).apply {
                     setOnImageAvailableListener(onImageAvailableListener, imageReaderHandler)
@@ -366,9 +387,6 @@
 
                 this.cameraId = cameraId
                 this.characteristics = cameraManager.getCameraCharacteristics(cameraId)
-                viewfinderSurfaceRequest = ViewfinderSurfaceRequest(largest, characteristics)
-
-                Log.d(TAG, "viewfinderSurfaceRequest created = $viewfinderSurfaceRequest")
                 return
             }
         } catch (e: CameraAccessException) {
@@ -376,31 +394,42 @@
         }
     }
 
-    private fun initializeCamera(surface: Surface) = lifecycleScope.launch(Dispatchers.IO) {
-        cameraOpenCloseLock.acquire()
+    private suspend fun initializeCamera(surface: Surface) {
+        cameraOpenCloseLock.lock()
 
-        // Open the selected camera
-        camera = openCamera(cameraManager, cameraId, cameraHandler)
+        withContext(Dispatchers.IO) {
+            // Open the selected camera
+            camera = openCamera(cameraManager, checkNotNull(cameraId) {
+                "camera id cannot be null"
+            }, cameraHandler)
 
-        // Creates list of Surfaces where the camera will output frames
-        val targets = listOf(surface, imageReader.surface)
+            // Creates list of Surfaces where the camera will output frames
+            val targets = listOf(surface, checkNotNull(imageReader?.surface) {
+                "image reader surface cannot be null"
+            })
 
-        try {
-            // Start a capture session using our open camera and list of Surfaces where frames will go
-            session = createCaptureSession(camera, targets, cameraHandler)
+            try {
+                // Start a capture session using our open camera and list of Surfaces where frames will go
+                session = createCaptureSession(checkNotNull(camera) {
+                    "camera cannot be null"
+                }, targets, cameraHandler)
 
-            val captureRequest = camera.createCaptureRequest(
-                CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(surface) }
+                val captureRequest = checkNotNull(camera) {
+                    "camera cannot be null"
+                }.createCaptureRequest(
+                    CameraDevice.TEMPLATE_PREVIEW
+                ).apply { addTarget(surface) }
 
-            // This will keep sending the capture request as frequently as possible until the
-            // session is torn down or session.stopRepeating() is called
-            session.setRepeatingRequest(captureRequest.build(), null, cameraHandler)
-        } catch (e: CameraAccessException) {
-            Log.e(TAG, "createCaptureSession CameraAccessException")
-        } catch (e: IllegalArgumentException) {
-            Log.e(TAG, "createCaptureSession IllegalArgumentException")
-        } catch (e: SecurityException) {
-            Log.e(TAG, "createCaptureSession SecurityException")
+                // This will keep sending the capture request as frequently as possible until the
+                // session is torn down or session.stopRepeating() is called
+                session?.setRepeatingRequest(captureRequest.build(), null, cameraHandler)
+            } catch (e: CameraAccessException) {
+                Log.e(TAG, "createCaptureSession CameraAccessException")
+            } catch (e: IllegalArgumentException) {
+                Log.e(TAG, "createCaptureSession IllegalArgumentException")
+            } catch (e: SecurityException) {
+                Log.e(TAG, "createCaptureSession SecurityException")
+            }
         }
     }
 
@@ -409,58 +438,55 @@
         manager: CameraManager,
         cameraId: String,
         handler: Handler? = null
-    ): CameraDevice = suspendCancellableCoroutine { cont ->
-        try {
-            manager.openCamera(cameraId, object : CameraDevice.StateCallback() {
-                override fun onOpened(device: CameraDevice) {
-                    cameraOpenCloseLock.release()
-                    cont.resume(device)
-                }
-
-                override fun onDisconnected(device: CameraDevice) {
-                    Log.w(TAG, "Camera $cameraId has been disconnected")
-                    cameraOpenCloseLock.release()
-                }
-
-                override fun onError(device: CameraDevice, error: Int) {
-                    val msg = when (error) {
-                        ERROR_CAMERA_DEVICE -> "Fatal (device)"
-                        ERROR_CAMERA_DISABLED -> "Device policy"
-                        ERROR_CAMERA_IN_USE -> "Camera in use"
-                        ERROR_CAMERA_SERVICE -> "Fatal (service)"
-                        ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
-                        else -> "Unknown"
+    ): CameraDevice = withContext(Dispatchers.IO) {
+        suspendCancellableCoroutine { cont ->
+            try {
+                manager.openCamera(cameraId, object : CameraDevice.StateCallback() {
+                    override fun onOpened(device: CameraDevice) {
+                        cameraOpenCloseLock.unlock()
+                        cont.resume(device)
                     }
-                    Log.e(TAG, "Camera $cameraId error: ($error) $msg")
-                }
-            }, handler)
-        } catch (e: CameraAccessException) {
-            Log.e(TAG, "openCamera CameraAccessException")
-        } catch (e: IllegalArgumentException) {
-            Log.e(TAG, "openCamera IllegalArgumentException")
-        } catch (e: SecurityException) {
-            Log.e(TAG, "openCamera SecurityException")
+
+                    override fun onDisconnected(device: CameraDevice) {
+                        Log.w(TAG, "Camera $cameraId has been disconnected")
+                        cameraOpenCloseLock.unlock()
+                    }
+
+                    override fun onError(device: CameraDevice, error: Int) {
+                        val msg = when (error) {
+                            ERROR_CAMERA_DEVICE -> "Fatal (device)"
+                            ERROR_CAMERA_DISABLED -> "Device policy"
+                            ERROR_CAMERA_IN_USE -> "Camera in use"
+                            ERROR_CAMERA_SERVICE -> "Fatal (service)"
+                            ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
+                            else -> "Unknown"
+                        }
+                        Log.e(TAG, "Camera $cameraId error: ($error) $msg")
+                    }
+                }, handler)
+            } catch (e: CameraAccessException) {
+                Log.e(TAG, "openCamera CameraAccessException")
+            } catch (e: IllegalArgumentException) {
+                Log.e(TAG, "openCamera IllegalArgumentException")
+            } catch (e: SecurityException) {
+                Log.e(TAG, "openCamera SecurityException")
+            }
         }
     }
 
-    private fun closeCamera() {
+    private suspend fun closeCamera() = withContext(Dispatchers.IO) {
         try {
-            cameraOpenCloseLock.acquire()
-            if (::session.isInitialized) {
-                session.close()
-            }
-
-            if (::camera.isInitialized) {
-                camera.close()
-            }
-
-            if (::imageReader.isInitialized) {
-                imageReader.close()
-            }
+            cameraOpenCloseLock.lock()
+            session?.close()
+            camera?.close()
+            imageReader?.close()
+            session = null
+            camera = null
+            imageReader = null
         } catch (exc: Throwable) {
             Log.e(TAG, "Error closing camera", exc)
         } finally {
-            cameraOpenCloseLock.release()
+            cameraOpenCloseLock.unlock()
         }
     }
 
@@ -469,26 +495,30 @@
         device: CameraDevice,
         targets: List<Surface>,
         handler: Handler? = null
-    ): CameraCaptureSession = suspendCoroutine { cont ->
+    ): CameraCaptureSession = withContext(Dispatchers.IO) {
+        suspendCoroutine { cont ->
 
-        // Create a capture session using the predefined targets; this also involves defining the
-        // session state callback to be notified of when the session is ready
-        device.createCaptureSession(targets, object : CameraCaptureSession.StateCallback() {
+            // Create a capture session using the predefined targets; this also involves defining the
+            // session state callback to be notified of when the session is ready
+            device.createCaptureSession(targets, object : CameraCaptureSession.StateCallback() {
 
-            override fun onConfigured(session: CameraCaptureSession) = cont.resume(session)
+                override fun onConfigured(session: CameraCaptureSession) = cont.resume(session)
 
-            override fun onConfigureFailed(session: CameraCaptureSession) {
-                val exc = RuntimeException("Camera ${device.id} session configuration failed")
-                Log.e(TAG, exc.message, exc)
-                cont.resumeWithException(exc)
-            }
-        }, handler)
+                override fun onConfigureFailed(session: CameraCaptureSession) {
+                    val exc = RuntimeException("Camera ${device.id} session configuration failed")
+                    Log.e(TAG, exc.message, exc)
+                    cont.resumeWithException(exc)
+                }
+            }, handler)
+        }
     }
 
     // ------------- Toggle Camera -----------
     private fun toggleCamera() {
-        closeCamera()
-        sendSurfaceRequest(true)
+        lifecycleScope.launch {
+            closeCamera()
+            sendSurfaceRequest(null, true)
+        }
     }
 
     // ------------- Save Bitmap ------------
@@ -509,8 +539,10 @@
             val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
             val uri = resolver.insert(contentUri, values)
             try {
-                val fos = resolver.openOutputStream(uri!!)
-                bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos!!)
+                val fos = resolver.openOutputStream(checkNotNull(uri) { "uri cannot be null" })
+                bitmap.compress(Bitmap.CompressFormat.PNG, 100, checkNotNull(fos) {
+                    "fos cannot be null"
+                })
                 fos.close()
                 showToast("Saved: $displayName")
             } catch (e: IOException) {
@@ -745,83 +777,89 @@
 
         // Flush any images left in the image reader
         @Suppress("ControlFlowWithEmptyBody")
-        while (imageReader.acquireNextImage() != null) {
+        while (imageReader?.acquireNextImage() != null) {
         }
 
         // Start a new image queue
         val imageQueue = ArrayBlockingQueue<Image>(IMAGE_BUFFER_SIZE)
-        imageReader.setOnImageAvailableListener({ reader ->
+        imageReader?.setOnImageAvailableListener({ reader ->
             val image = reader.acquireNextImage()
             Log.d(TAG, "Image available in queue: ${image.timestamp}")
             imageQueue.add(image)
         }, imageReaderHandler)
 
-        val captureRequest = session.device.createCaptureRequest(
-            CameraDevice.TEMPLATE_STILL_CAPTURE).apply { addTarget(imageReader.surface) }
-        session.capture(captureRequest.build(), object : CameraCaptureSession.CaptureCallback() {
+        val captureRequest = session?.device?.createCaptureRequest(
+            CameraDevice.TEMPLATE_STILL_CAPTURE).apply {
+            imageReader?.surface?.let { this?.addTarget(it) } }
+        if (captureRequest != null) {
+            session?.capture(captureRequest.build(),
+                object : CameraCaptureSession.CaptureCallback() {
 
-            override fun onCaptureStarted(
-                session: CameraCaptureSession,
-                request: CaptureRequest,
-                timestamp: Long,
-                frameNumber: Long
-            ) {
-                super.onCaptureStarted(session, request, timestamp, frameNumber)
-            }
+                override fun onCaptureStarted(
+                    session: CameraCaptureSession,
+                    request: CaptureRequest,
+                    timestamp: Long,
+                    frameNumber: Long
+                ) {
+                    super.onCaptureStarted(session, request, timestamp, frameNumber)
+                }
 
-            override fun onCaptureCompleted(
-                session: CameraCaptureSession,
-                request: CaptureRequest,
-                result: TotalCaptureResult
-            ) {
-                super.onCaptureCompleted(session, request, result)
-                val resultTimestamp = result.get(CaptureResult.SENSOR_TIMESTAMP)
-                Log.d(TAG, "Capture result received: $resultTimestamp")
+                override fun onCaptureCompleted(
+                    session: CameraCaptureSession,
+                    request: CaptureRequest,
+                    result: TotalCaptureResult
+                ) {
+                    super.onCaptureCompleted(session, request, result)
+                    val resultTimestamp = result.get(CaptureResult.SENSOR_TIMESTAMP)
+                    Log.d(TAG, "Capture result received: $resultTimestamp")
 
-                // Set a timeout in case image captured is dropped from the pipeline
-                val exc = TimeoutException("Image dequeuing took too long")
-                val timeoutRunnable = Runnable { cont.resumeWithException(exc) }
-                imageReaderHandler.postDelayed(timeoutRunnable, IMAGE_CAPTURE_TIMEOUT_MILLIS)
+                    // Set a timeout in case image captured is dropped from the pipeline
+                    val exc = TimeoutException("Image dequeuing took too long")
+                    val timeoutRunnable = Runnable { cont.resumeWithException(exc) }
+                    imageReaderHandler?.postDelayed(timeoutRunnable, IMAGE_CAPTURE_TIMEOUT_MILLIS)
 
-                // Loop in the coroutine's context until an image with matching timestamp comes
-                // We need to launch the coroutine context again because the callback is done in
-                //  the handler provided to the `capture` method, not in our coroutine context
-                @Suppress("BlockingMethodInNonBlockingContext")
-                lifecycleScope.launch(cont.context) {
-                    while (true) {
+                    // Loop in the coroutine's context until an image with matching timestamp comes
+                    // We need to launch the coroutine context again because the callback is done in
+                    //  the handler provided to the `capture` method, not in our coroutine context
+                    @Suppress("BlockingMethodInNonBlockingContext")
+                    lifecycleScope.launch(cont.context) {
+                        while (true) {
 
-                        // Dequeue images while timestamps don't match
-                        val image = imageQueue.take()
-                        // if (image.timestamp != resultTimestamp) continue
-                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
-                            image.format != ImageFormat.DEPTH_JPEG &&
-                            image.timestamp != resultTimestamp) continue
-                        Log.d(TAG, "Matching image dequeued: ${image.timestamp}")
+                            // Dequeue images while timestamps don't match
+                            val image = imageQueue.take()
+                            // if (image.timestamp != resultTimestamp) continue
+                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
+                                image.format != ImageFormat.DEPTH_JPEG &&
+                                image.timestamp != resultTimestamp) continue
+                            Log.d(TAG, "Matching image dequeued: ${image.timestamp}")
 
-                        // Unset the image reader listener
-                        imageReaderHandler.removeCallbacks(timeoutRunnable)
-                        imageReader.setOnImageAvailableListener(null, null)
+                            // Unset the image reader listener
+                            imageReaderHandler?.removeCallbacks(timeoutRunnable)
+                            imageReader?.setOnImageAvailableListener(null, null)
 
-                        // Clear the queue of images, if there are left
-                        while (imageQueue.size > 0) {
-                            imageQueue.take().close()
+                            // Clear the queue of images, if there are left
+                            while (imageQueue.size > 0) {
+                                imageQueue.take().close()
+                            }
+
+                            // Compute EXIF orientation metadata
+                            val rotation = relativeOrientation?.value ?: 0
+                            val mirrored = characteristics?.get(
+                                CameraCharacteristics.LENS_FACING) ==
+                                CameraCharacteristics.LENS_FACING_FRONT
+                            val exifOrientation = computeExifOrientation(rotation, mirrored)
+
+                            // Build the result and resume progress
+                            cont.resume(CombinedCaptureResult(
+                                image, result, exifOrientation, checkNotNull(imageReader) {
+                                    "image reader cannot be null"
+                                }.imageFormat))
+                            // There is no need to break out of the loop, this coroutine will suspend
                         }
-
-                        // Compute EXIF orientation metadata
-                        val rotation = relativeOrientation.value ?: 0
-                        val mirrored = characteristics.get(CameraCharacteristics.LENS_FACING) ==
-                            CameraCharacteristics.LENS_FACING_FRONT
-                        val exifOrientation = computeExifOrientation(rotation, mirrored)
-
-                        // Build the result and resume progress
-                        cont.resume(CombinedCaptureResult(
-                            image, result, exifOrientation, imageReader.imageFormat))
-
-                        // There is no need to break out of the loop, this coroutine will suspend
                     }
                 }
-            }
-        }, cameraHandler)
+            }, cameraHandler)
+        }
     }
 
     /** Helper function used to save a [CombinedCaptureResult] into a [File] */
@@ -844,7 +882,9 @@
 
             // When the format is RAW we use the DngCreator utility library
             ImageFormat.RAW_SENSOR -> {
-                val dngCreator = DngCreator(characteristics, result.metadata)
+                val dngCreator = DngCreator(checkNotNull(characteristics) {
+                    "camera characteristics cannot be null"
+                }, result.metadata)
                 try {
                     val output = createFile("dng")
                     FileOutputStream(output).use { dngCreator.writeImage(it, result.image) }
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ky/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ky/strings.xml
index b776820..d6f99b5 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ky/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ky/strings.xml
@@ -35,7 +35,7 @@
     <string name="unchecked_action_title" msgid="802503415474307811">"Белгиси алынды"</string>
     <string name="on_action_title" msgid="4129601573763429611">"Күйүк"</string>
     <string name="off_action_title" msgid="8669201170189204848">"Өчүк"</string>
-    <string name="settings_action_title" msgid="8616900063253887861">"Жөндөөлөр"</string>
+    <string name="settings_action_title" msgid="8616900063253887861">"Параметрлер"</string>
     <string name="accept_action_title" msgid="4899660585470647578">"Кабыл алуу"</string>
     <string name="reject_action_title" msgid="6730366705938402668">"Четке кагуу"</string>
     <string name="ok_action_title" msgid="7128494973966098611">"Макул"</string>
@@ -63,7 +63,7 @@
     <string name="third_item_checked_toast_msg" msgid="3022450599567347361">"Үчүнчү нерсе текшерилди"</string>
     <string name="fifth_item_checked_toast_msg" msgid="1627599668504718594">"Бешинчи элемент текшерилди"</string>
     <string name="sixth_item_toast_msg" msgid="6117028866385793707">"Алтынчы нерсе чыкылдатылды"</string>
-    <string name="settings_toast_msg" msgid="7697794473002342727">"Жөндөөлөр чыкылдатылды"</string>
+    <string name="settings_toast_msg" msgid="7697794473002342727">"Параметрлер чыкылдатылды"</string>
     <string name="parked_toast_msg" msgid="2532422265890824446">"Токтотулган аракети"</string>
     <string name="more_toast_msg" msgid="5938288138225509885">"Дагы чыкылдатылган"</string>
     <string name="commute_toast_msg" msgid="4112684360647638688">"Өтүү баскычы басылды"</string>
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/CarMessage.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/CarMessage.java
index 51e24b5..439cb47 100644
--- a/car/app/app/src/main/java/androidx/car/app/messaging/model/CarMessage.java
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/CarMessage.java
@@ -24,11 +24,13 @@
 import androidx.annotation.Nullable;
 import androidx.car.app.annotations.CarProtocol;
 import androidx.car.app.annotations.ExperimentalCarApi;
+import androidx.car.app.annotations.KeepFields;
 import androidx.car.app.annotations.RequiresCarApi;
 import androidx.car.app.model.CarText;
-import androidx.car.app.annotations.KeepFields;
 import androidx.core.app.Person;
 
+import java.util.Objects;
+
 /** Represents a single message in a {@link ConversationItem} */
 @ExperimentalCarApi
 @CarProtocol
@@ -42,6 +44,72 @@
     private final long mReceivedTimeEpochMillis;
     private final boolean mIsRead;
 
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                getPersonHashCode(getSender()),
+                mBody,
+                mReceivedTimeEpochMillis,
+                mIsRead
+        );
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof CarMessage)) {
+            return false;
+        }
+        CarMessage otherCarMessage = (CarMessage) other;
+
+        return
+                arePeopleEqual(getSender(), otherCarMessage.getSender())
+                        && Objects.equals(mBody, otherCarMessage.mBody)
+                        && mReceivedTimeEpochMillis == otherCarMessage.mReceivedTimeEpochMillis
+                        && mIsRead == otherCarMessage.mIsRead
+                ;
+    }
+
+    // TODO(b/266877597): Move to androidx.core.app.Person
+    private static boolean arePeopleEqual(Person person1, Person person2) {
+        // If a unique ID was provided, use it
+        String key1 = person1.getKey();
+        String key2 = person2.getKey();
+        if (key1 != null || key2 != null) {
+            return Objects.equals(key1, key2);
+        }
+
+        // CharSequence doesn't have well-defined "equals" behavior -- convert to String instead
+        String name1 = Objects.toString(person1.getName());
+        String name2 = Objects.toString(person2.getName());
+
+        // Fallback: Compare field-by-field
+        return
+                Objects.equals(name1, name2)
+                        && Objects.equals(person1.getUri(), person2.getUri())
+                        && Objects.equals(person1.isBot(), person2.isBot())
+                        && Objects.equals(person1.isImportant(), person2.isImportant());
+    }
+
+    // TODO(b/266877597): Move to androidx.core.app.Person
+    private static int getPersonHashCode(Person person) {
+        // If a unique ID was provided, use it
+        String key = person.getKey();
+        if (key != null) {
+            return key.hashCode();
+        }
+
+        // Fallback: Use hash code for individual fields
+        return Objects.hash(
+                person.getName(),
+                person.getUri(),
+                person.isBot(),
+                person.isImportant()
+        );
+    }
+
     CarMessage(@NonNull Builder builder) {
         this.mSender = requireNonNull(builder.mSender).toBundle();
         this.mBody = requireNonNull(builder.mBody);
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..4c98df4 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,9 +31,11 @@
 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;
+import java.util.Objects;
 
 /** Represents a conversation */
 @ExperimentalCarApi
@@ -51,12 +55,43 @@
     @NonNull
     private final ConversationCallbackDelegate mConversationCallbackDelegate;
 
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                mId,
+                mTitle,
+                mIcon,
+                mIsGroupConversation,
+                mMessages
+        );
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof ConversationItem)) {
+            return false;
+        }
+        ConversationItem otherConversationItem = (ConversationItem) other;
+
+        return
+                Objects.equals(mId, otherConversationItem.mId)
+                        && Objects.equals(mTitle, otherConversationItem.mTitle)
+                        && Objects.equals(mIcon, otherConversationItem.mIcon)
+                        && mIsGroupConversation == otherConversationItem.mIsGroupConversation
+                        && Objects.equals(mMessages, otherConversationItem.mMessages)
+                ;
+    }
+
     ConversationItem(@NonNull Builder builder) {
         this.mId = requireNonNull(builder.mId);
         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/main/java/androidx/car/app/model/constraints/ActionsConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/ActionsConstraints.java
index 67ad8dc..edf1329 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/constraints/ActionsConstraints.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/ActionsConstraints.java
@@ -138,6 +138,7 @@
     public static final ActionsConstraints ACTIONS_CONSTRAINTS_ROW =
             new ActionsConstraints.Builder()
                     .setMaxActions(1)
+                    .setMaxCustomTitles(1)
                     .addAllowedActionType(Action.TYPE_CUSTOM)
                     .setRequireActionIcons(true)
                     .setOnClickListenerAllowed(true)
diff --git a/car/app/app/src/test/java/androidx/car/app/messaging/model/CarMessageTest.java b/car/app/app/src/test/java/androidx/car/app/messaging/model/CarMessageTest.java
new file mode 100644
index 0000000..f250727
--- /dev/null
+++ b/car/app/app/src/test/java/androidx/car/app/messaging/model/CarMessageTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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 com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.model.CarText;
+import androidx.core.app.Person;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+/** Tests for {@link CarMessage}. */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class CarMessageTest {
+    /** Ensure the builder does not fail for the minimum set of required fields. */
+    @Test
+    public void build_withRequiredFieldsOnly() {
+        TestConversationFactory.createMinimalMessage();
+
+        // assert no crash
+    }
+
+    /** Ensure the builder does not fail when all fields are assigned. */
+    @Test
+    public void build_withAllFields() {
+        TestConversationFactory.createFullyPopulatedMessage();
+
+        // assert no crash
+    }
+
+    // Ignore nullability, so we can null out a builder field
+    @SuppressWarnings("ConstantConditions")
+    @Test
+    public void build_throwsException_ifSenderMissing() {
+        assertThrows(
+                NullPointerException.class,
+                () -> TestConversationFactory.createMinimalMessageBuilder()
+                        .setSender(null)
+                        .build()
+        );
+    }
+
+    // Ignore nullability, so we can null out a builder field
+    @SuppressWarnings("ConstantConditions")
+    @Test
+    public void build_throwsException_ifMessageBodyMissing() {
+        assertThrows(
+                NullPointerException.class,
+                () -> TestConversationFactory.createMinimalMessageBuilder()
+                        .setBody(null)
+                        .build()
+        );
+    }
+
+    // region .equals() & .hashCode()
+    @Test
+    public void equalsAndHashCode_areEqual_forMinimalMessage() {
+        CarMessage message1 =
+                TestConversationFactory.createMinimalMessage();
+        CarMessage message2 =
+                TestConversationFactory.createMinimalMessage();
+
+        assertEqual(message1, message2);
+    }
+
+    @Test
+    public void equalsAndHashCode_areEqual_forFullyPopulatedMessage() {
+        CarMessage message1 =
+                TestConversationFactory.createFullyPopulatedMessage();
+        CarMessage message2 =
+                TestConversationFactory.createFullyPopulatedMessage();
+
+        assertEqual(message1, message2);
+    }
+
+    @Test
+    public void equalsAndHashCode_produceCorrectResult_ifIndividualFieldDiffers() {
+        // Create base item, for comparison
+        CarMessage fullyPopulatedMessage =
+                TestConversationFactory.createFullyPopulatedMessage();
+
+        // Create various non-equal items
+        CarMessage modifiedSender =
+                TestConversationFactory
+                        .createFullyPopulatedMessageBuilder()
+                        .setSender(
+                                TestConversationFactory
+                                        .createMinimalPersonBuilder()
+                                        .setKey("Modified Key")
+                                        .build()
+                        )
+                        .build();
+        CarMessage modifiedBody =
+                TestConversationFactory
+                        .createFullyPopulatedMessageBuilder()
+                        .setBody(CarText.create("Modified Message Body"))
+                        .build();
+        CarMessage modifiedReceivedTimeEpochMillis =
+                TestConversationFactory
+                        .createFullyPopulatedMessageBuilder()
+                        .setReceivedTimeEpochMillis(
+                                // Guaranteed to be different :)
+                                fullyPopulatedMessage.getReceivedTimeEpochMillis() + 1
+                        )
+                        .build();
+        CarMessage modifiedIsRead =
+                TestConversationFactory
+                        .createFullyPopulatedMessageBuilder()
+                        .setRead(!fullyPopulatedMessage.isRead())
+                        .build();
+
+        // Verify (lack of) equality
+        assertNotEqual(fullyPopulatedMessage, modifiedSender);
+        assertNotEqual(fullyPopulatedMessage, modifiedBody);
+        assertNotEqual(fullyPopulatedMessage, modifiedReceivedTimeEpochMillis);
+        assertNotEqual(fullyPopulatedMessage, modifiedIsRead);
+    }
+
+    @Test
+    public void equalsAndHashCode_produceCorrectResult_ifSenderIdMissing() {
+        Person.Builder senderWithoutId = TestConversationFactory
+                .createMinimalPersonBuilder()
+                .setKey(null);
+        Person.Builder senderWithId = TestConversationFactory
+                .createMinimalPersonBuilder()
+                .setKey("Test Sender Key");
+
+        // Create Test Data
+        CarMessage senderKeyMissingWithStandardData =
+                TestConversationFactory.createMinimalMessageBuilder()
+                        .setSender(senderWithoutId.build())
+                        .build();
+        CarMessage senderKeyMissingWithStandardData2 =
+                TestConversationFactory.createMinimalMessageBuilder()
+                        .setSender(senderWithoutId.build())
+                        .build();
+        CarMessage senderKeyMissingWithModifiedData =
+                TestConversationFactory.createMinimalMessageBuilder()
+                        .setSender(senderWithoutId.build())
+                        .setBody(CarText.create("Modified Message Body"))
+                        .build();
+        CarMessage senderKeyProvidedWithStandardData =
+                TestConversationFactory.createMinimalMessageBuilder()
+                        .setSender(senderWithId.build())
+                        .build();
+        CarMessage senderKeyProvidedWithStandardData2 =
+                TestConversationFactory.createMinimalMessageBuilder()
+                        .setSender(senderWithId.build())
+                        .build();
+        CarMessage senderKeyProvidedWithModifiedData =
+                TestConversationFactory.createMinimalMessageBuilder()
+                        .setSender(senderWithId.build())
+                        .setBody(CarText.create("Modified Message Body"))
+                        .build();
+
+        // Verify (in)equality
+
+        // Sender & message content are equal: == EQUAL ==
+        assertEqual(senderKeyMissingWithStandardData, senderKeyMissingWithStandardData2);
+        assertEqual(senderKeyProvidedWithStandardData, senderKeyProvidedWithStandardData2);
+
+        // One sender is missing a key: == NOT EQUAL ==
+        assertNotEqual(senderKeyMissingWithStandardData, senderKeyProvidedWithStandardData);
+
+        // Sender is equal, but message content is not: == NOT EQUAL ==
+        assertNotEqual(senderKeyMissingWithStandardData, senderKeyMissingWithModifiedData);
+        assertNotEqual(senderKeyProvidedWithStandardData, senderKeyProvidedWithModifiedData);
+    }
+
+    private void assertEqual(CarMessage message1, CarMessage message2) {
+        assertThat(message1).isEqualTo(message2);
+        assertThat(message1.hashCode()).isEqualTo(message2.hashCode());
+    }
+
+    private void assertNotEqual(CarMessage message1, CarMessage message2) {
+        assertThat(message1).isNotEqualTo(message2);
+        assertThat(message1.hashCode()).isNotEqualTo(message2.hashCode());
+    }
+    // endregion
+}
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..9f61ade
--- /dev/null
+++ b/car/app/app/src/test/java/androidx/car/app/messaging/model/ConversationItemTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** 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.createMinimalConversationItem();
+
+        // assert no crash
+    }
+
+    /** Ensure the builder does not fail when all fields are assigned. */
+    @Test
+    public void build_withAllFields() {
+        TestConversationFactory.createFullyPopulatedConversationItem();
+
+        // assert no crash
+    }
+
+    @Test
+    public void build_throwsException_ifMessageListEmpty() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> TestConversationFactory.createMinimalConversationItemBuilder()
+                        .setMessages(new ArrayList<>())
+                        .build()
+        );
+    }
+
+    // region .equals() & .hashCode()
+    @Test
+    public void equalsAndHashCode_areEqual_forMinimalConversationItem() {
+        ConversationItem item1 =
+                TestConversationFactory.createMinimalConversationItem();
+        ConversationItem item2 =
+                TestConversationFactory.createMinimalConversationItem();
+
+        assertEqual(item1, item2);
+    }
+
+    @Test
+    public void equalsAndHashCode_areEqual_forFullyPopulatedConversationItem() {
+        ConversationItem item1 =
+                TestConversationFactory.createFullyPopulatedConversationItem();
+        ConversationItem item2 =
+                TestConversationFactory.createFullyPopulatedConversationItem();
+
+        assertEqual(item1, item2);
+    }
+
+    @Test
+    public void equalsAndHashCode_produceCorrectResult_ifIndividualFieldDiffers() {
+        // Create base item, for comparison
+        ConversationItem fullyPopulatedItem =
+                TestConversationFactory.createFullyPopulatedConversationItem();
+
+        // Create various non-equal items
+        ConversationItem modifiedId =
+                TestConversationFactory
+                        .createFullyPopulatedConversationItemBuilder()
+                        .setId("Modified ID")
+                        .build();
+        ConversationItem modifiedTitle =
+                TestConversationFactory
+                        .createFullyPopulatedConversationItemBuilder()
+                        .setTitle(CarText.create("Modified Title"))
+                        .build();
+        ConversationItem modifiedIcon =
+                TestConversationFactory
+                        .createFullyPopulatedConversationItemBuilder()
+                        .setIcon(CarIcon.ALERT)
+                        .build();
+        ConversationItem modifiedGroupStatus =
+                TestConversationFactory
+                        .createFullyPopulatedConversationItemBuilder()
+                        .setGroupConversation(!fullyPopulatedItem.isGroupConversation())
+                        .build();
+        List<CarMessage> modifiedMessages = new ArrayList<>(1);
+        modifiedMessages.add(
+                TestConversationFactory
+                        .createMinimalMessageBuilder()
+                        .setBody(CarText.create("Modified Message Body"))
+                        .build()
+        );
+        ConversationItem modifiedMessageList =
+                TestConversationFactory
+                        .createFullyPopulatedConversationItemBuilder()
+                        .setMessages(modifiedMessages)
+                        .build();
+        ConversationItem modifiedConversationCallback =
+                TestConversationFactory
+                        .createFullyPopulatedConversationItemBuilder()
+                        .setConversationCallback(new ConversationCallback() {
+                            @Override
+                            public void onMarkAsRead() {
+
+                            }
+
+                            @Override
+                            public void onTextReply(@NonNull String replyText) {
+
+                            }
+                        })
+                        .build();
+
+        // Verify (lack of) equality
+        assertNotEqual(fullyPopulatedItem, modifiedId);
+        assertNotEqual(fullyPopulatedItem, modifiedTitle);
+        assertNotEqual(fullyPopulatedItem, modifiedIcon);
+        assertNotEqual(fullyPopulatedItem, modifiedGroupStatus);
+        assertNotEqual(fullyPopulatedItem, modifiedMessageList);
+
+        // NOTE: Conversation Callback does not affect equality
+        assertEqual(fullyPopulatedItem, modifiedConversationCallback);
+    }
+
+    private void assertEqual(ConversationItem item1, ConversationItem item2) {
+        assertThat(item1).isEqualTo(item2);
+        assertThat(item1.hashCode()).isEqualTo(item2.hashCode());
+    }
+
+    private void assertNotEqual(ConversationItem item1, ConversationItem item2) {
+        assertThat(item1).isNotEqualTo(item2);
+        assertThat(item1.hashCode()).isNotEqualTo(item2.hashCode());
+    }
+    // endregion
+}
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..19359bb
--- /dev/null
+++ b/car/app/app/src/test/java/androidx/car/app/messaging/model/TestConversationFactory.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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 android.graphics.Bitmap;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.ItemList;
+import androidx.core.app.Person;
+import androidx.core.graphics.drawable.IconCompat;
+
+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 IconCompat TEST_SENDER_ICON =
+            IconCompat.createWithBitmap(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888));
+
+    private static final Uri TEST_SENDER_URI =
+            Uri.parse("http://foo.com/test/sender/uri");
+    private static final ConversationCallback EMPTY_CONVERSATION_CALLBACK =
+            new ConversationCallback() {
+                @Override
+                public void onMarkAsRead() {
+                }
+
+                @Override
+                public void onTextReply(@NonNull String replyText) {
+                }
+            };
+
+    // region Person
+    /**
+     * Creates a {@link Person.Builder} instance for testing
+     *
+     * <p>This method fills in the minimum required data to create a valid {@link Person}.
+     */
+    public static Person.Builder createMinimalPersonBuilder() {
+        return new Person.Builder().setName("Person Name");
+    }
+
+    /**
+     * 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 createMinimalPersonBuilder().build();
+    }
+
+    /**
+     * Creates a {@link Person.Builder} instance for testing
+     *
+     * <p>This method fills in the minimum required data to create a valid {@link Person}.
+     */
+    public static Person.Builder createFullyPopulatedPersonBuilder() {
+        return createMinimalPersonBuilder()
+                .setKey("Foo Person")
+                .setIcon(TEST_SENDER_ICON)
+                .setUri(TEST_SENDER_URI.toString())
+                .setBot(true)
+                .setImportant(true);
+    }
+
+    /**
+     * 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 createFullyPopulatedPerson() {
+        return createFullyPopulatedPersonBuilder().build();
+    }
+    // endregion
+
+    // region Message
+    /**
+     * Creates a {@link CarMessage.Builder} instance for testing
+     *
+     * <p>This method fills in the minimum required data to create a valid {@link CarMessage}.
+     */
+    public static CarMessage.Builder createMinimalMessageBuilder() {
+        return new CarMessage.Builder()
+                .setSender(createMinimalPerson())
+                .setBody(CarText.create("Message body"));
+    }
+
+    /**
+     * Creates a {@link CarMessage} instance for testing
+     *
+     * <p>This method fills in the minimum required data to create a valid {@link CarMessage}.
+     */
+    public static CarMessage createMinimalMessage() {
+        return createMinimalMessageBuilder().build();
+    }
+
+    /**
+     * Creates a {@link CarMessage.Builder} instance for testing
+     *
+     * <p>This method populates every field in  {@link CarMessage.Builder}.
+     */
+    public static CarMessage.Builder createFullyPopulatedMessageBuilder() {
+        return createMinimalMessageBuilder()
+                .setRead(true)
+                .setReceivedTimeEpochMillis(12345);
+    }
+
+    /**
+     * Creates a {@link CarMessage} instance for testing
+     *
+     * <p>This method populates every field in  {@link CarMessage.Builder}.
+     */
+    public static CarMessage createFullyPopulatedMessage() {
+        return createFullyPopulatedMessageBuilder().build();
+    }
+    // endregion
+
+    // region ConversationItem
+    /**
+     * 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();
+    }
+
+    /**
+     * Creates a {@link ConversationItem.Builder} instance for testing
+     *
+     * <p>This method populates every field in {@link ConversationItem.Builder}.
+     */
+    public static ConversationItem.Builder createFullyPopulatedConversationItemBuilder() {
+        return createMinimalConversationItemBuilder()
+                // APP_ICON was chosen because it is easy to access in code
+                // In the future, it may make more sense to add a "realistic" contact photo here.
+                .setIcon(CarIcon.APP_ICON)
+                .setGroupConversation(true);
+    }
+
+    /**
+     * Creates a {@link ConversationItem} instance for testing
+     *
+     * <p>This method populates every field in {@link ConversationItem.Builder}.
+     */
+    public static ConversationItem createFullyPopulatedConversationItem() {
+        return createFullyPopulatedConversationItemBuilder().build();
+    }
+    // endregion
+
+    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/collection/collection-benchmark/build.gradle b/collection/collection-benchmark/build.gradle
index 9d23ccc..dc0cf11 100644
--- a/collection/collection-benchmark/build.gradle
+++ b/collection/collection-benchmark/build.gradle
@@ -118,6 +118,7 @@
     scheme = "testapp-ios"
     // ios 13, 15.2
     destination = "platform=iOS Simulator,name=iPhone 13,OS=15.2"
+    referenceSha.set(androidx.getReferenceSha())
 }
 
 android {
diff --git a/collection/collection/build.gradle b/collection/collection/build.gradle
index a3b11b3..0ac4f14 100644
--- a/collection/collection/build.gradle
+++ b/collection/collection/build.gradle
@@ -131,7 +131,7 @@
     // collection-ktx transitively, which would lead to duplicate definition since the -ktx
     // extensions were moved into the main artifact.
     constraints {
-        jvmImplementation("androidx.collection:collection-ktx:1.3.0-alpha01")
+        jvmMainImplementation("androidx.collection:collection-ktx:1.3.0-alpha01")
     }
 }
 
diff --git a/compose/OWNERS b/compose/OWNERS
index 578f153..d441e85 100644
--- a/compose/OWNERS
+++ b/compose/OWNERS
@@ -2,3 +2,6 @@
 chuckj@google.com
 jsproch@google.com
 lelandr@google.com
+aelias@google.com
+malkov@google.com
+clarabayarri@google.com
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-daemon/src/main/kotlin/androidx/compose/compiler/daemon/Compiler.kt b/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/Compiler.kt
index 3038c88..976bfa8 100644
--- a/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/Compiler.kt
+++ b/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/Compiler.kt
@@ -98,7 +98,7 @@
         workingDir = Files.createTempDirectory("workingDir").toFile(),
         reporter = BuildReporter(DoNothingICReporter, DoNothingBuildMetricsReporter),
         usePreciseJavaTracking = true,
-        outputFiles = emptyList(),
+        outputDirs = null,
         buildHistoryFile = Files.createTempFile("build-history", ".bin").toFile(),
         modulesApiHistory = EmptyModulesApiHistory,
         kotlinSourceFilesExtensions = DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS,
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 3c1d258..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() }
@@ -533,12 +534,13 @@
               public final useAB()V
               static <clinit>()V
               public final static I %stable
+              public final static INNERCLASS A%B A B
             }
         """
     )
 
     @Test
-    fun testDataClassHashCode(): Unit = validateBytecode(
+    fun testDataClassHashCode() = validateBytecode(
         """
         data class Foo(
             val bar: @Composable () -> Unit
@@ -550,7 +552,7 @@
 
     @Test
     @Ignore("b/179279455")
-    fun testCorrectComposerPassed1(): Unit = checkComposerParam(
+    fun testCorrectComposerPassed1() = checkComposerParam(
         """
             var a: Composer? = null
             fun run() {
@@ -564,7 +566,7 @@
 
     @Test
     @Ignore("b/179279455")
-    fun testCorrectComposerPassed2(): Unit = checkComposerParam(
+    fun testCorrectComposerPassed2() = checkComposerParam(
         """
             var a: Composer? = null
             @Composable fun Foo() {
@@ -581,7 +583,7 @@
 
     @Test
     @Ignore("b/179279455")
-    fun testCorrectComposerPassed3(): Unit = checkComposerParam(
+    fun testCorrectComposerPassed3() = checkComposerParam(
         """
             var a: Composer? = null
             var b: Composer? = null
@@ -605,7 +607,7 @@
 
     @Test
     @Ignore("b/179279455")
-    fun testCorrectComposerPassed4(): Unit = checkComposerParam(
+    fun testCorrectComposerPassed4() = checkComposerParam(
         """
             var a: Composer? = null
             var b: Composer? = null
@@ -632,7 +634,7 @@
 
     @Test
     @Ignore("b/179279455")
-    fun testCorrectComposerPassed5(): Unit = checkComposerParam(
+    fun testCorrectComposerPassed5() = checkComposerParam(
         """
             var a: Composer? = null
             @Composable fun Wrap(content: @Composable () -> Unit) {
@@ -657,7 +659,7 @@
     )
 
     @Test
-    fun testDefaultParameters(): Unit = checkApi(
+    fun testDefaultParameters() = checkApi(
         """
             @Composable fun Foo(x: Int = 0) {
 
@@ -682,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)) {
@@ -723,7 +725,7 @@
     )
 
     @Test
-    fun testBasicCallAndParameterUsage(): Unit = checkApi(
+    fun testBasicCallAndParameterUsage() = checkApi(
         """
             @Composable fun Foo(a: Int, b: String) {
                 print(a)
@@ -767,7 +769,7 @@
     )
 
     @Test
-    fun testCallFromInlinedLambda(): Unit = checkApi(
+    fun testCallFromInlinedLambda() = checkApi(
         """
             @Composable fun Foo() {
                 listOf(1, 2, 3).forEach { Bar(it) }
@@ -803,7 +805,7 @@
     )
 
     @Test
-    fun testBasicLambda(): Unit = checkApi(
+    fun testBasicLambda() = checkApi(
         """
             val foo = @Composable { x: Int -> print(x)  }
             @Composable fun Bar() {
@@ -847,7 +849,7 @@
     )
 
     @Test
-    fun testLocalLambda(): Unit = checkApi(
+    fun testLocalLambda() = checkApi(
         """
             @Composable fun Bar(content: @Composable () -> Unit) {
                 val foo = @Composable { x: Int -> print(x)  }
@@ -890,7 +892,7 @@
     )
 
     @Test
-    fun testNesting(): Unit = checkApi(
+    fun testNesting() = checkApi(
         """
             @Composable fun Wrap(content: @Composable (x: Int) -> Unit) {
                 content(123)
@@ -931,8 +933,8 @@
               public synthetic bridge invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
               final synthetic I %x
               OUTERCLASS TestKt App (ILandroidx/compose/runtime/Composer;I)V
-              final static INNERCLASS TestKt%App%1%1 null null
               final static INNERCLASS TestKt%App%1 null null
+              final static INNERCLASS TestKt%App%1%1 null null
             }
             final class TestKt%App%1%1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function3 {
               <init>(II)V
@@ -957,7 +959,7 @@
     )
 
     @Test
-    fun testComposableInterface(): Unit = checkApi(
+    fun testComposableInterface() = checkApi(
         """
             interface Foo {
                 @Composable fun bar()
@@ -991,7 +993,7 @@
     )
 
     @Test
-    fun testSealedClassEtc(): Unit = checkApi(
+    fun testSealedClassEtc() = checkApi(
         """
             sealed class CompositionLocal2<T> {
                 inline val current: T
@@ -1042,7 +1044,7 @@
     )
 
     @Test
-    fun testComposableTopLevelProperty(): Unit = checkApi(
+    fun testComposableTopLevelProperty() = checkApi(
         """
             val foo: Int @Composable get() { return 123 }
         """,
@@ -1054,7 +1056,7 @@
     )
 
     @Test
-    fun testComposableProperty(): Unit = checkApi(
+    fun testComposableProperty() = checkApi(
         """
             class Foo {
                 val foo: Int @Composable get() { return 123 }
@@ -1071,7 +1073,7 @@
     )
 
     @Test
-    fun testTableLambdaThing(): Unit = validateBytecode(
+    fun testTableLambdaThing() = validateBytecode(
         """
         @Composable
         fun Foo() {
@@ -1086,7 +1088,7 @@
     }
 
     @Test
-    fun testDefaultArgs(): Unit = validateBytecode(
+    fun testDefaultArgs() = validateBytecode(
         """
         @Composable
         fun Scaffold(
@@ -1098,7 +1100,7 @@
     }
 
     @Test
-    fun testSyntheticAccessFunctions(): Unit = validateBytecode(
+    fun testSyntheticAccessFunctions() = validateBytecode(
         """
         class Foo {
             @Composable private fun Bar() {}
@@ -1109,7 +1111,7 @@
     }
 
     @Test
-    fun testLambdaMemoization(): Unit = validateBytecode(
+    fun testLambdaMemoization() = validateBytecode(
         """
         fun subcompose(block: @Composable () -> Unit) {}
         private class Foo {
@@ -1127,7 +1129,7 @@
     }
 
     @Test
-    fun testCallingProperties(): Unit = checkApi(
+    fun testCallingProperties() = checkApi(
         """
             val bar: Int @Composable get() { return 123 }
 
@@ -1153,7 +1155,7 @@
     )
 
     @Test
-    fun testAbstractComposable(): Unit = checkApi(
+    fun testAbstractComposable() = checkApi(
         """
             abstract class BaseFoo {
                 @Composable abstract fun bar()
@@ -1190,7 +1192,7 @@
     )
 
     @Test
-    fun testLocalClassAndObjectLiterals(): Unit = checkApi(
+    fun testLocalClassAndObjectLiterals() = checkApi(
         """
             @Composable
             fun Wat() {}
@@ -1242,7 +1244,7 @@
     )
 
     @Test
-    fun testNonComposableCode(): Unit = checkApi(
+    fun testNonComposableCode() = checkApi(
         """
             fun A() {}
             val b: Int get() = 123
@@ -1296,8 +1298,8 @@
               static <clinit>()V
               public final static LTestKt%J%1; INSTANCE
               OUTERCLASS TestKt J ()V
-              final static INNERCLASS TestKt%J%1%1 null null
               final static INNERCLASS TestKt%J%1 null null
+              final static INNERCLASS TestKt%J%1%1 null null
             }
             final class TestKt%J%1%1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function0 {
               <init>()V
@@ -1313,7 +1315,7 @@
     )
 
     @Test
-    fun testCircularCall(): Unit = checkApi(
+    fun testCircularCall() = checkApi(
         """
             @Composable fun Example() {
                 Example()
@@ -1336,7 +1338,7 @@
     )
 
     @Test
-    fun testInlineCall(): Unit = checkApi(
+    fun testInlineCall() = checkApi(
         """
             @Composable inline fun Example(content: @Composable () -> Unit) {
                 content()
@@ -1364,7 +1366,7 @@
     )
 
     @Test
-    fun testDexNaming(): Unit = checkApi(
+    fun testDexNaming() = checkApi(
         """
             val myProperty: () -> Unit @Composable get() {
                 return {  }
@@ -1388,7 +1390,7 @@
     )
 
     @Test
-    fun testInnerClass(): Unit = checkApi(
+    fun testInnerClass() = checkApi(
         """
             interface A {
                 fun b() {}
@@ -1429,7 +1431,7 @@
     )
 
     @Test
-    fun testFunInterfaces(): Unit = checkApi(
+    fun testFunInterfaces() = checkApi(
         """
             fun interface A {
                 fun compute(value: Int): Unit
@@ -1462,7 +1464,7 @@
     )
 
     @Test
-    fun testComposableFunInterfaces(): Unit = checkApi(
+    fun testComposableFunInterfaces() = checkApi(
         """
             fun interface A {
                 @Composable fun compute(value: Int): Unit
@@ -1484,8 +1486,8 @@
               public final compute(ILandroidx/compose/runtime/Composer;I)V
               final synthetic LA; %a
               OUTERCLASS TestKt Example (LA;)V
-              final static INNERCLASS TestKt%Example%1%compute%1 null null
               final static INNERCLASS TestKt%Example%1 null null
+              final static INNERCLASS TestKt%Example%1%compute%1 null null
             }
             final class TestKt%Example%1%compute%1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function2 {
               <init>(LTestKt%Example%1;II)V
@@ -1502,7 +1504,7 @@
     )
 
     @Test
-    fun testFunInterfaceWithInlineReturnType(): Unit = checkApi(
+    fun testFunInterfaceWithInlineReturnType() = checkApi(
         """
             inline class Color(val value: Int)
             fun interface A {
@@ -1547,7 +1549,7 @@
     )
 
     @Test
-    fun testComposableFunInterfaceWithInlineReturnType(): Unit = checkApi(
+    fun testComposableFunInterfaceWithInlineReturnType() = checkApi(
         """
             inline class Color(val value: Int)
             fun interface A {
@@ -1592,7 +1594,7 @@
     )
 
     @Test
-    fun testComposableMap(): Unit = codegen(
+    fun testComposableMap() = codegen(
         """
             class Repro {
                 private val composables = linkedMapOf<String, @Composable () -> Unit>()
@@ -1605,7 +1607,7 @@
     )
 
     @Test
-    fun testComposableColorFunInterfaceExample(): Unit = checkApi(
+    fun testComposableColorFunInterfaceExample() = checkApi(
         """
             import androidx.compose.material.Text
             import androidx.compose.ui.graphics.Color
@@ -1654,6 +1656,7 @@
               public final static LTestKt%Test%1; INSTANCE
               OUTERCLASS TestKt Test (Landroidx/compose/runtime/Composer;I)V
               final static INNERCLASS TestKt%Test%1 null null
+              public final static INNERCLASS androidx/compose/ui/graphics/Color%Companion androidx/compose/ui/graphics/Color Companion
             }
             final class TestKt%Test%2 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function2 {
               <init>(I)V
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..68825f3 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()) {
@@ -4027,16 +3931,15 @@
             @Composable
             fun <T> provided(value: T, %composer: Composer?, %changed: Int): State<T> {
               %composer.startReplaceableGroup(<>)
-              sourceInformation(%composer, "C(provided):Test.kt")
+              sourceInformation(%composer, "C(provided)*<rememb...>:Test.kt")
               if (isTraceInProgress()) {
                 traceEventStart(<>, %changed, -1, <>)
               }
-              val tmp0 = %composer.cache(false) {
+              val tmp0 = remember({
                 mutableStateOf(
                   value = value
                 )
-              }
-              .apply {
+              }, %composer, 0).apply {
                 %this%apply.value = value
               }
               if (isTraceInProgress()) {
@@ -4156,14 +4059,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 +5400,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 +5489,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 +5541,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 +5589,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 +5610,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 +5699,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()
@@ -5890,7 +5768,7 @@
             @Composable
             fun Test(start: Int, end: Int, %composer: Composer?, %changed: Int) {
               %composer = %composer.startRestartGroup(<>)
-              sourceInformation(%composer, "C(Test)P(1)*<get(bK...>:Test.kt")
+              sourceInformation(%composer, "C(Test)P(1)<rememb...>,*<get(bK...>:Test.kt")
               val %dirty = %changed
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(start)) 0b0100 else 0b0010
@@ -5902,9 +5780,9 @@
                 if (isTraceInProgress()) {
                   traceEventStart(<>, %changed, -1, <>)
                 }
-                val a = %composer.cache(false) {
+                val a = remember({
                   A()
-                }
+                }, %composer, 0)
                 val tmp0_iterator = start until end.iterator()
                 while (tmp0_iterator.hasNext()) {
                   val i = tmp0_iterator.next()
@@ -6028,4 +5906,80 @@
             }
         """
     )
-}
\ No newline at end of file
+
+    @Test
+    fun testComposeIrSkippingWithDefaultsRelease() = verifyComposeIrTransform(
+        """
+            import androidx.compose.ui.text.input.TextFieldValue
+            import androidx.compose.runtime.*
+            import androidx.compose.foundation.layout.*
+            import androidx.compose.foundation.text.KeyboardActions
+            import androidx.compose.material.*
+
+            object Ui {}
+
+            @Composable
+            fun Ui.UiTextField(
+                isError: Boolean = false,
+                keyboardActions2: Boolean = false,
+            ) {
+                println("t41 insideFunction ${'$'}isError")
+                println("t41 insideFunction ${'$'}keyboardActions2")
+                Column {
+                    Text("${'$'}isError")
+                    Text("${'$'}keyboardActions2")
+                }
+            }
+        """.trimIndent(),
+        """
+            @StabilityInferred(parameters = 0)
+            object Ui {
+              static val %stable: Int = 0
+            }
+            @Composable
+            @ComposableTarget(applier = "androidx.compose.ui.UiComposable")
+            fun Ui.UiTextField(isError: Boolean, keyboardActions2: Boolean, %composer: Composer?, %changed: Int, %default: Int) {
+              %composer = %composer.startRestartGroup(<>)
+              sourceInformation(%composer, "C(UiTextField)<Column>:Test.kt")
+              val %dirty = %changed
+              if (%default and 0b0001 !== 0) {
+                %dirty = %dirty or 0b00110000
+              } else if (%changed and 0b01110000 === 0) {
+                %dirty = %dirty or if (%composer.changed(isError)) 0b00100000 else 0b00010000
+              }
+              if (%default and 0b0010 !== 0) {
+                %dirty = %dirty or 0b000110000000
+              } else if (%changed and 0b001110000000 === 0) {
+                %dirty = %dirty or if (%composer.changed(keyboardActions2)) 0b000100000000 else 0b10000000
+              }
+              if (%dirty and 0b001011010001 !== 0b10010000 || !%composer.skipping) {
+                if (%default and 0b0001 !== 0) {
+                  isError = false
+                }
+                if (%default and 0b0010 !== 0) {
+                  keyboardActions2 = false
+                }
+                if (isTraceInProgress()) {
+                  traceEventStart(<>, %changed, -1, <>)
+                }
+                println("t41 insideFunction %isError")
+                println("t41 insideFunction %keyboardActions2")
+                Column(null, null, null, { %composer: Composer?, %changed: Int ->
+                  sourceInformationMarkerStart(%composer, <>, "C<Text("...>,<Text("...>:Test.kt")
+                  Text("%isError", null, <unsafe-coerce>(0L), <unsafe-coerce>(0L), null, null, null, <unsafe-coerce>(0L), null, null, <unsafe-coerce>(0L), <unsafe-coerce>(0), false, 0, 0, null, null, %composer, 0, 0, 0b00011111111111111110)
+                  Text("%keyboardActions2", null, <unsafe-coerce>(0L), <unsafe-coerce>(0L), null, null, null, <unsafe-coerce>(0L), null, null, <unsafe-coerce>(0L), <unsafe-coerce>(0), false, 0, 0, null, null, %composer, 0, 0, 0b00011111111111111110)
+                  sourceInformationMarkerEnd(%composer)
+                }, %composer, 0, 0b0111)
+                if (isTraceInProgress()) {
+                  traceEventEnd()
+                }
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                UiTextField(isError, keyboardActions2, %composer, updateChangedFlags(%changed or 0b0001), %default)
+              }
+            }
+        """
+    )
+}
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..87e5fde 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
@@ -215,7 +215,7 @@
             @Composable
             fun Err(y: Int, z: Int, %composer: Composer?, %changed: Int) {
               %composer.startReplaceableGroup(<>)
-              sourceInformation(%composer, "C(Err):Test.kt")
+              sourceInformation(%composer, "C(Err)<{>:Test.kt")
               if (isTraceInProgress()) {
                 traceEventStart(<>, %changed, -1, <>)
               }
@@ -225,11 +225,11 @@
                   return x + y + w
                 }
               }
-              %composer.cache(%changed and 0b1110 xor 0b0110 > 4 && %composer.changed(y) || %changed and 0b0110 === 0b0100 or %changed and 0b01110000 xor 0b00110000 > 32 && %composer.changed(z) || %changed and 0b00110000 === 0b00100000) {
+              remember(y, z, {
                 {
                   Local().something(2)
                 }
-              }
+              }, %composer, 0b1110 and %changed or 0b01110000 and %changed)
               if (isTraceInProgress()) {
                 traceEventEnd()
               }
@@ -241,7 +241,7 @@
     )
 
     @Test
-    fun testLocalClassCaptures2(): Unit = verifyComposeIrTransform(
+    fun testLocalClassCaptures2() = verifyComposeIrTransform(
         """
             import androidx.compose.runtime.Composable
             import androidx.compose.runtime.NonRestartableComposable
@@ -260,18 +260,18 @@
             @Composable
             fun Example(z: Int, %composer: Composer?, %changed: Int) {
               %composer.startReplaceableGroup(<>)
-              sourceInformation(%composer, "C(Example):Test.kt")
+              sourceInformation(%composer, "C(Example)<{>:Test.kt")
               if (isTraceInProgress()) {
                 traceEventStart(<>, %changed, -1, <>)
               }
               class Foo(val x: Int) {
                 val y: Int = z
               }
-              val lambda = %composer.cache(%changed and 0b1110 xor 0b0110 > 4 && %composer.changed(z) || %changed and 0b0110 === 0b0100) {
+              val lambda = remember(z, {
                 {
                   Foo(1)
                 }
-              }
+              }, %composer, 0b1110 and %changed)
               if (isTraceInProgress()) {
                 traceEventEnd()
               }
@@ -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) {
@@ -622,7 +622,7 @@
             @Composable
             fun Example(x: Int, %composer: Composer?, %changed: Int) {
               %composer = %composer.startRestartGroup(<>)
-              sourceInformation(%composer, "C(Example):Test.kt")
+              sourceInformation(%composer, "C(Example)<{>:Test.kt")
               val %dirty = %changed
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
@@ -634,11 +634,11 @@
                 fun foo() {
                   use(x)
                 }
-                val shouldMemoize = %composer.cache(%dirty and 0b1110 === 0b0100) {
+                val shouldMemoize = remember(x, {
                   {
                     foo
                   }
-                }
+                }, %composer, 0b1110 and %dirty)
                 if (isTraceInProgress()) {
                   traceEventEnd()
                 }
@@ -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..a95dc1b 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() {
@@ -549,4 +574,176 @@
             }
         """
     )
+
+    @Test
+    fun testComposeIrSkippingWithDefaultsRelease() = verifyComposeIrTransform(
+        """
+            import androidx.compose.ui.text.input.TextFieldValue
+            import androidx.compose.runtime.*
+            import androidx.compose.foundation.layout.*
+            import androidx.compose.foundation.text.KeyboardActions
+            import androidx.compose.material.*
+
+            object Ui {}
+
+            @Composable
+            fun Ui.UiTextField(
+                isError: Boolean = false,
+                keyboardActions2: Boolean = false,
+            ) {
+                println("t41 insideFunction ${'$'}isError")
+                println("t41 insideFunction ${'$'}keyboardActions2")
+                Column {
+                    Text("${'$'}isError")
+                    Text("${'$'}keyboardActions2")
+                }
+            }
+        """.trimIndent(),
+        """
+            @StabilityInferred(parameters = 0)
+            object Ui {
+              static val %stable: Int = LiveLiterals%TestKt.Int%class-Ui()
+            }
+            @Composable
+            @ComposableTarget(applier = "androidx.compose.ui.UiComposable")
+            fun Ui.UiTextField(isError: Boolean, keyboardActions2: Boolean, %composer: Composer?, %changed: Int, %default: Int) {
+              %composer = %composer.startRestartGroup(<>)
+              sourceInformation(%composer, "C(UiTextField)")
+              val %dirty = %changed
+              if (%changed and 0b01110000 === 0) {
+                %dirty = %dirty or if (%default and 0b0001 === 0 && %composer.changed(isError)) 0b00100000 else 0b00010000
+              }
+              if (%changed and 0b001110000000 === 0) {
+                %dirty = %dirty or if (%default and 0b0010 === 0 && %composer.changed(keyboardActions2)) 0b000100000000 else 0b10000000
+              }
+              if (%dirty and 0b001011010001 !== 0b10010000 || !%composer.skipping) {
+                %composer.startDefaults()
+                if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
+                  if (%default and 0b0001 !== 0) {
+                    isError = LiveLiterals%TestKt.Boolean%param-isError%fun-UiTextField()
+                    %dirty = %dirty and 0b01110000.inv()
+                  }
+                  if (%default and 0b0010 !== 0) {
+                    keyboardActions2 = LiveLiterals%TestKt.Boolean%param-keyboardActions2%fun-UiTextField()
+                    %dirty = %dirty and 0b001110000000.inv()
+                  }
+                } else {
+                  %composer.skipToGroupEnd()
+                  if (%default and 0b0001 !== 0) {
+                    %dirty = %dirty and 0b01110000.inv()
+                  }
+                  if (%default and 0b0010 !== 0) {
+                    %dirty = %dirty and 0b001110000000.inv()
+                  }
+                }
+                %composer.endDefaults()
+                if (isTraceInProgress()) {
+                  traceEventStart(<>, %changed, -1, <>)
+                }
+                println("%{LiveLiterals%TestKt.String%0%str%arg-0%call-println%fun-UiTextField()}%isError")
+                println("%{LiveLiterals%TestKt.String%0%str%arg-0%call-println-1%fun-UiTextField()}%keyboardActions2")
+                Column(null, null, null, { %composer: Composer?, %changed: Int ->
+                  Text("%isError", null, <unsafe-coerce>(0L), <unsafe-coerce>(0L), null, null, null, <unsafe-coerce>(0L), null, null, <unsafe-coerce>(0L), <unsafe-coerce>(0), false, 0, 0, null, null, %composer, 0, 0, 0b00011111111111111110)
+                  Text("%keyboardActions2", null, <unsafe-coerce>(0L), <unsafe-coerce>(0L), null, null, null, <unsafe-coerce>(0L), null, null, <unsafe-coerce>(0L), <unsafe-coerce>(0), false, 0, 0, null, null, %composer, 0, 0, 0b00011111111111111110)
+                }, %composer, 0, 0b0111)
+                if (isTraceInProgress()) {
+                  traceEventEnd()
+                }
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                UiTextField(isError, keyboardActions2, %composer, updateChangedFlags(%changed or 0b0001), %default)
+              }
+            }
+            @LiveLiteralFileInfo(file = "/Test.kt")
+            internal object LiveLiterals%TestKt {
+              val Int%class-Ui: Int = 0
+              var State%Int%class-Ui: State<Int>?
+              @LiveLiteralInfo(key = "Int%class-Ui", offset = -1)
+              fun Int%class-Ui(): Int {
+                if (!isLiveLiteralsEnabled) {
+                  return Int%class-Ui
+                }
+                val tmp0 = State%Int%class-Ui
+                return if (tmp0 == null) {
+                  val tmp1 = liveLiteral("Int%class-Ui", Int%class-Ui)
+                  State%Int%class-Ui = tmp1
+                  tmp1
+                } else {
+                  tmp0
+                }
+                .value
+              }
+              val Boolean%param-isError%fun-UiTextField: Boolean = false
+              var State%Boolean%param-isError%fun-UiTextField: State<Boolean>?
+              @LiveLiteralInfo(key = "Boolean%param-isError%fun-UiTextField", offset = 292)
+              fun Boolean%param-isError%fun-UiTextField(): Boolean {
+                if (!isLiveLiteralsEnabled) {
+                  return Boolean%param-isError%fun-UiTextField
+                }
+                val tmp0 = State%Boolean%param-isError%fun-UiTextField
+                return if (tmp0 == null) {
+                  val tmp1 = liveLiteral("Boolean%param-isError%fun-UiTextField", Boolean%param-isError%fun-UiTextField)
+                  State%Boolean%param-isError%fun-UiTextField = tmp1
+                  tmp1
+                } else {
+                  tmp0
+                }
+                .value
+              }
+              val Boolean%param-keyboardActions2%fun-UiTextField: Boolean = false
+              var State%Boolean%param-keyboardActions2%fun-UiTextField: State<Boolean>?
+              @LiveLiteralInfo(key = "Boolean%param-keyboardActions2%fun-UiTextField", offset = 331)
+              fun Boolean%param-keyboardActions2%fun-UiTextField(): Boolean {
+                if (!isLiveLiteralsEnabled) {
+                  return Boolean%param-keyboardActions2%fun-UiTextField
+                }
+                val tmp0 = State%Boolean%param-keyboardActions2%fun-UiTextField
+                return if (tmp0 == null) {
+                  val tmp1 = liveLiteral("Boolean%param-keyboardActions2%fun-UiTextField", Boolean%param-keyboardActions2%fun-UiTextField)
+                  State%Boolean%param-keyboardActions2%fun-UiTextField = tmp1
+                  tmp1
+                } else {
+                  tmp0
+                }
+                .value
+              }
+              val String%0%str%arg-0%call-println%fun-UiTextField: String = "t41 insideFunction "
+              var State%String%0%str%arg-0%call-println%fun-UiTextField: State<String>?
+              @LiveLiteralInfo(key = "String%0%str%arg-0%call-println%fun-UiTextField", offset = 355)
+              fun String%0%str%arg-0%call-println%fun-UiTextField(): String {
+                if (!isLiveLiteralsEnabled) {
+                  return String%0%str%arg-0%call-println%fun-UiTextField
+                }
+                val tmp0 = State%String%0%str%arg-0%call-println%fun-UiTextField
+                return if (tmp0 == null) {
+                  val tmp1 = liveLiteral("String%0%str%arg-0%call-println%fun-UiTextField", String%0%str%arg-0%call-println%fun-UiTextField)
+                  State%String%0%str%arg-0%call-println%fun-UiTextField = tmp1
+                  tmp1
+                } else {
+                  tmp0
+                }
+                .value
+              }
+              val String%0%str%arg-0%call-println-1%fun-UiTextField: String = "t41 insideFunction "
+              var State%String%0%str%arg-0%call-println-1%fun-UiTextField: State<String>?
+              @LiveLiteralInfo(key = "String%0%str%arg-0%call-println-1%fun-UiTextField", offset = 398)
+              fun String%0%str%arg-0%call-println-1%fun-UiTextField(): String {
+                if (!isLiveLiteralsEnabled) {
+                  return String%0%str%arg-0%call-println-1%fun-UiTextField
+                }
+                val tmp0 = State%String%0%str%arg-0%call-println-1%fun-UiTextField
+                return if (tmp0 == null) {
+                  val tmp1 = liveLiteral("String%0%str%arg-0%call-println-1%fun-UiTextField", String%0%str%arg-0%call-println-1%fun-UiTextField)
+                  State%String%0%str%arg-0%call-println-1%fun-UiTextField = tmp1
+                  tmp1
+                } else {
+                  tmp0
+                }
+                .value
+              }
+            }
+        """
+    )
 }
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..0b42755 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() {
@@ -551,4 +576,177 @@
             }
         """
     )
+
+    @Test
+    fun testComposeIrSkippingWithDefaultsRelease() = verifyComposeIrTransform(
+        """
+            import androidx.compose.ui.text.input.TextFieldValue
+            import androidx.compose.runtime.*
+            import androidx.compose.foundation.layout.*
+            import androidx.compose.foundation.text.KeyboardActions
+            import androidx.compose.material.*
+
+            object Ui {}
+
+            @Composable
+            fun Ui.UiTextField(
+                isError: Boolean = false,
+                keyboardActions2: Boolean = false,
+            ) {
+                println("t41 insideFunction ${'$'}isError")
+                println("t41 insideFunction ${'$'}keyboardActions2")
+                Column {
+                    Text("${'$'}isError")
+                    Text("${'$'}keyboardActions2")
+                }
+            }
+        """.trimIndent(),
+        """
+            @StabilityInferred(parameters = 0)
+            object Ui {
+              static val %stable: Int = LiveLiterals%TestKt.Int%class-Ui()
+            }
+            @Composable
+            @ComposableTarget(applier = "androidx.compose.ui.UiComposable")
+            fun Ui.UiTextField(isError: Boolean, keyboardActions2: Boolean, %composer: Composer?, %changed: Int, %default: Int) {
+              %composer = %composer.startRestartGroup(<>)
+              sourceInformation(%composer, "C(UiTextField)")
+              val %dirty = %changed
+              if (%changed and 0b01110000 === 0) {
+                %dirty = %dirty or if (%default and 0b0001 === 0 && %composer.changed(isError)) 0b00100000 else 0b00010000
+              }
+              if (%changed and 0b001110000000 === 0) {
+                %dirty = %dirty or if (%default and 0b0010 === 0 && %composer.changed(keyboardActions2)) 0b000100000000 else 0b10000000
+              }
+              if (%dirty and 0b001011010001 !== 0b10010000 || !%composer.skipping) {
+                %composer.startDefaults()
+                if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
+                  if (%default and 0b0001 !== 0) {
+                    isError = LiveLiterals%TestKt.Boolean%param-isError%fun-UiTextField()
+                    %dirty = %dirty and 0b01110000.inv()
+                  }
+                  if (%default and 0b0010 !== 0) {
+                    keyboardActions2 = LiveLiterals%TestKt.Boolean%param-keyboardActions2%fun-UiTextField()
+                    %dirty = %dirty and 0b001110000000.inv()
+                  }
+                } else {
+                  %composer.skipToGroupEnd()
+                  if (%default and 0b0001 !== 0) {
+                    %dirty = %dirty and 0b01110000.inv()
+                  }
+                  if (%default and 0b0010 !== 0) {
+                    %dirty = %dirty and 0b001110000000.inv()
+                  }
+                }
+                %composer.endDefaults()
+                if (isTraceInProgress()) {
+                  traceEventStart(<>, %changed, -1, <>)
+                }
+                println("%{LiveLiterals%TestKt.String%0%str%arg-0%call-println%fun-UiTextField()}%isError")
+                println("%{LiveLiterals%TestKt.String%0%str%arg-0%call-println-1%fun-UiTextField()}%keyboardActions2")
+                Column(null, null, null, { %composer: Composer?, %changed: Int ->
+                  Text("%isError", null, <unsafe-coerce>(0L), <unsafe-coerce>(0L), null, null, null, <unsafe-coerce>(0L), null, null, <unsafe-coerce>(0L), <unsafe-coerce>(0), false, 0, 0, null, null, %composer, 0, 0, 0b00011111111111111110)
+                  Text("%keyboardActions2", null, <unsafe-coerce>(0L), <unsafe-coerce>(0L), null, null, null, <unsafe-coerce>(0L), null, null, <unsafe-coerce>(0L), <unsafe-coerce>(0), false, 0, 0, null, null, %composer, 0, 0, 0b00011111111111111110)
+                }, %composer, 0, 0b0111)
+                if (isTraceInProgress()) {
+                  traceEventEnd()
+                }
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                UiTextField(isError, keyboardActions2, %composer, updateChangedFlags(%changed or 0b0001), %default)
+              }
+            }
+            @LiveLiteralFileInfo(file = "/Test.kt")
+            internal object LiveLiterals%TestKt {
+              val enabled: Boolean = false
+              val Int%class-Ui: Int = 0
+              var State%Int%class-Ui: State<Int>?
+              @LiveLiteralInfo(key = "Int%class-Ui", offset = -1)
+              fun Int%class-Ui(): Int {
+                if (!enabled) {
+                  return Int%class-Ui
+                }
+                val tmp0 = State%Int%class-Ui
+                return if (tmp0 == null) {
+                  val tmp1 = liveLiteral("Int%class-Ui", Int%class-Ui)
+                  State%Int%class-Ui = tmp1
+                  tmp1
+                } else {
+                  tmp0
+                }
+                .value
+              }
+              val Boolean%param-isError%fun-UiTextField: Boolean = false
+              var State%Boolean%param-isError%fun-UiTextField: State<Boolean>?
+              @LiveLiteralInfo(key = "Boolean%param-isError%fun-UiTextField", offset = 292)
+              fun Boolean%param-isError%fun-UiTextField(): Boolean {
+                if (!enabled) {
+                  return Boolean%param-isError%fun-UiTextField
+                }
+                val tmp0 = State%Boolean%param-isError%fun-UiTextField
+                return if (tmp0 == null) {
+                  val tmp1 = liveLiteral("Boolean%param-isError%fun-UiTextField", Boolean%param-isError%fun-UiTextField)
+                  State%Boolean%param-isError%fun-UiTextField = tmp1
+                  tmp1
+                } else {
+                  tmp0
+                }
+                .value
+              }
+              val Boolean%param-keyboardActions2%fun-UiTextField: Boolean = false
+              var State%Boolean%param-keyboardActions2%fun-UiTextField: State<Boolean>?
+              @LiveLiteralInfo(key = "Boolean%param-keyboardActions2%fun-UiTextField", offset = 331)
+              fun Boolean%param-keyboardActions2%fun-UiTextField(): Boolean {
+                if (!enabled) {
+                  return Boolean%param-keyboardActions2%fun-UiTextField
+                }
+                val tmp0 = State%Boolean%param-keyboardActions2%fun-UiTextField
+                return if (tmp0 == null) {
+                  val tmp1 = liveLiteral("Boolean%param-keyboardActions2%fun-UiTextField", Boolean%param-keyboardActions2%fun-UiTextField)
+                  State%Boolean%param-keyboardActions2%fun-UiTextField = tmp1
+                  tmp1
+                } else {
+                  tmp0
+                }
+                .value
+              }
+              val String%0%str%arg-0%call-println%fun-UiTextField: String = "t41 insideFunction "
+              var State%String%0%str%arg-0%call-println%fun-UiTextField: State<String>?
+              @LiveLiteralInfo(key = "String%0%str%arg-0%call-println%fun-UiTextField", offset = 355)
+              fun String%0%str%arg-0%call-println%fun-UiTextField(): String {
+                if (!enabled) {
+                  return String%0%str%arg-0%call-println%fun-UiTextField
+                }
+                val tmp0 = State%String%0%str%arg-0%call-println%fun-UiTextField
+                return if (tmp0 == null) {
+                  val tmp1 = liveLiteral("String%0%str%arg-0%call-println%fun-UiTextField", String%0%str%arg-0%call-println%fun-UiTextField)
+                  State%String%0%str%arg-0%call-println%fun-UiTextField = tmp1
+                  tmp1
+                } else {
+                  tmp0
+                }
+                .value
+              }
+              val String%0%str%arg-0%call-println-1%fun-UiTextField: String = "t41 insideFunction "
+              var State%String%0%str%arg-0%call-println-1%fun-UiTextField: State<String>?
+              @LiveLiteralInfo(key = "String%0%str%arg-0%call-println-1%fun-UiTextField", offset = 398)
+              fun String%0%str%arg-0%call-println-1%fun-UiTextField(): String {
+                if (!enabled) {
+                  return String%0%str%arg-0%call-println-1%fun-UiTextField
+                }
+                val tmp0 = State%String%0%str%arg-0%call-println-1%fun-UiTextField
+                return if (tmp0 == null) {
+                  val tmp1 = liveLiteral("String%0%str%arg-0%call-println-1%fun-UiTextField", String%0%str%arg-0%call-println-1%fun-UiTextField)
+                  State%String%0%str%arg-0%call-println-1%fun-UiTextField = tmp1
+                  tmp1
+                } else {
+                  tmp0
+                }
+                .value
+              }
+            }
+        """
+    )
 }
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..0b41128 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
@@ -17,11 +17,14 @@
 package androidx.compose.compiler.plugins.kotlin
 
 import org.intellij.lang.annotations.Language
+import org.jetbrains.kotlin.config.CompilerConfiguration
 import org.junit.Test
 
 class RememberIntrinsicTransformTests : AbstractIrTransformTest() {
-    override val intrinsicRememberEnabled: Boolean
-        get() = true
+    override fun CompilerConfiguration.updateConfiguration() {
+        put(ComposeConfiguration.SOURCE_INFORMATION_ENABLED_KEY, true)
+        put(ComposeConfiguration.INTRINSIC_REMEMBER_OPTIMIZATION_ENABLED_KEY, true)
+    }
 
     private fun comparisonPropagation(
         @Language("kotlin")
@@ -1691,4 +1694,64 @@
             fun Text(value: String) { }
         """
     )
+
+    @Test
+    fun testVarargsIntrinsicRemember() = verifyComposeIrTransform(
+        source = """
+            import androidx.compose.runtime.*
+
+            @Composable
+            fun Test(vararg strings: String) {
+                val show = remember { mutableStateOf(false) }
+                if (show.value) {
+                    Text("Showing")
+                }
+            }
+        """,
+        extra = """
+            import androidx.compose.runtime.*
+
+            @Composable
+            fun Text(value: String) { }
+        """,
+        expectedTransformed = """
+            @Composable
+            fun Test(strings: Array<out String>, %composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>)
+              sourceInformation(%composer, "C(Test)<rememb...>,<Text("...>:Test.kt")
+              val %dirty = %changed
+              %composer.startMovableGroup(<>, strings.size)
+              val tmp0_iterator = strings.iterator()
+              while (tmp0_iterator.hasNext()) {
+                val value = tmp0_iterator.next()
+                %dirty = %dirty or if (%composer.changed(value)) 0b0100 else 0
+              }
+              %composer.endMovableGroup()
+              if (%dirty and 0b1110 === 0) {
+                %dirty = %dirty or 0b0010
+              }
+              if (%dirty and 0b0001 !== 0 || !%composer.skipping) {
+                if (isTraceInProgress()) {
+                  traceEventStart(<>, %changed, -1, <>)
+                }
+                val show = remember({
+                  mutableStateOf(
+                    value = false
+                  )
+                }, %composer, 0)
+                if (show.value) {
+                  Text("Showing", %composer, 0b0110)
+                }
+                if (isTraceInProgress()) {
+                  traceEventEnd()
+                }
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                Test(*strings, %composer, updateChangedFlags(%changed or 0b0001))
+              }
+            }
+        """
+    )
 }
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 e368c06..666f74d 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
@@ -26,7 +26,7 @@
 import org.jetbrains.kotlin.compiler.plugin.CliOption
 import org.jetbrains.kotlin.compiler.plugin.CliOptionProcessingException
 import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
-import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
+import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
 import org.jetbrains.kotlin.config.CompilerConfiguration
 import org.jetbrains.kotlin.config.CompilerConfigurationKey
 import org.jetbrains.kotlin.config.JVMConfigurationKeys
@@ -61,6 +61,7 @@
         CompilerConfigurationKey<Boolean>("Generate decoy methods in IR transform")
 }
 
+@OptIn(ExperimentalCompilerApi::class)
 class ComposeCommandLineProcessor : CommandLineProcessor {
     companion object {
         val PLUGIN_ID = "androidx.compose.compiler.plugins.kotlin"
@@ -188,77 +189,98 @@
     }
 }
 
-class ComposeComponentRegistrar : ComponentRegistrar {
+@OptIn(ExperimentalCompilerApi::class)
+class ComposeComponentRegistrar :
+    @Suppress("DEPRECATION") org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar {
     override fun registerProjectComponents(
         project: MockProject,
         configuration: CompilerConfiguration
     ) {
         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.7.21"
-            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) {
@@ -290,10 +312,9 @@
             )
         }
 
-        fun registerIrExtension(
-            project: Project,
+        fun createComposeIrExtension(
             configuration: CompilerConfiguration
-        ) {
+        ): ComposeIrGenerationExtension {
             val liveLiteralsEnabled = configuration.getBoolean(
                 ComposeConfiguration.LIVE_LITERALS_ENABLED_KEY,
             )
@@ -308,7 +329,7 @@
             )
             val intrinsicRememberEnabled = configuration.get(
                 ComposeConfiguration.INTRINSIC_REMEMBER_OPTIMIZATION_ENABLED_KEY,
-                true
+                false
             )
             val decoysEnabled = configuration.getBoolean(
                 ComposeConfiguration.DECOYS_ENABLED_KEY,
@@ -325,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/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index 67f33e3..0b8e35a 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -104,6 +104,7 @@
             9100 to "1.4.0-alpha03",
             9200 to "1.4.0-alpha04",
             9300 to "1.4.0-alpha05",
+            9400 to "1.4.0-alpha06",
         )
 
         /**
@@ -116,7 +117,7 @@
          * The maven version string of this compiler. This string should be updated before/after every
          * release.
          */
-        const val compilerVersion: String = "1.4.0-alpha05"
+        const val compilerVersion: String = "1.4.1"
         private val minimumRuntimeVersion: String
             get() = runtimeVersionToMavenVersionTable[minimumRuntimeVersionInt] ?: "unknown"
     }
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..5ec690d 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
@@ -390,12 +390,12 @@
  *     }
  *
  * Note that this makes use of bitmasks for the $changed and $dirty values. These bitmasks work
- * in a different bit-space than the $default bitmask because two bits are needed to hold the
- * four different possible states of each parameter. Additionally, the lowest bit of the bitmask
+ * in a different bit-space than the $default bitmask because three bits are needed to hold the
+ * six different possible states of each parameter. Additionally, the lowest bit of the bitmask
  * is a special bit which forces execution of the function.
  *
- * This means that for the ith parameter of a composable function, the bit range of i*2 + 1 to
- * i*2 + 2 are used to store the state of the parameter.
+ * This means that for the ith parameter of a composable function, the bit range of i*3 + 1 to
+ * i*3 + 3 are used to store the state of the parameter.
  *
  * The states are outlines by the [ParamState] class.
  *
@@ -860,7 +860,10 @@
 
         val emitTraceMarkers = traceEventMarkersEnabled && !scope.function.isInline
 
-        scope.updateIntrinsiceRememberSafety(!mightUseDefaultGroup(false, scope, defaultParam))
+        scope.updateIntrinsiceRememberSafety(
+            !mightUseDefaultGroup(false, scope, defaultParam) &&
+                !mightUseVarArgsGroup(false, scope)
+        )
 
         transformed = transformed.apply {
             transformChildrenVoid()
@@ -958,14 +961,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
@@ -991,7 +1004,10 @@
 
         val emitTraceMarkers = traceEventMarkersEnabled && !scope.isInlinedLambda
 
-        scope.updateIntrinsiceRememberSafety(!mightUseDefaultGroup(canSkipExecution, scope, null))
+        scope.updateIntrinsiceRememberSafety(
+            !mightUseDefaultGroup(canSkipExecution, scope, null) &&
+                !mightUseVarArgsGroup(canSkipExecution, scope)
+        )
 
         // we must transform the body first, since that will allow us to see whether or not we
         // are using the dispatchReceiverParameter or the extensionReceiverParameter
@@ -1001,7 +1017,6 @@
                 wrapWithTraceEvents(irFunctionSourceKey(), scope)
             }
         }
-
         canSkipExecution = buildPreambleStatementsAndReturnIfSkippingPossible(
             body,
             skipPreamble,
@@ -1073,6 +1088,7 @@
                     *skipPreamble.statements.toTypedArray(),
                     *bodyPreamble.statements.toTypedArray(),
                     transformed,
+                    *bodyEpilogue.statements.toTypedArray(),
                     returnVar?.let { irReturn(declaration.symbol, irGet(it)) }
                 )
             )
@@ -1147,7 +1163,8 @@
         val defaultScope = transformDefaults(scope)
 
         scope.updateIntrinsiceRememberSafety(
-            !mightUseDefaultGroup(true, scope, defaultParam)
+            !mightUseDefaultGroup(true, scope, defaultParam) &&
+                !mightUseVarArgsGroup(true, scope)
         )
 
         // we must transform the body first, since that will allow us to see whether or not we
@@ -1325,6 +1342,13 @@
         return parameters.any { it.defaultValue?.expression?.isStatic() == false }
     }
 
+    // Like mightUseDefaultGroup(), this is an intentionally conservative value that must be true
+    // when ever a varargs group could be generated but can be true when it is not.
+    private fun mightUseVarArgsGroup(
+        isSkippableDeclaration: Boolean,
+        scope: Scope.FunctionScope
+    ) = isSkippableDeclaration && scope.allTrackedParams.any { it.isVararg }
+
     private fun buildPreambleStatementsAndReturnIfSkippingPossible(
         sourceElement: IrElement,
         skipPreamble: IrStatementContainer,
@@ -1460,7 +1484,7 @@
                     val defaultValueIsStatic = defaultExprIsStatic[slotIndex]
                     val callChanged = irChanged(irGet(param))
                     val isChanged = if (defaultParam != null && !defaultValueIsStatic)
-                        irAndAnd(irIsProvided(defaultParam, slotIndex), callChanged)
+                        irAndAnd(irIsProvided(defaultParam, defaultIndex), callChanged)
                     else
                         callChanged
                     val modifyDirtyFromChangedResult = dirty.irOrSetBitsAtSlot(
@@ -1525,6 +1549,7 @@
                     irGet(param),
                     param.type.classOrNull!!.getPropertyGetter("size")!!.owner
                 )
+
                 // TODO(lmr): verify this works with default vararg expressions!
                 skipPreamble.statements.add(
                     irStartMovableGroup(
diff --git a/compose/compiler/compiler/integration-tests/src/test/kotlin/androidx/compose/compiler/test/CompilerPluginRuntimeVersionCheckTest.kt b/compose/compiler/compiler/integration-tests/src/test/kotlin/androidx/compose/compiler/test/CompilerPluginRuntimeVersionCheckTest.kt
index c3191b3..fd0bf61 100644
--- a/compose/compiler/compiler/integration-tests/src/test/kotlin/androidx/compose/compiler/test/CompilerPluginRuntimeVersionCheckTest.kt
+++ b/compose/compiler/compiler/integration-tests/src/test/kotlin/androidx/compose/compiler/test/CompilerPluginRuntimeVersionCheckTest.kt
@@ -190,6 +190,14 @@
             dependencies {
                 $dependenciesBlock
             }
+
+            tasks.withType(
+                org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+            ).configureEach {
+                kotlinOptions {
+                    jvmTarget = "1.8"
+                }
+            }
             """.trimIndent()
         )
     }
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-newtext/src/androidAndroidTest/kotlin/androidx/compose/foundation/newtext/text/modifiers/MultiParagraphLayoutCacheTest.kt b/compose/foundation/foundation-newtext/src/androidAndroidTest/kotlin/androidx/compose/foundation/newtext/text/modifiers/MultiParagraphLayoutCacheTest.kt
index f7cdf7b..471b37e 100644
--- a/compose/foundation/foundation-newtext/src/androidAndroidTest/kotlin/androidx/compose/foundation/newtext/text/modifiers/MultiParagraphLayoutCacheTest.kt
+++ b/compose/foundation/foundation-newtext/src/androidAndroidTest/kotlin/androidx/compose/foundation/newtext/text/modifiers/MultiParagraphLayoutCacheTest.kt
@@ -52,13 +52,12 @@
             val spanStyle = SpanStyle(fontSize = fontSize, fontFamily = fontFamily)
             val annotatedString = AnnotatedString(text, spanStyle)
             val textDelegate = MultiParagraphLayoutCache(
-                StaticTextLayoutDrawParams(
-                    text = annotatedString,
-                    style = TextStyle.Default,
-                    fontFamilyResolver = fontFamilyResolver,
-                ),
-                density = this
-            )
+                text = annotatedString,
+                style = TextStyle.Default,
+                fontFamilyResolver = fontFamilyResolver,
+            ).also {
+                it.density = this
+            }
 
             textDelegate.layoutWithConstraints(Constraints.fixed(0, 0), LayoutDirection.Ltr)
 
@@ -75,13 +74,12 @@
             val spanStyle = SpanStyle(fontSize = fontSize, fontFamily = fontFamily)
             val annotatedString = AnnotatedString(text, spanStyle)
             val textDelegate = MultiParagraphLayoutCache(
-                StaticTextLayoutDrawParams(
-                    text = annotatedString,
-                    style = TextStyle.Default,
-                    fontFamilyResolver = fontFamilyResolver,
-                ),
-                density = this
-            )
+                text = annotatedString,
+                style = TextStyle.Default,
+                fontFamilyResolver = fontFamilyResolver,
+            ).also {
+                it.density = this
+            }
 
             textDelegate.layoutWithConstraints(Constraints.fixed(0, 0), LayoutDirection.Ltr)
 
@@ -93,13 +91,12 @@
     @Test
     fun TextLayoutInput_reLayout_withDifferentHeight() {
         val textDelegate = MultiParagraphLayoutCache(
-            StaticTextLayoutDrawParams(
-                text = AnnotatedString(text = "Hello World!"),
-                style = TextStyle.Default,
-                fontFamilyResolver = fontFamilyResolver,
-            ),
-            density = density
-        )
+            text = AnnotatedString("Hello World"),
+            style = TextStyle.Default,
+            fontFamilyResolver = fontFamilyResolver,
+        ).also {
+            it.density = density
+        }
         val width = 200
         val heightFirstLayout = 100
         val heightSecondLayout = 200
@@ -121,13 +118,12 @@
     @Test
     fun TextLayoutResult_reLayout_withDifferentHeight() {
         val textDelegate = MultiParagraphLayoutCache(
-            StaticTextLayoutDrawParams(
-                text = AnnotatedString(text = "Hello World!"),
-                style = TextStyle.Default,
-                fontFamilyResolver = fontFamilyResolver,
-            ),
-            density = density
-        )
+            text = AnnotatedString("Hello World"),
+            style = TextStyle.Default,
+            fontFamilyResolver = fontFamilyResolver,
+        ).also {
+            it.density = density
+        }
         val width = 200
         val heightFirstLayout = 100
         val heightSecondLayout = 200
@@ -151,16 +147,14 @@
         val fontSize = 20f
         val text = AnnotatedString(text = "Hello World! Hello World! Hello World! Hello World!")
         val textDelegate = MultiParagraphLayoutCache(
-            StaticTextLayoutDrawParams(
-                text = text,
-                style = TextStyle(fontSize = fontSize.sp),
-                fontFamilyResolver = fontFamilyResolver,
-                softWrap = false,
-                overflow = TextOverflow.Ellipsis,
-            ),
-
-            density = density
-        )
+            text = text,
+            style = TextStyle(fontSize = fontSize.sp),
+            fontFamilyResolver = fontFamilyResolver,
+            softWrap = false,
+            overflow = TextOverflow.Ellipsis,
+        ).also {
+            it.density = density
+        }
 
         textDelegate.layoutWithConstraints(Constraints.fixed(0, 0), LayoutDirection.Ltr)
         // Makes width smaller than needed.
@@ -177,15 +171,15 @@
     fun TextLayoutResult_layoutWithLimitedHeight_withEllipsis() {
         val fontSize = 20f
         val text = AnnotatedString(text = "Hello World! Hello World! Hello World! Hello World!")
+
         val textDelegate = MultiParagraphLayoutCache(
-            StaticTextLayoutDrawParams(
-                text = text,
-                style = TextStyle(fontSize = fontSize.sp),
-                fontFamilyResolver = fontFamilyResolver,
-                overflow = TextOverflow.Ellipsis,
-            ),
-            density = density
-        )
+            text = text,
+            style = TextStyle(fontSize = fontSize.sp),
+            fontFamilyResolver = fontFamilyResolver,
+            overflow = TextOverflow.Ellipsis,
+        ).also {
+            it.density = density
+        }
         textDelegate.layoutWithConstraints(Constraints.fixed(0, 0), LayoutDirection.Ltr)
         val constraints = Constraints(
             maxWidth = textDelegate.maxIntrinsicWidth / 4,
@@ -202,15 +196,16 @@
     fun TextLayoutResult_sameWidth_inRtlAndLtr_withLetterSpacing() {
         val fontSize = 20f
         val text = AnnotatedString(text = "Hello World")
+
         val textDelegate = MultiParagraphLayoutCache(
-            StaticTextLayoutDrawParams(
-                text = text,
-                style = TextStyle(fontSize = fontSize.sp, letterSpacing = 0.5.sp),
-                fontFamilyResolver = fontFamilyResolver,
-                overflow = TextOverflow.Ellipsis,
-            ),
-            density = density
-        )
+            text = text,
+            style = TextStyle(fontSize = fontSize.sp, letterSpacing = 0.5.sp),
+            fontFamilyResolver = fontFamilyResolver,
+            overflow = TextOverflow.Ellipsis,
+        ).also {
+            it.density = density
+        }
+
         textDelegate.layoutWithConstraints(Constraints(), LayoutDirection.Ltr)
         val layoutResultLtr = textDelegate.layout
         textDelegate.layoutWithConstraints(Constraints(), LayoutDirection.Rtl)
diff --git a/compose/foundation/foundation-newtext/src/androidAndroidTest/kotlin/androidx/compose/foundation/newtext/text/modifiers/MultiParagraphLayoutCacheWidthWithLetterSpacingTest.kt b/compose/foundation/foundation-newtext/src/androidAndroidTest/kotlin/androidx/compose/foundation/newtext/text/modifiers/MultiParagraphLayoutCacheWidthWithLetterSpacingTest.kt
index e858dd4..a2195df 100644
--- a/compose/foundation/foundation-newtext/src/androidAndroidTest/kotlin/androidx/compose/foundation/newtext/text/modifiers/MultiParagraphLayoutCacheWidthWithLetterSpacingTest.kt
+++ b/compose/foundation/foundation-newtext/src/androidAndroidTest/kotlin/androidx/compose/foundation/newtext/text/modifiers/MultiParagraphLayoutCacheWidthWithLetterSpacingTest.kt
@@ -73,18 +73,17 @@
 
     private fun assertLineCount(style: TextStyle) {
         val textDelegate = MultiParagraphLayoutCache(
-            StaticTextLayoutDrawParams(
-                text = AnnotatedString(text = "This is a callout message"),
-                style = style.copy(
-                    fontFamily = fontFamily,
-                    fontSize = fontSize
-                ),
-                fontFamilyResolver = fontFamilyResolver,
-                softWrap = true,
-                overflow = TextOverflow.Clip
+            text = AnnotatedString(text = "This is a callout message"),
+            style = style.copy(
+                fontFamily = fontFamily,
+                fontSize = fontSize
             ),
-            density = density,
-        )
+            fontFamilyResolver = fontFamilyResolver,
+            softWrap = true,
+            overflow = TextOverflow.Clip
+        ).also {
+            it.density = density
+        }
         textDelegate.layoutWithConstraints(Constraints(), LayoutDirection.Ltr)
         val layoutResult = textDelegate.layout
         assertThat(layoutResult.lineCount).isEqualTo(1)
diff --git a/compose/foundation/foundation-newtext/src/androidAndroidTest/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextLayoutResultIntegrationTest.kt b/compose/foundation/foundation-newtext/src/androidAndroidTest/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextLayoutResultIntegrationTest.kt
index d0822aa..4b8b7c1 100644
--- a/compose/foundation/foundation-newtext/src/androidAndroidTest/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextLayoutResultIntegrationTest.kt
+++ b/compose/foundation/foundation-newtext/src/androidAndroidTest/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextLayoutResultIntegrationTest.kt
@@ -56,13 +56,12 @@
             val spanStyle = SpanStyle(fontSize = fontSize, fontFamily = fontFamily)
             val annotatedString = AnnotatedString(text, spanStyle)
             val textDelegate = MultiParagraphLayoutCache(
-                StaticTextLayoutDrawParams(
-                    text = annotatedString,
-                    style = TextStyle.Default,
-                    fontFamilyResolver = fontFamilyResolver
-                ),
-                density = this,
-            )
+                text = annotatedString,
+                style = TextStyle.Default,
+                fontFamilyResolver = fontFamilyResolver
+            ).also {
+                it.density = this
+            }
 
             textDelegate.layoutWithConstraints(Constraints(0, 200), layoutDirection)
             val layoutResult = textDelegate.layout
@@ -80,13 +79,12 @@
         val spanStyle = SpanStyle(fontSize = 20.sp, fontFamily = fontFamily)
         val annotatedString = AnnotatedString(text, spanStyle)
         val textDelegate = MultiParagraphLayoutCache(
-            StaticTextLayoutDrawParams(
-                text = annotatedString,
-                style = TextStyle.Default,
-                fontFamilyResolver = fontFamilyResolver
-            ),
-            density = density,
-        )
+            text = annotatedString,
+            style = TextStyle.Default,
+            fontFamilyResolver = fontFamilyResolver
+        ).also {
+            it.density = density
+        }
 
         textDelegate.layoutWithConstraints(Constraints(maxWidth = width), layoutDirection)
         val layoutResult = textDelegate.layout
@@ -102,13 +100,12 @@
             val text = "hello"
             val annotatedString = AnnotatedString(text, spanStyle)
             val textDelegate = MultiParagraphLayoutCache(
-                StaticTextLayoutDrawParams(
-                    text = annotatedString,
-                    style = TextStyle.Default,
-                    fontFamilyResolver = fontFamilyResolver
-                ),
-                density = this,
-            )
+                text = annotatedString,
+                style = TextStyle.Default,
+                fontFamilyResolver = fontFamilyResolver
+            ).also {
+                it.density = this
+            }
 
             textDelegate.layoutWithConstraints(Constraints(), layoutDirection)
             val layoutResult = textDelegate.layout
@@ -120,13 +117,12 @@
     @Test
     fun layout_build_layoutResult() {
         val textDelegate = MultiParagraphLayoutCache(
-            StaticTextLayoutDrawParams(
-                text = AnnotatedString("Hello"),
-                style = TextStyle.Default,
-                fontFamilyResolver = fontFamilyResolver
-            ),
-            density = density,
-        )
+            text = AnnotatedString("hello"),
+            style = TextStyle.Default,
+            fontFamilyResolver = fontFamilyResolver
+        ).also {
+            it.density = density
+        }
 
         textDelegate.layoutWithConstraints(Constraints(0, 20), layoutDirection)
         val layoutResult = textDelegate.layout
@@ -147,13 +143,12 @@
         )
 
         val textDelegate = MultiParagraphLayoutCache(
-            StaticTextLayoutDrawParams(
-                text = annotatedString,
-                style = TextStyle.Default,
-                fontFamilyResolver = fontFamilyResolver
-            ),
-            density = density,
-        )
+            text = annotatedString,
+            style = TextStyle.Default,
+            fontFamilyResolver = fontFamilyResolver
+        ).also {
+            it.density = density
+        }
         textDelegate.layoutWithConstraints(Constraints(), layoutDirection)
         val layoutResult = textDelegate.layout
 
@@ -175,13 +170,12 @@
             )
 
             val textDelegate = MultiParagraphLayoutCache(
-                StaticTextLayoutDrawParams(
-                    text = annotatedString,
-                    style = TextStyle.Default,
-                    fontFamilyResolver = fontFamilyResolver
-                ),
-                density = this,
-            )
+                text = annotatedString,
+                style = TextStyle.Default,
+                fontFamilyResolver = fontFamilyResolver
+            ).also {
+                it.density = this
+            }
             textDelegate.layoutWithConstraints(Constraints(), layoutDirection)
             val layoutResult = textDelegate.layout
 
@@ -199,13 +193,12 @@
         val spanStyle = SpanStyle(fontSize = 20.sp, fontFamily = fontFamily)
         val annotatedString = AnnotatedString(text, spanStyle)
         val textDelegate = MultiParagraphLayoutCache(
-            StaticTextLayoutDrawParams(
-                text = annotatedString,
-                style = TextStyle.Default,
-                fontFamilyResolver = fontFamilyResolver
-            ),
-            density = density,
-        )
+            text = annotatedString,
+            style = TextStyle.Default,
+            fontFamilyResolver = fontFamilyResolver
+        ).also {
+            it.density = density
+        }
 
         textDelegate.layoutWithConstraints(Constraints(), layoutDirection)
         val layoutResult = textDelegate.layout
@@ -224,14 +217,13 @@
         val maxLines = 3
 
         val textDelegate = MultiParagraphLayoutCache(
-            StaticTextLayoutDrawParams(
-                text = annotatedString,
-                style = TextStyle.Default,
-                fontFamilyResolver = fontFamilyResolver,
-                maxLines = maxLines
-            ),
-            density = density,
-        )
+            text = annotatedString,
+            style = TextStyle.Default,
+            fontFamilyResolver = fontFamilyResolver,
+            maxLines = maxLines
+        ).also {
+            it.density = density
+        }
 
         textDelegate.layoutWithConstraints(Constraints.fixed(0, 0), LayoutDirection.Ltr)
         // Tries to make 5 lines of text, which exceeds the given maxLines(3).
@@ -253,14 +245,13 @@
         val maxLines = 10
 
         val textDelegate = MultiParagraphLayoutCache(
-            StaticTextLayoutDrawParams(
-                text = annotatedString,
-                style = TextStyle.Default,
-                fontFamilyResolver = fontFamilyResolver,
-                maxLines = maxLines
-            ),
-            density = density,
-        )
+            text = annotatedString,
+            style = TextStyle.Default,
+            fontFamilyResolver = fontFamilyResolver,
+            maxLines = maxLines
+        ).also {
+            it.density = density
+        }
 
         textDelegate.layoutWithConstraints(Constraints.fixed(0, 0), LayoutDirection.Ltr)
         // Tries to make 5 lines of text, which doesn't exceed the given maxLines(10).
@@ -281,13 +272,12 @@
         val annotatedString = AnnotatedString(text, spanStyle)
 
         val textDelegate = MultiParagraphLayoutCache(
-            StaticTextLayoutDrawParams(
-                text = annotatedString,
-                style = TextStyle.Default,
-                fontFamilyResolver = fontFamilyResolver,
-            ),
-            density = density,
-        )
+            text = annotatedString,
+            style = TextStyle.Default,
+            fontFamilyResolver = fontFamilyResolver,
+        ).also {
+            it.density = density
+        }
 
         textDelegate.layoutWithConstraints(
             Constraints(),
@@ -314,13 +304,12 @@
         val annotatedString = AnnotatedString(text, spanStyle)
 
         val textDelegate = MultiParagraphLayoutCache(
-            StaticTextLayoutDrawParams(
-                text = annotatedString,
-                style = TextStyle.Default,
-                fontFamilyResolver = fontFamilyResolver
-            ),
-            density = density,
-        )
+            text = annotatedString,
+            style = TextStyle.Default,
+            fontFamilyResolver = fontFamilyResolver,
+        ).also {
+            it.density = density
+        }
 
         textDelegate.layoutWithConstraints(
             Constraints(),
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/TextUsingModifier.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/TextUsingModifier.kt
index 213a7e9..ca96202 100644
--- a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/TextUsingModifier.kt
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/TextUsingModifier.kt
@@ -18,9 +18,11 @@
 
 import androidx.compose.foundation.newtext.text.copypasta.selection.LocalSelectionRegistrar
 import androidx.compose.foundation.newtext.text.copypasta.selection.LocalTextSelectionColors
-import androidx.compose.foundation.newtext.text.modifiers.StaticTextSelectionModifierController
-import androidx.compose.foundation.newtext.text.modifiers.StaticTextLayoutDrawParams
-import androidx.compose.foundation.newtext.text.modifiers.StaticTextModifier
+import androidx.compose.foundation.newtext.text.modifiers.SelectableTextAnnotatedStringElement
+import androidx.compose.foundation.newtext.text.modifiers.TextAnnotatedStringElement
+import androidx.compose.foundation.newtext.text.modifiers.SelectionController
+import androidx.compose.foundation.newtext.text.modifiers.TextStringSimpleElement
+import androidx.compose.foundation.newtext.text.modifiers.validateMinMaxLines
 import androidx.compose.foundation.text.InlineTextContent
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
@@ -34,9 +36,7 @@
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.platform.LocalFontFamilyResolver
-import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.Placeholder
@@ -53,24 +53,25 @@
 /**
  * Rewrite of BasicText
  */
+@OptIn(ExperimentalComposeUiApi::class)
 @ExperimentalTextApi
 @Composable
 fun TextUsingModifier(
     text: String,
     modifier: Modifier = Modifier,
     style: TextStyle = TextStyle.Default,
-    onTextLayout: (TextLayoutResult) -> Unit = {},
+    onTextLayout: ((TextLayoutResult) -> Unit)? = null,
     overflow: TextOverflow = TextOverflow.Clip,
     softWrap: Boolean = true,
     maxLines: Int = Int.MAX_VALUE,
     minLines: Int = 1,
 ) {
-
+    validateMinMaxLines(minLines, maxLines)
     val selectionRegistrar = LocalSelectionRegistrar.current
     val selectionController = if (selectionRegistrar != null) {
         val backgroundSelectionColor = LocalTextSelectionColors.current.backgroundColor
         remember(selectionRegistrar, backgroundSelectionColor) {
-            StaticTextSelectionModifierController(
+            SelectionController(
                 selectionRegistrar,
                 backgroundSelectionColor
             )
@@ -78,8 +79,8 @@
     } else {
         null
     }
-    Layout(
-        modifier = modifier.textModifier(
+    val finalModifier = if (selectionController != null || onTextLayout != null) {
+        modifier.textModifier(
             AnnotatedString(text),
             style = style,
             onTextLayout = onTextLayout,
@@ -91,9 +92,19 @@
             placeholders = null,
             onPlaceholderLayout = null,
             selectionController = selectionController
-        ),
-        EmptyMeasurePolicy
-    )
+        )
+    } else {
+        modifier then TextStringSimpleElement(
+            text,
+            style,
+            LocalFontFamilyResolver.current,
+            overflow,
+            softWrap,
+            maxLines,
+            minLines
+        )
+    }
+    Layout(finalModifier, EmptyMeasurePolicy)
 }
 
 /**
@@ -112,11 +123,12 @@
     minLines: Int = 1,
     inlineContent: Map<String, InlineTextContent>? = null,
 ) {
+    validateMinMaxLines(minLines, maxLines)
     val selectionRegistrar = LocalSelectionRegistrar.current
     val selectionController = if (selectionRegistrar != null) {
         val backgroundSelectionColor = LocalTextSelectionColors.current.backgroundColor
         remember(selectionRegistrar, backgroundSelectionColor) {
-            StaticTextSelectionModifierController(
+            SelectionController(
                 selectionRegistrar,
                 backgroundSelectionColor
             )
@@ -224,30 +236,37 @@
     fontFamilyResolver: FontFamily.Resolver,
     placeholders: List<AnnotatedString.Range<Placeholder>>?,
     onPlaceholderLayout: ((List<Rect?>) -> Unit)?,
-    selectionController: StaticTextSelectionModifierController?
+    selectionController: SelectionController?
 ): Modifier {
-    val params = StaticTextLayoutDrawParams(
-        text,
-        style,
-        fontFamilyResolver,
-        onTextLayout,
-        overflow,
-        softWrap,
-        maxLines,
-        minLines,
-        placeholders,
-        onPlaceholderLayout,
-        selectionController
-    )
-    val staticTextModifier = object : ModifierNodeElement<StaticTextModifier>(
-        params,
-        false,
-        debugInspectorInfo {}
-    ) {
-        override fun create(): StaticTextModifier = StaticTextModifier(params)
-        override fun update(node: StaticTextModifier): StaticTextModifier =
-            node.also { it.params = params }
+    if (selectionController == null) {
+        val staticTextModifier = TextAnnotatedStringElement(
+            text,
+            style,
+            fontFamilyResolver,
+            onTextLayout,
+            overflow,
+            softWrap,
+            maxLines,
+            minLines,
+            placeholders,
+            onPlaceholderLayout,
+            null
+        )
+        return this then Modifier /* selection position */ then staticTextModifier
+    } else {
+        val selectableTextModifier = SelectableTextAnnotatedStringElement(
+            text,
+            style,
+            fontFamilyResolver,
+            onTextLayout,
+            overflow,
+            softWrap,
+            maxLines,
+            minLines,
+            placeholders,
+            onPlaceholderLayout,
+            selectionController
+        )
+        return this then selectionController.modifier then selectableTextModifier
     }
-    val selectionModifier = selectionController?.modifier ?: Modifier
-    return this then selectionModifier then staticTextModifier
 }
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/MinMaxLinesCoercer.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/MinMaxLinesCoercer.kt
index 8d114fc..c616ac9 100644
--- a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/MinMaxLinesCoercer.kt
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/MinMaxLinesCoercer.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 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.
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/MultiParagraphLayoutCache.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/MultiParagraphLayoutCache.kt
index f44de77..fa494d1 100644
--- a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/MultiParagraphLayoutCache.kt
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/MultiParagraphLayoutCache.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 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.
@@ -16,12 +16,16 @@
 
 package androidx.compose.foundation.newtext.text.modifiers
 
+import androidx.compose.foundation.newtext.text.DefaultMinLines
 import androidx.compose.foundation.newtext.text.ceilToIntPx
-import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.MultiParagraph
 import androidx.compose.ui.text.MultiParagraphIntrinsics
+import androidx.compose.ui.text.Placeholder
 import androidx.compose.ui.text.TextLayoutInput
 import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.resolveDefaults
 import androidx.compose.ui.text.style.LineBreak
 import androidx.compose.ui.text.style.TextOverflow
@@ -32,18 +36,38 @@
 import androidx.compose.ui.unit.constrain
 
 internal class MultiParagraphLayoutCache(
-    private val params: StaticTextLayoutDrawParams,
-    private val density: Density
+    private var text: AnnotatedString,
+    private var style: TextStyle,
+    private var fontFamilyResolver: FontFamily.Resolver,
+    private var overflow: TextOverflow = TextOverflow.Clip,
+    private var softWrap: Boolean = true,
+    private var maxLines: Int = Int.MAX_VALUE,
+    private var minLines: Int = DefaultMinLines,
+    private var placeholders: List<AnnotatedString.Range<Placeholder>>? = null,
 ) {
     private var minMaxLinesCoercer: MinMaxLinesCoercer? = null
+    internal var density: Density? = null
+        set(value) {
+            val localField = field
+            if (value == null || localField == null) {
+                field = value
+                return
+            }
+
+            if (localField.density != value.density || localField.fontScale != value.fontScale) {
+                field = value
+                // none of our results are correct if density changed
+                markDirty()
+            }
+        }
 
     /*@VisibleForTesting*/
     // NOTE(text-perf-review): it seems like TextDelegate essentially guarantees that we use
     // MultiParagraph. Can we have a fast-path that uses just Paragraph in simpler cases (ie,
     // String)?
-    internal var paragraphIntrinsics: MultiParagraphIntrinsics? = null
+    private var paragraphIntrinsics: MultiParagraphIntrinsics? = null
 
-    internal var intrinsicsLayoutDirection: LayoutDirection? = null
+    private var intrinsicsLayoutDirection: LayoutDirection? = null
 
     private var layoutCache: TextLayoutResult? = null
     private var cachedIntrinsicHeight: Pair<Int, Int>? = null
@@ -75,6 +99,120 @@
         get() = layoutCache
 
     /**
+     * Update layout constraints for this text
+     *
+     * @return true if constraints caused a text layout invalidation
+     */
+    fun layoutWithConstraints(
+        constraints: Constraints,
+        layoutDirection: LayoutDirection
+    ): Boolean {
+        if (!layoutCache.newLayoutWillBeDifferent(constraints, layoutDirection)) {
+            return false
+        }
+        val finalConstraints = if (maxLines != Int.MAX_VALUE || minLines > 1) {
+            val localMinMax = MinMaxLinesCoercer.from(
+                minMaxLinesCoercer,
+                layoutDirection,
+                style,
+                density!!,
+                fontFamilyResolver
+            ).also {
+                minMaxLinesCoercer = it
+            }
+            localMinMax.coerceMaxMinLines(
+                inConstraints = constraints,
+                minLines = minLines,
+                maxLines = maxLines
+            )
+        } else {
+            constraints
+        }
+        val multiParagraph = layoutText(finalConstraints, layoutDirection)
+
+        val size = finalConstraints.constrain(
+            IntSize(
+                multiParagraph.width.ceilToIntPx(),
+                multiParagraph.height.ceilToIntPx()
+            )
+        )
+
+        layoutCache = TextLayoutResult(
+            TextLayoutInput(
+                text,
+                style,
+                placeholders.orEmpty(),
+                maxLines,
+                softWrap,
+                overflow,
+                density!!,
+                layoutDirection,
+                fontFamilyResolver,
+                finalConstraints
+            ),
+            multiParagraph,
+            size
+        )
+        return true
+    }
+
+    fun intrinsicHeightAt(width: Int, layoutDirection: LayoutDirection): Int {
+        cachedIntrinsicHeight?.let { (prevWidth, prevHeight) ->
+            if (width == prevWidth) return prevHeight
+        }
+        val result = layoutText(
+            Constraints(0, width, 0, Constraints.Infinity),
+            layoutDirection
+        ).height.ceilToIntPx()
+
+        cachedIntrinsicHeight = width to result
+        return result
+    }
+
+    fun update(
+        text: AnnotatedString,
+        style: TextStyle,
+        fontFamilyResolver: FontFamily.Resolver,
+        overflow: TextOverflow,
+        softWrap: Boolean,
+        maxLines: Int,
+        minLines: Int,
+        placeholders: List<AnnotatedString.Range<Placeholder>>?
+    ) {
+        this.text = text
+        this.style = style
+        this.fontFamilyResolver = fontFamilyResolver
+        this.overflow = overflow
+        this.softWrap = softWrap
+        this.maxLines = maxLines
+        this.minLines = minLines
+        this.placeholders = placeholders
+        markDirty()
+    }
+
+    private fun setLayoutDirection(layoutDirection: LayoutDirection) {
+        val localIntrinsics = paragraphIntrinsics
+        val intrinsics = if (
+            localIntrinsics == null ||
+            layoutDirection != intrinsicsLayoutDirection ||
+            localIntrinsics.hasStaleResolvedFonts
+        ) {
+            intrinsicsLayoutDirection = layoutDirection
+            MultiParagraphIntrinsics(
+                annotatedString = text,
+                style = resolveDefaults(style, layoutDirection),
+                density = density!!,
+                fontFamilyResolver = fontFamilyResolver,
+                placeholders = placeholders.orEmpty()
+            )
+        } else {
+            localIntrinsics
+        }
+
+        paragraphIntrinsics = intrinsics
+    }
+
+    /**
      * Computes the visual position of the glyphs for painting the text.
      *
      * The text will layout with a width that's as close to its max intrinsic width as possible
@@ -87,7 +225,7 @@
         setLayoutDirection(layoutDirection)
 
         val minWidth = constraints.minWidth
-        val widthMatters = params.softWrap || params.overflow == TextOverflow.Ellipsis
+        val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
         val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
             constraints.maxWidth
         } else {
@@ -108,8 +246,8 @@
         //     AA…
         // Here we assume there won't be any '\n' character when softWrap is false. And make
         // maxLines 1 to implement the similar behavior.
-        val overwriteMaxLines = !params.softWrap && params.overflow == TextOverflow.Ellipsis
-        val finalMaxLines = if (overwriteMaxLines) 1 else params.maxLines.coerceAtLeast(1)
+        val overwriteMaxLines = !softWrap && overflow == TextOverflow.Ellipsis
+        val finalMaxLines = if (overwriteMaxLines) 1 else maxLines.coerceAtLeast(1)
 
         // if minWidth == maxWidth the width is fixed.
         //    therefore we can pass that value to our paragraph and use it
@@ -131,92 +269,11 @@
             constraints = Constraints(maxWidth = width, maxHeight = constraints.maxHeight),
             // This is a fallback behavior for ellipsis. Native
             maxLines = finalMaxLines,
-            ellipsis = params.overflow == TextOverflow.Ellipsis
+            ellipsis = overflow == TextOverflow.Ellipsis
         )
     }
 
-    private fun setLayoutDirection(layoutDirection: LayoutDirection) {
-        val localIntrinsics = paragraphIntrinsics
-        val intrinsics = if (
-            localIntrinsics == null ||
-            layoutDirection != intrinsicsLayoutDirection ||
-            localIntrinsics.hasStaleResolvedFonts
-        ) {
-            intrinsicsLayoutDirection = layoutDirection
-            MultiParagraphIntrinsics(
-                annotatedString = params.text,
-                style = resolveDefaults(params.style, layoutDirection),
-                density = density,
-                fontFamilyResolver = params.fontFamilyResolver,
-                placeholders = params.placeholders.orEmpty()
-            )
-        } else {
-            localIntrinsics
-        }
-
-        paragraphIntrinsics = intrinsics
-    }
-
-    /**
-     * Update layout constraints for this text
-     *
-     * @return true if constraints caused a text layout invalidation
-     */
-    fun layoutWithConstraints(
-        constraints: Constraints,
-        layoutDirection: LayoutDirection
-    ): Boolean {
-        if (!layoutCache.newConstraintsProduceNewLayout(constraints, layoutDirection)) {
-            return false
-        }
-        val finalConstraints = if (params.maxLines != Int.MAX_VALUE || params.minLines > 1) {
-            val localMinMax = MinMaxLinesCoercer.from(
-                minMaxLinesCoercer,
-                layoutDirection,
-                params.style,
-                density,
-                params.fontFamilyResolver
-            ).also {
-                minMaxLinesCoercer = it
-            }
-            localMinMax.coerceMaxMinLines(
-                inConstraints = constraints,
-                minLines = params.minLines,
-                maxLines = params.maxLines
-            )
-        } else {
-            constraints
-        }
-        val multiParagraph = layoutText(finalConstraints, layoutDirection)
-
-        val size = finalConstraints.constrain(
-            IntSize(
-                multiParagraph.width.ceilToIntPx(),
-                multiParagraph.height.ceilToIntPx()
-            )
-        )
-
-        layoutCache = TextLayoutResult(
-            TextLayoutInput(
-                params.text,
-                params.style,
-                params.placeholders.orEmpty(),
-                params.maxLines,
-                params.softWrap,
-                params.overflow,
-                density,
-                layoutDirection,
-                params.fontFamilyResolver,
-                finalConstraints
-            ),
-            multiParagraph,
-            size
-        )
-        return true
-    }
-
-    @OptIn(ExperimentalTextApi::class)
-    private fun TextLayoutResult?.newConstraintsProduceNewLayout(
+    private fun TextLayoutResult?.newLayoutWillBeDifferent(
         constraints: Constraints,
         layoutDirection: LayoutDirection
     ): Boolean {
@@ -234,17 +291,17 @@
 
         // only be clever if we can predict line break behavior exactly, which is only possible with
         // simple geometry math for the greedy layout case
-        if (params.style.lineBreak != LineBreak.Simple) {
+        if (style.lineBreak != LineBreak.Simple) {
             return true
         }
 
         // see if width would produce the same wraps (greedy wraps only)
-        val canWrap = params.softWrap && params.maxLines > 1
+        val canWrap = softWrap && maxLines > 1
         if (canWrap && size.width != multiParagraph.maxIntrinsicWidth.ceilToIntPx()) {
             // some soft wrapping happened, check to see if we're between the previous measure and
             // the next wrap
-            val prevActualMaxWidth = params.paraMaxWidthFor(layoutInput.constraints)
-            val newMaxWidth = params.paraMaxWidthFor(constraints)
+            val prevActualMaxWidth = maxWidth(layoutInput.constraints)
+            val newMaxWidth = maxWidth(constraints)
             if (newMaxWidth > prevActualMaxWidth) {
                 // we've grown the potential layout area, and may break longer lines
                 return true
@@ -273,7 +330,7 @@
         return false
     }
 
-    private fun StaticTextLayoutDrawParams.paraMaxWidthFor(constraints: Constraints): Int {
+    private fun maxWidth(constraints: Constraints): Int {
         val minWidth = constraints.minWidth
         val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
         val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
@@ -288,20 +345,8 @@
         }
     }
 
-    fun intrinsicHeightAt(width: Int, layoutDirection: LayoutDirection): Int {
-        cachedIntrinsicHeight?.let { (prevWidth, prevHeight) ->
-            if (width == prevWidth) return prevHeight
-        }
-        val result = layoutText(
-            Constraints(0, width, 0, Constraints.Infinity),
-            layoutDirection
-        ).height.ceilToIntPx()
-
-        cachedIntrinsicHeight = width to result
-        return result
-    }
-
-    fun diff(value: StaticTextLayoutDrawParams): StaticTextLayoutDrawParamsDiff {
-        return params.diff(value)
+    private fun markDirty() {
+        paragraphIntrinsics = null
+        layoutCache = null
     }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/ParagraphLayoutCache.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/ParagraphLayoutCache.kt
new file mode 100644
index 0000000..45330d0
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/ParagraphLayoutCache.kt
@@ -0,0 +1,389 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.newtext.text.modifiers
+
+import androidx.compose.foundation.newtext.text.DefaultMinLines
+import androidx.compose.foundation.newtext.text.ceilToIntPx
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.MultiParagraph
+import androidx.compose.ui.text.MultiParagraphIntrinsics
+import androidx.compose.ui.text.Paragraph
+import androidx.compose.ui.text.ParagraphIntrinsics
+import androidx.compose.ui.text.TextLayoutInput
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.resolveDefaults
+import androidx.compose.ui.text.style.LineBreak
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrain
+
+internal class ParagraphLayoutCache(
+    private var text: String,
+    private var style: TextStyle,
+    private var fontFamilyResolver: FontFamily.Resolver,
+    private var overflow: TextOverflow = TextOverflow.Clip,
+    private var softWrap: Boolean = true,
+    private var maxLines: Int = Int.MAX_VALUE,
+    private var minLines: Int = DefaultMinLines,
+) {
+    internal var density: Density? = null
+        set(value) {
+            val localField = field
+            if (localField == null) {
+                field = value
+                return
+            }
+
+            if (value == null) {
+                field = value
+                markDirty()
+                return
+            }
+
+            if (localField.density != value.density || localField.fontScale != value.fontScale) {
+                field = value
+                // none of our results are correct if density changed
+                markDirty()
+            }
+        }
+    internal val observeFontChanges: Unit
+        get() {
+            paragraphIntrinsics?.hasStaleResolvedFonts
+        }
+
+    internal var paragraph: Paragraph? = null
+    internal var didOverflow: Boolean = false
+    internal var layoutSize: IntSize = IntSize(0, 0)
+
+    private var minMaxLinesCoercer: MinMaxLinesCoercer? = null
+    private var paragraphIntrinsics: ParagraphIntrinsics? = null
+
+    private var intrinsicsLayoutDirection: LayoutDirection? = null
+    private var prevConstraints: Constraints = Constraints.fixed(0, 0)
+
+    private var cachedIntrinsicWidth: Int = -1
+    private var cachedIntrinsicHeight: Int = -1
+
+    private val nonNullIntrinsics: ParagraphIntrinsics
+        get() = paragraphIntrinsics ?: throw IllegalStateException(
+            "MeasureScope.measure() must be called first to query text intrinsics"
+        )
+
+    /**
+     * The width for text if all soft wrap opportunities were taken.
+     *
+     * Valid only after [layoutWithConstraints] has been called.
+     */
+    val minIntrinsicWidth: Int get() = nonNullIntrinsics.minIntrinsicWidth.ceilToIntPx()
+
+    /**
+     * The width at which increasing the width of the text no lonfger decreases the height.
+     *
+     * Valid only after [layoutWithConstraints] has been called.
+     */
+    val maxIntrinsicWidth: Int get() = nonNullIntrinsics.maxIntrinsicWidth.ceilToIntPx()
+
+    /**
+     * Update layout constraints for this text
+     *
+     * @return true if constraints caused a text layout invalidation
+     */
+    fun layoutWithConstraints(
+        constraints: Constraints,
+        layoutDirection: LayoutDirection
+    ): Boolean {
+        val finalConstraints = if (maxLines != Int.MAX_VALUE || minLines > 1) {
+            val localMinMax = MinMaxLinesCoercer.from(
+                minMaxLinesCoercer,
+                layoutDirection,
+                style,
+                density!!,
+                fontFamilyResolver
+            ).also {
+                minMaxLinesCoercer = it
+            }
+            localMinMax.coerceMaxMinLines(
+                inConstraints = constraints,
+                minLines = minLines,
+                maxLines = maxLines
+            )
+        } else {
+            constraints
+        }
+        if (!newLayoutWillBeDifferent(finalConstraints, layoutDirection)) {
+            return false
+        }
+        paragraph = layoutText(finalConstraints, layoutDirection).also {
+            prevConstraints = finalConstraints
+            val localSize = finalConstraints.constrain(
+                IntSize(
+                    it.width.ceilToIntPx(),
+                    it.height.ceilToIntPx()
+                )
+            )
+            layoutSize = localSize
+            didOverflow = overflow != TextOverflow.Visible &&
+                (localSize.width < it.width || localSize.height < it.height)
+        }
+        return true
+    }
+
+    fun intrinsicHeightAt(width: Int, layoutDirection: LayoutDirection): Int {
+        val localWidth = cachedIntrinsicWidth
+        val localHeght = cachedIntrinsicHeight
+        if (width == localWidth && localWidth != -1) return localHeght
+        val result = layoutText(
+            Constraints(0, width, 0, Constraints.Infinity),
+            layoutDirection
+        ).height.ceilToIntPx()
+
+        cachedIntrinsicWidth = width
+        cachedIntrinsicHeight = result
+        return result
+    }
+
+    fun update(
+        text: String,
+        style: TextStyle,
+        fontFamilyResolver: FontFamily.Resolver,
+        overflow: TextOverflow,
+        softWrap: Boolean,
+        maxLines: Int,
+        minLines: Int
+    ) {
+        this.text = text
+        this.style = style
+        this.fontFamilyResolver = fontFamilyResolver
+        this.overflow = overflow
+        this.softWrap = softWrap
+        this.maxLines = maxLines
+        this.minLines = minLines
+        markDirty()
+    }
+
+    private fun setLayoutDirection(layoutDirection: LayoutDirection) {
+        val localIntrinsics = paragraphIntrinsics
+        val intrinsics = if (
+            localIntrinsics == null ||
+            layoutDirection != intrinsicsLayoutDirection ||
+            localIntrinsics.hasStaleResolvedFonts
+        ) {
+            intrinsicsLayoutDirection = layoutDirection
+            ParagraphIntrinsics(
+                text = text,
+                style = resolveDefaults(style, layoutDirection),
+                density = density!!,
+                fontFamilyResolver = fontFamilyResolver
+            )
+        } else {
+            localIntrinsics
+        }
+        paragraphIntrinsics = intrinsics
+    }
+
+    /**
+     * Computes the visual position of the glyphs for painting the text.
+     *
+     * The text will layout with a width that's as close to its max intrinsic width as possible
+     * while still being greater than or equal to `minWidth` and less than or equal to `maxWidth`.
+     */
+    private fun layoutText(
+        constraints: Constraints,
+        layoutDirection: LayoutDirection
+    ): Paragraph {
+        setLayoutDirection(layoutDirection)
+
+        val minWidth = constraints.minWidth
+        val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
+        val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
+            constraints.maxWidth
+        } else {
+            Constraints.Infinity
+        }
+
+        // This is a fallback behavior because native text layout doesn't support multiple
+        // ellipsis in one text layout.
+        // When softWrap is turned off and overflow is ellipsis, it's expected that each line
+        // that exceeds maxWidth will be ellipsized.
+        // For example,
+        // input text:
+        //     "AAAA\nAAAA"
+        // maxWidth:
+        //     3 * fontSize that only allow 3 characters to be displayed each line.
+        // expected output:
+        //     AA…
+        //     AA…
+        // Here we assume there won't be any '\n' character when softWrap is false. And make
+        // maxLines 1 to implement the similar behavior.
+        val overwriteMaxLines = !softWrap && overflow == TextOverflow.Ellipsis
+        val finalMaxLines = if (overwriteMaxLines) 1 else maxLines.coerceAtLeast(1)
+
+        // if minWidth == maxWidth the width is fixed.
+        //    therefore we can pass that value to our paragraph and use it
+        // if minWidth != maxWidth there is a range
+        //    then we should check if the max intrinsic width is in this range to decide the
+        //    width to be passed to Paragraph
+        //        if max intrinsic width is between minWidth and maxWidth
+        //           we can use it to layout
+        //        else if max intrinsic width is greater than maxWidth, we can only use maxWidth
+        //        else if max intrinsic width is less than minWidth, we should use minWidth
+        val width = if (minWidth == maxWidth) {
+            maxWidth
+        } else {
+            maxIntrinsicWidth.coerceIn(minWidth, maxWidth)
+        }
+
+        val finalConstraints = Constraints(maxWidth = width, maxHeight = constraints.maxHeight)
+        return Paragraph(
+            paragraphIntrinsics = nonNullIntrinsics,
+            constraints = finalConstraints,
+            // This is a fallback behavior for ellipsis. Native
+            maxLines = finalMaxLines,
+            ellipsis = overflow == TextOverflow.Ellipsis
+        )
+    }
+
+    private fun newLayoutWillBeDifferent(
+        constraints: Constraints,
+        layoutDirection: LayoutDirection
+    ): Boolean {
+        val localParagraph = paragraph ?: return true
+        val localParagraphIntrinsics = paragraphIntrinsics ?: return true
+        // no layout yet
+
+        // async typeface changes
+        if (localParagraphIntrinsics.hasStaleResolvedFonts) return true
+
+        // layout direction changed
+        if (layoutDirection != intrinsicsLayoutDirection) return true
+
+        // if we were passed identical constraints just skip more work
+        if (constraints == prevConstraints) return false
+
+        // only be clever if we can predict line break behavior exactly, which is only possible with
+        // simple geometry math for the greedy layout case
+        if (style.lineBreak != LineBreak.Simple) {
+            return true
+        }
+
+        // see if width would produce the same wraps (greedy wraps only)
+        val canWrap = softWrap && maxLines > 1
+        if (canWrap && layoutSize.width != localParagraph.maxIntrinsicWidth.ceilToIntPx()) {
+            // some soft wrapping happened, check to see if we're between the previous measure and
+            // the next wrap
+            val prevActualMaxWidth = maxWidth(prevConstraints)
+            val newMaxWidth = maxWidth(constraints)
+            if (newMaxWidth > prevActualMaxWidth) {
+                // we've grown the potential layout area, and may break longer lines
+                return true
+            }
+            if (newMaxWidth <= layoutSize.width) {
+                // it's possible to shrink this text (possible opt: check minIntrinsicWidth
+                return true
+            }
+        }
+
+        // check any constraint width changes for single line text
+        if (!canWrap &&
+            (constraints.maxWidth != prevConstraints.maxWidth ||
+                (constraints.minWidth != prevConstraints.minWidth))) {
+            // no soft wrap and width is different, always invalidate
+            return true
+        }
+
+        // if we get here width won't change, height may be clipped
+        if (constraints.maxHeight < localParagraph.height) {
+            // vertical clip changes
+            return true
+        }
+
+        // breaks can't change, height can't change
+        return false
+    }
+
+    private fun maxWidth(constraints: Constraints): Int {
+        val minWidth = constraints.minWidth
+        val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
+        val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
+            constraints.maxWidth
+        } else {
+            Constraints.Infinity
+        }
+        return if (minWidth == maxWidth) {
+            maxWidth
+        } else {
+            maxIntrinsicWidth.coerceIn(minWidth, maxWidth)
+        }
+    }
+
+    private fun markDirty() {
+        paragraph = null
+        paragraphIntrinsics = null
+        intrinsicsLayoutDirection = null
+        cachedIntrinsicWidth = -1
+        cachedIntrinsicHeight = -1
+        prevConstraints = Constraints.fixed(0, 0)
+        layoutSize = IntSize(0, 0)
+        didOverflow = false
+    }
+
+    /**
+     * This does an entire Text layout to produce the result, it is slow
+     */
+    fun slowCreateTextLayoutResultOrNull(): TextLayoutResult? {
+        // make sure we're in a valid place
+        val localLayoutDirection = intrinsicsLayoutDirection ?: return null
+        val localDensity = density ?: return null
+        val annotatedString = AnnotatedString(text)
+        paragraph ?: return null
+        paragraphIntrinsics ?: return null
+
+        // and redo layout with MultiParagraph
+        return TextLayoutResult(
+            TextLayoutInput(
+                annotatedString,
+                style,
+                emptyList(),
+                maxLines,
+                softWrap,
+                overflow,
+                localDensity,
+                localLayoutDirection,
+                fontFamilyResolver,
+                prevConstraints
+            ),
+            MultiParagraph(
+                MultiParagraphIntrinsics(
+                    annotatedString = annotatedString,
+                    style = style,
+                    placeholders = emptyList(),
+                    density = localDensity,
+                    fontFamilyResolver = fontFamilyResolver
+                ),
+                prevConstraints,
+                maxLines,
+                overflow == TextOverflow.Ellipsis
+            ),
+            layoutSize
+        )
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/SelectableTextAnnotatedStringElement.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/SelectableTextAnnotatedStringElement.kt
new file mode 100644
index 0000000..1514df6
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/SelectableTextAnnotatedStringElement.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.newtext.text.modifiers
+
+import androidx.compose.foundation.newtext.text.DefaultMinLines
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.Placeholder
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.style.TextOverflow
+
+@ExperimentalComposeUiApi
+internal class SelectableTextAnnotatedStringElement(
+    private val text: AnnotatedString,
+    private val style: TextStyle,
+    private val fontFamilyResolver: FontFamily.Resolver,
+    private val onTextLayout: ((TextLayoutResult) -> Unit)? = null,
+    private val overflow: TextOverflow = TextOverflow.Clip,
+    private val softWrap: Boolean = true,
+    private val maxLines: Int = Int.MAX_VALUE,
+    private val minLines: Int = DefaultMinLines,
+    private val placeholders: List<AnnotatedString.Range<Placeholder>>? = null,
+    private val onPlaceholderLayout: ((List<Rect?>) -> Unit)? = null,
+    private val selectionController: SelectionController? = null
+) : ModifierNodeElement<SelectableTextAnnotatedStringNode>(inspectorInfo = debugInspectorInfo { }) {
+    override fun create(): SelectableTextAnnotatedStringNode = SelectableTextAnnotatedStringNode(
+        text,
+        style,
+        fontFamilyResolver,
+        onTextLayout,
+        overflow,
+        softWrap,
+        maxLines,
+        minLines,
+        placeholders,
+        onPlaceholderLayout,
+        selectionController
+    )
+
+    override fun update(
+        node: SelectableTextAnnotatedStringNode
+    ): SelectableTextAnnotatedStringNode {
+        node.update(
+            text = text,
+            style = style,
+            placeholders = placeholders,
+            minLines = minLines,
+            maxLines = maxLines,
+            softWrap = softWrap,
+            fontFamilyResolver = fontFamilyResolver,
+            overflow = overflow,
+            onTextLayout = onTextLayout,
+            onPlaceholderLayout = onPlaceholderLayout,
+            selectionController = selectionController
+        )
+        return node
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+
+        if (other !is SelectableTextAnnotatedStringElement) return false
+
+        // these three are most likely to actually change
+        if (text != other.text) return false
+        if (style != other.style) return false
+        if (placeholders != other.placeholders) return false
+
+        // these are equally unlikely to change
+        if (fontFamilyResolver != other.fontFamilyResolver) return false
+        if (onTextLayout != other.onTextLayout) return false
+        if (overflow != other.overflow) return false
+        if (softWrap != other.softWrap) return false
+        if (maxLines != other.maxLines) return false
+        if (minLines != other.minLines) return false
+
+        // these never change, but check anyway for correctness
+        if (onPlaceholderLayout != other.onPlaceholderLayout) return false
+        if (selectionController != other.selectionController) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = super.hashCode()
+        result = 31 * result + text.hashCode()
+        result = 31 * result + style.hashCode()
+        result = 31 * result + fontFamilyResolver.hashCode()
+        result = 31 * result + (onTextLayout?.hashCode() ?: 0)
+        result = 31 * result + overflow.hashCode()
+        result = 31 * result + softWrap.hashCode()
+        result = 31 * result + maxLines
+        result = 31 * result + minLines
+        result = 31 * result + (placeholders?.hashCode() ?: 0)
+        result = 31 * result + (onPlaceholderLayout?.hashCode() ?: 0)
+        result = 31 * result + (selectionController?.hashCode() ?: 0)
+        return result
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/SelectableTextAnnotatedStringNode.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/SelectableTextAnnotatedStringNode.kt
new file mode 100644
index 0000000..c369c40
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/SelectableTextAnnotatedStringNode.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.newtext.text.modifiers
+
+import androidx.compose.foundation.newtext.text.DefaultMinLines
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.layout.IntrinsicMeasurable
+import androidx.compose.ui.layout.IntrinsicMeasureScope
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.GlobalPositionAwareModifierNode
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.SemanticsModifierNode
+import androidx.compose.ui.node.invalidateMeasurements
+import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.Placeholder
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Constraints
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal class SelectableTextAnnotatedStringNode(
+    text: AnnotatedString,
+    style: TextStyle,
+    fontFamilyResolver: FontFamily.Resolver,
+    onTextLayout: ((TextLayoutResult) -> Unit)? = null,
+    overflow: TextOverflow = TextOverflow.Clip,
+    softWrap: Boolean = true,
+    maxLines: Int = Int.MAX_VALUE,
+    minLines: Int = DefaultMinLines,
+    placeholders: List<AnnotatedString.Range<Placeholder>>? = null,
+    onPlaceholderLayout: ((List<Rect?>) -> Unit)? = null,
+    private val selectionController: SelectionController? = null
+) : DelegatingNode(), LayoutModifierNode, DrawModifierNode, GlobalPositionAwareModifierNode,
+    SemanticsModifierNode {
+
+    private val delegate = delegated {
+        TextAnnotatedStringNode(
+            text = text,
+            style = style,
+            fontFamilyResolver = fontFamilyResolver,
+            onTextLayout = onTextLayout,
+            overflow = overflow,
+            softWrap = softWrap,
+            maxLines = maxLines,
+            minLines = minLines,
+            placeholders = placeholders,
+            onPlaceholderLayout = onPlaceholderLayout,
+            selectionController = selectionController
+        )
+    }
+
+    init {
+        requireNotNull(selectionController) {
+            "Do not use SelectionCapableStaticTextModifier unless selectionController != null"
+        }
+    }
+
+    override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
+        selectionController?.updateGlobalPosition(coordinates)
+    }
+
+    override fun ContentDrawScope.draw() = delegate.drawNonExtension(this)
+
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult = delegate.measureNonExtension(this, measurable, constraints)
+
+    override val semanticsConfiguration: SemanticsConfiguration
+        get() = delegate.semanticsConfiguration
+
+    override fun IntrinsicMeasureScope.minIntrinsicWidth(
+        measurable: IntrinsicMeasurable,
+        height: Int
+    ): Int = delegate.minIntrinsicWidthNonExtension(this, measurable, height)
+
+    override fun IntrinsicMeasureScope.minIntrinsicHeight(
+        measurable: IntrinsicMeasurable,
+        width: Int
+    ): Int = delegate.minIntrinsicHeightNonExtension(this, measurable, width)
+
+    override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+        measurable: IntrinsicMeasurable,
+        height: Int
+    ): Int = delegate.maxIntrinsicWidthNonExtension(this, measurable, height)
+
+    override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+        measurable: IntrinsicMeasurable,
+        width: Int
+    ): Int = delegate.maxIntrinsicHeightNonExtension(this, measurable, width)
+
+    fun update(
+        text: AnnotatedString,
+        style: TextStyle,
+        placeholders: List<AnnotatedString.Range<Placeholder>>?,
+        minLines: Int,
+        maxLines: Int,
+        softWrap: Boolean,
+        fontFamilyResolver: FontFamily.Resolver,
+        overflow: TextOverflow,
+        onTextLayout: ((TextLayoutResult) -> Unit)?,
+        onPlaceholderLayout: ((List<Rect?>) -> Unit)?,
+        selectionController: SelectionController?
+    ) {
+        delegate.doInvalidations(
+            textChanged = delegate.updateText(
+                text = text
+            ),
+            layoutChanged = delegate.updateLayoutRelatedArgs(
+                style = style,
+                placeholders = placeholders,
+                minLines = minLines,
+                maxLines = maxLines,
+                softWrap = softWrap,
+                fontFamilyResolver = fontFamilyResolver,
+                overflow = overflow
+            ),
+            callbacksChanged = delegate.updateCallbacks(
+                onTextLayout = onTextLayout,
+                onPlaceholderLayout = onPlaceholderLayout,
+                selectionController = selectionController
+            )
+        )
+        // we always relayout when we're selectable
+        invalidateMeasurements()
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/SelectionController.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/SelectionController.kt
new file mode 100644
index 0000000..ed21488
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/SelectionController.kt
@@ -0,0 +1,327 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.newtext.text.modifiers
+
+import androidx.compose.foundation.newtext.text.copypasta.TextDragObserver
+import androidx.compose.foundation.newtext.text.copypasta.detectDragGesturesAfterLongPressWithObserver
+import androidx.compose.foundation.newtext.text.copypasta.selection.MouseSelectionObserver
+import androidx.compose.foundation.newtext.text.copypasta.selection.MultiWidgetSelectionDelegate
+import androidx.compose.foundation.newtext.text.copypasta.selection.Selectable
+import androidx.compose.foundation.newtext.text.copypasta.selection.SelectionAdjustment
+import androidx.compose.foundation.newtext.text.copypasta.selection.SelectionRegistrar
+import androidx.compose.foundation.newtext.text.copypasta.selection.hasSelection
+import androidx.compose.foundation.newtext.text.copypasta.selection.mouseSelectionDetector
+import androidx.compose.foundation.newtext.text.copypasta.textPointerIcon
+import androidx.compose.runtime.RememberObserver
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.input.pointer.pointerHoverIcon
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.TextLayoutResult
+
+internal data class StaticTextSelectionParams(
+    val layoutCoordinates: LayoutCoordinates?,
+    val textLayoutResult: TextLayoutResult?
+) {
+    companion object {
+        val Empty = StaticTextSelectionParams(null, null)
+    }
+}
+
+// This is _basically_ a Modifier.Node but moved into remember because we need to do pointerInput
+// TODO: Refactor when Modifier.pointerInput is available for delegation
+internal class SelectionController(
+    private val selectionRegistrar: SelectionRegistrar,
+    private val backgroundSelectionColor: Color
+) : RememberObserver {
+    private var selectable: Selectable? = null
+    private val selectableId = selectionRegistrar.nextSelectableId()
+    // TODO: Move these into Modifer.element eventually
+    private var params: StaticTextSelectionParams = StaticTextSelectionParams.Empty
+
+    val modifier: Modifier = selectionRegistrar.makeSelectionModifier(
+        selectableId = selectableId,
+        layoutCoordinates = { params.layoutCoordinates },
+        textLayoutResult = { params.textLayoutResult },
+        // TODO: Use real isInTouchMode on merge
+        isInTouchMode = true /* fake it to android hardcode */
+    )
+
+    override fun onRemembered() {
+        selectable = selectionRegistrar.subscribe(
+            MultiWidgetSelectionDelegate(
+                selectableId = selectableId,
+                coordinatesCallback = { params.layoutCoordinates },
+                layoutResultCallback = { params.textLayoutResult }
+            )
+        )
+    }
+
+    override fun onForgotten() {
+        val localSelectable = selectable
+        if (localSelectable != null) {
+            selectionRegistrar.unsubscribe(localSelectable)
+            selectable = null
+        }
+    }
+
+    override fun onAbandoned() {
+        val localSelectable = selectable
+        if (localSelectable != null) {
+            selectionRegistrar.unsubscribe(localSelectable)
+            selectable = null
+        }
+    }
+
+    fun updateTextLayout(textLayoutResult: TextLayoutResult) {
+        params = params.copy(textLayoutResult = textLayoutResult)
+    }
+
+    fun updateGlobalPosition(coordinates: LayoutCoordinates) {
+        params = params.copy(layoutCoordinates = coordinates)
+    }
+
+    fun draw(contentDrawScope: ContentDrawScope) {
+        val layoutResult = params.textLayoutResult ?: return
+        val selection = selectionRegistrar.subselections[selectableId]
+
+        if (selection != null) {
+            val start = if (!selection.handlesCrossed) {
+                selection.start.offset
+            } else {
+                selection.end.offset
+            }
+            val end = if (!selection.handlesCrossed) {
+                selection.end.offset
+            } else {
+                selection.start.offset
+            }
+
+            if (start != end) {
+                val selectionPath = layoutResult.multiParagraph.getPathForRange(start, end)
+                with(contentDrawScope) {
+                    drawPath(selectionPath, backgroundSelectionColor)
+                }
+            }
+        }
+    }
+}
+
+// this is not chained, but is a standalone factory
+@Suppress("ModifierFactoryExtensionFunction")
+private fun SelectionRegistrar.makeSelectionModifier(
+    selectableId: Long,
+    layoutCoordinates: () -> LayoutCoordinates?,
+    textLayoutResult: () -> TextLayoutResult?,
+    isInTouchMode: Boolean
+): Modifier {
+    return if (isInTouchMode) {
+        val longPressDragObserver = object : TextDragObserver {
+            /**
+             * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
+             * recalculated.
+             */
+            var lastPosition = Offset.Zero
+
+            /**
+             * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
+             * it will be zeroed out.
+             */
+            var dragTotalDistance = Offset.Zero
+
+            override fun onDown(point: Offset) {
+                // Not supported for long-press-drag.
+            }
+
+            override fun onUp() {
+                // Nothing to do.
+            }
+
+            override fun onStart(startPoint: Offset) {
+                layoutCoordinates()?.let {
+                    if (!it.isAttached) return
+
+                    if (textLayoutResult().outOfBoundary(startPoint, startPoint)) {
+                        notifySelectionUpdateSelectAll(
+                            selectableId = selectableId
+                        )
+                    } else {
+                        notifySelectionUpdateStart(
+                            layoutCoordinates = it,
+                            startPosition = startPoint,
+                            adjustment = SelectionAdjustment.Word
+                        )
+                    }
+
+                    lastPosition = startPoint
+                }
+                // selection never started
+                if (!hasSelection(selectableId)) return
+                // Zero out the total distance that being dragged.
+                dragTotalDistance = Offset.Zero
+            }
+
+            override fun onDrag(delta: Offset) {
+                layoutCoordinates()?.let {
+                    if (!it.isAttached) return
+                    // selection never started, did not consume any drag
+                    if (!hasSelection(selectableId)) return
+
+                    dragTotalDistance += delta
+                    val newPosition = lastPosition + dragTotalDistance
+
+                    if (!textLayoutResult().outOfBoundary(lastPosition, newPosition)) {
+                        // Notice that only the end position needs to be updated here.
+                        // Start position is left unchanged. This is typically important when
+                        // long-press is using SelectionAdjustment.WORD or
+                        // SelectionAdjustment.PARAGRAPH that updates the start handle position from
+                        // the dragBeginPosition.
+                        val consumed = notifySelectionUpdate(
+                            layoutCoordinates = it,
+                            previousPosition = lastPosition,
+                            newPosition = newPosition,
+                            isStartHandle = false,
+                            adjustment = SelectionAdjustment.CharacterWithWordAccelerate
+                        )
+                        if (consumed) {
+                            lastPosition = newPosition
+                            dragTotalDistance = Offset.Zero
+                        }
+                    }
+                }
+            }
+
+            override fun onStop() {
+                if (hasSelection(selectableId)) {
+                    notifySelectionUpdateEnd()
+                }
+            }
+
+            override fun onCancel() {
+                if (hasSelection(selectableId)) {
+                    notifySelectionUpdateEnd()
+                }
+            }
+        }
+        Modifier.pointerInput(longPressDragObserver) {
+            detectDragGesturesAfterLongPressWithObserver(
+                longPressDragObserver
+            )
+        }
+    } else {
+        val mouseSelectionObserver = object : MouseSelectionObserver {
+            var lastPosition = Offset.Zero
+
+            override fun onExtend(downPosition: Offset): Boolean {
+                layoutCoordinates()?.let { layoutCoordinates ->
+                    if (!layoutCoordinates.isAttached) return false
+                    val consumed = notifySelectionUpdate(
+                        layoutCoordinates = layoutCoordinates,
+                        newPosition = downPosition,
+                        previousPosition = lastPosition,
+                        isStartHandle = false,
+                        adjustment = SelectionAdjustment.None
+                    )
+                    if (consumed) {
+                        lastPosition = downPosition
+                    }
+                    return hasSelection(selectableId)
+                }
+                return false
+            }
+
+            override fun onExtendDrag(dragPosition: Offset): Boolean {
+                layoutCoordinates()?.let { layoutCoordinates ->
+                    if (!layoutCoordinates.isAttached) return false
+                    if (!hasSelection(selectableId)) return false
+
+                    val consumed = notifySelectionUpdate(
+                        layoutCoordinates = layoutCoordinates,
+                        newPosition = dragPosition,
+                        previousPosition = lastPosition,
+                        isStartHandle = false,
+                        adjustment = SelectionAdjustment.None
+                    )
+
+                    if (consumed) {
+                        lastPosition = dragPosition
+                    }
+                }
+                return true
+            }
+
+            override fun onStart(
+                downPosition: Offset,
+                adjustment: SelectionAdjustment
+            ): Boolean {
+                layoutCoordinates()?.let {
+                    if (!it.isAttached) return false
+
+                    notifySelectionUpdateStart(
+                        layoutCoordinates = it,
+                        startPosition = downPosition,
+                        adjustment = adjustment
+                    )
+
+                    lastPosition = downPosition
+                    return hasSelection(selectableId)
+                }
+
+                return false
+            }
+
+            override fun onDrag(
+                dragPosition: Offset,
+                adjustment: SelectionAdjustment
+            ): Boolean {
+                layoutCoordinates()?.let {
+                    if (!it.isAttached) return false
+                    if (!hasSelection(selectableId)) return false
+
+                    val consumed = notifySelectionUpdate(
+                        layoutCoordinates = it,
+                        previousPosition = lastPosition,
+                        newPosition = dragPosition,
+                        isStartHandle = false,
+                        adjustment = adjustment
+                    )
+                    if (consumed) {
+                        lastPosition = dragPosition
+                    }
+                }
+                return true
+            }
+        }
+        Modifier.pointerInput(mouseSelectionObserver) {
+            mouseSelectionDetector(mouseSelectionObserver)
+        }.pointerHoverIcon(textPointerIcon)
+    }
+}
+
+private fun TextLayoutResult?.outOfBoundary(start: Offset, end: Offset): Boolean {
+    this ?: return false
+
+    val lastOffset = layoutInput.text.text.length
+    val rawStartOffset = getOffsetForPosition(start)
+    val rawEndOffset = getOffsetForPosition(end)
+
+    return rawStartOffset >= lastOffset - 1 && rawEndOffset >= lastOffset - 1 ||
+        rawStartOffset < 0 && rawEndOffset < 0
+}
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/StaticTextLayoutDrawParams.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/StaticTextLayoutDrawParams.kt
deleted file mode 100644
index 5315354..0000000
--- a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/StaticTextLayoutDrawParams.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.newtext.text.modifiers
-
-import androidx.compose.foundation.newtext.text.DefaultMinLines
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.Placeholder
-import androidx.compose.ui.text.TextLayoutResult
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.style.TextOverflow
-import kotlin.experimental.and
-import kotlin.experimental.or
-
-// TODO(seanmcq): break this into (text, style) and (rest...) objects to avoid high-invalidation cost
-// TODO(seanmcq): Explore this holding non-AnnotatedString (future perf opt)
-internal data class StaticTextLayoutDrawParams constructor(
-    val text: AnnotatedString,
-    val style: TextStyle,
-    val fontFamilyResolver: FontFamily.Resolver,
-    val onTextLayout: ((TextLayoutResult) -> Unit)? = null,
-    val overflow: TextOverflow = TextOverflow.Clip,
-    val softWrap: Boolean = true,
-    val maxLines: Int = Int.MAX_VALUE,
-    val minLines: Int = DefaultMinLines,
-    val placeholders: List<AnnotatedString.Range<Placeholder>>? = null,
-    val onPlaceholderLayout: ((List<Rect?>) -> Unit)? = null,
-    val selectionController: StaticTextSelectionModifierController? = null
-) {
-    init {
-        validateMinMaxLines(minLines, maxLines)
-    }
-}
-
-internal fun StaticTextLayoutDrawParams.diff(
-    other: StaticTextLayoutDrawParams
-): StaticTextLayoutDrawParamsDiff {
-    return packCompares(
-        text = text != other.text,
-        style = style != other.style,
-        placeholders = placeholders != other.placeholders,
-        layoutParams = !(minLines == other.minLines && maxLines == other.maxLines &&
-            softWrap == other.softWrap &&
-            fontFamilyResolver == other.fontFamilyResolver &&
-            overflow == other.overflow),
-        onTextLayout = onTextLayout != other.onTextLayout,
-        onPlaceholderLayout = onPlaceholderLayout != other.onPlaceholderLayout,
-        selectionController = selectionController != other.selectionController
-    )
-}
-
-@JvmInline
-internal value class StaticTextLayoutDrawParamsDiff(internal val value: Short)
-
-internal const val TextFlag: Short = 0b0000000000000001
-private const val StyleFlag: Short = 0b0000000000000010
-private const val PlaceholderFlag: Short = 0b0000000000000100
-private const val LayoutFlag: Short = 0b0000000000001000
-private const val CallbackFlag: Short = 0b0000000000010000
-private const val SelectionFlag: Short = 0b0000000000100000
-
-private val AllLayoutAffectingFlags =
-    TextFlag or StyleFlag or PlaceholderFlag or LayoutFlag
-
-private val AllCallbackFlags =
-    CallbackFlag or SelectionFlag
-
-internal val StaticTextLayoutDrawParamsDiff.anyDiffs
-    get() = value != 0.toShort()
-
-internal val StaticTextLayoutDrawParamsDiff.hasLayoutDiffs: Boolean
-    get() = (value and AllLayoutAffectingFlags).toInt() != 0
-
-internal val StaticTextLayoutDrawParamsDiff.hasSemanticsDiffs: Boolean
-    get() = (value and TextFlag).toInt() != 0
-
-internal val StaticTextLayoutDrawParamsDiff.hasCallbackDiffs: Boolean
-    get() = (value and AllCallbackFlags).toInt() != 0
-
-private fun packCompares(
-    text: Boolean,
-    style: Boolean,
-    placeholders: Boolean,
-    layoutParams: Boolean,
-    onTextLayout: Boolean,
-    onPlaceholderLayout: Boolean,
-    selectionController: Boolean
-): StaticTextLayoutDrawParamsDiff {
-    return StaticTextLayoutDrawParamsDiff(
-        text.adds(TextFlag) or
-        style.adds(StyleFlag) or
-        placeholders.adds(PlaceholderFlag) or
-        layoutParams.adds(LayoutFlag) or
-        (onTextLayout || onPlaceholderLayout).adds(CallbackFlag) or
-        selectionController.adds(SelectionFlag)
-    )
-}
-
-private fun Boolean.adds(result: Short): Short {
-    return if (this) result else 0
-}
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/StaticTextModifier.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/StaticTextModifier.kt
deleted file mode 100644
index 78d2371..0000000
--- a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/StaticTextModifier.kt
+++ /dev/null
@@ -1,262 +0,0 @@
-package androidx.compose.foundation.newtext.text.modifiers
-
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.drawscope.ContentDrawScope
-import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
-import androidx.compose.ui.layout.AlignmentLine
-import androidx.compose.ui.layout.FirstBaseline
-import androidx.compose.ui.layout.IntrinsicMeasurable
-import androidx.compose.ui.layout.IntrinsicMeasureScope
-import androidx.compose.ui.layout.LastBaseline
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.layout.MeasureScope
-import androidx.compose.ui.node.DrawModifierNode
-import androidx.compose.ui.node.GlobalPositionAwareModifierNode
-import androidx.compose.ui.node.LayoutModifierNode
-import androidx.compose.ui.node.SemanticsModifierNode
-import androidx.compose.ui.node.invalidateDraw
-import androidx.compose.ui.node.invalidateLayer
-import androidx.compose.ui.node.invalidateMeasurements
-import androidx.compose.ui.node.invalidateSemantics
-import androidx.compose.ui.semantics.SemanticsConfiguration
-import androidx.compose.ui.semantics.getTextLayoutResult
-import androidx.compose.ui.semantics.text
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.TextLayoutResult
-import androidx.compose.ui.text.TextPainter
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import kotlin.math.roundToInt
-
-/**
- * Modifier that does Layout and Draw for [StaticTextLayoutDrawParams]
- */
-@OptIn(ExperimentalComposeUiApi::class)
-internal class StaticTextModifier(
-    params: StaticTextLayoutDrawParams
-) : Modifier.Node(), LayoutModifierNode, DrawModifierNode, GlobalPositionAwareModifierNode,
-    SemanticsModifierNode {
-    private var baselineCache: Map<AlignmentLine, Int>? = null
-    private var layoutCache: MultiParagraphLayoutCache? = null
-    private var textDelegateDirty = true
-
-    internal var params: StaticTextLayoutDrawParams = params
-        set(value) {
-            validate(value)
-            field = value
-            layoutCache?.let { cache ->
-                val diff = cache.diff(value)
-                if (diff.hasSemanticsDiffs) {
-                    _semanticsConfiguration = null
-                    invalidateSemantics()
-                }
-                if (diff.hasLayoutDiffs || diff.hasCallbackDiffs) {
-                    textDelegateDirty = true
-                    invalidateMeasurements()
-                }
-                if (diff.anyDiffs) {
-                    // if anything changed we redraw
-                    invalidateDraw()
-                }
-                return
-            }
-
-            // if no layout has happened, 🎶 just invalidate everything 🎶
-            // we don't expect to hit this often, but if we do don't keep anything around from
-            // the previous params and restart all passes ⚽
-            _semanticsConfiguration = null
-            textDelegateDirty = true
-            invalidateSemantics()
-            invalidateMeasurements()
-            invalidateDraw()
-        }
-
-    private var _semanticsConfiguration: SemanticsConfiguration? = null
-
-    private var semanticsTextLayoutResult: ((MutableList<TextLayoutResult>) -> Boolean)? = null
-
-    private fun generateSemantics(text: AnnotatedString): SemanticsConfiguration {
-        var localSemanticsTextLayoutResult = semanticsTextLayoutResult
-        if (localSemanticsTextLayoutResult == null) {
-            localSemanticsTextLayoutResult = { textLayoutResult ->
-                val layout = layoutCache?.layoutOrNull?.also {
-                    textLayoutResult.add(it)
-                }
-                layout != null
-            }
-            semanticsTextLayoutResult = localSemanticsTextLayoutResult
-        }
-        return SemanticsConfiguration().also {
-            it.isMergingSemanticsOfDescendants = false
-            it.isClearingSemantics = false
-            it.text = text
-            it.getTextLayoutResult(action = localSemanticsTextLayoutResult)
-        }
-    }
-
-    override val semanticsConfiguration: SemanticsConfiguration
-        get() {
-            var localSemantics = _semanticsConfiguration
-            if (localSemantics == null) {
-                localSemantics = generateSemantics(params.text)
-                _semanticsConfiguration = localSemantics
-            }
-            return localSemantics
-        }
-
-    private fun validate(params: StaticTextLayoutDrawParams) {
-        validateMinMaxLines(params.minLines, params.maxLines)
-    }
-
-    private fun getOrUpdateTextDelegateInLayout(
-        density: Density
-    ): MultiParagraphLayoutCache {
-        val localLayoutCache = layoutCache
-        return if (!textDelegateDirty && localLayoutCache != null) {
-            localLayoutCache
-        } else {
-            val textDelegate = MultiParagraphLayoutCache(params, density)
-            this.layoutCache = textDelegate
-            textDelegateDirty = false
-            textDelegate
-        }
-    }
-
-    fun measureNonExtension(
-        measureScope: MeasureScope,
-        measurable: Measurable,
-        constraints: Constraints
-    ): MeasureResult {
-        return measureScope.measure(measurable, constraints)
-    }
-
-    override fun MeasureScope.measure(
-        measurable: Measurable,
-        constraints: Constraints
-    ): MeasureResult {
-        val td = getOrUpdateTextDelegateInLayout(this)
-
-        val didChangeLayout = td.layoutWithConstraints(constraints, layoutDirection)
-        val textLayoutResult = td.layout
-
-        // ensure measure restarts when hasStaleResolvedFonts by reading in measure
-        textLayoutResult.multiParagraph.intrinsics.hasStaleResolvedFonts
-
-        if (didChangeLayout) {
-            invalidateLayer()
-            params.onTextLayout?.invoke(textLayoutResult)
-            params.selectionController?.updateTextLayout(textLayoutResult)
-            baselineCache = mapOf(
-                FirstBaseline to textLayoutResult.firstBaseline.roundToInt(),
-                LastBaseline to textLayoutResult.lastBaseline.roundToInt()
-            )
-        }
-
-        // first share the placeholders
-        params.onPlaceholderLayout?.invoke(textLayoutResult.placeholderRects)
-
-        // then allow children to measure _inside_ our final box, with the above placeholders
-        val placeable = measurable.measure(
-            Constraints.fixed(
-                textLayoutResult.size.width,
-                textLayoutResult.size.height
-            )
-        )
-
-        return layout(
-            textLayoutResult.size.width,
-            textLayoutResult.size.height,
-            baselineCache!!
-        ) {
-            // this is basically a graphicsLayer
-            placeable.place(0, 0)
-        }
-    }
-
-    fun minIntrinsicWidthNonExtension(
-        intrinsicMeasureScope: IntrinsicMeasureScope,
-        measurable: IntrinsicMeasurable,
-        height: Int
-    ): Int {
-        return intrinsicMeasureScope.minIntrinsicWidth(measurable, height)
-    }
-
-    override fun IntrinsicMeasureScope.minIntrinsicWidth(
-        measurable: IntrinsicMeasurable,
-        height: Int
-    ): Int {
-        val td = getOrUpdateTextDelegateInLayout(this)
-        return td.minIntrinsicWidth
-    }
-
-    fun minIntrinsicHeightNonExtension(
-        intrinsicMeasureScope: IntrinsicMeasureScope,
-        measurable: IntrinsicMeasurable,
-        width: Int
-    ): Int {
-        return intrinsicMeasureScope.minIntrinsicHeight(measurable, width)
-    }
-
-    override fun IntrinsicMeasureScope.minIntrinsicHeight(
-        measurable: IntrinsicMeasurable,
-        width: Int
-    ): Int {
-        return getOrUpdateTextDelegateInLayout(this)
-            .intrinsicHeightAt(width, layoutDirection)
-    }
-
-    fun maxIntrinsicWidthNonExtension(
-        intrinsicMeasureScope: IntrinsicMeasureScope,
-        measurable: IntrinsicMeasurable,
-        height: Int
-    ): Int {
-        return intrinsicMeasureScope.maxIntrinsicWidth(measurable, height)
-    }
-
-    override fun IntrinsicMeasureScope.maxIntrinsicWidth(
-        measurable: IntrinsicMeasurable,
-        height: Int
-    ): Int {
-        val td = getOrUpdateTextDelegateInLayout(this)
-        return td.maxIntrinsicWidth
-    }
-
-    fun maxIntrinsicHeightNonExtension(
-        intrinsicMeasureScope: IntrinsicMeasureScope,
-        measurable: IntrinsicMeasurable,
-        width: Int
-    ): Int {
-        return intrinsicMeasureScope.maxIntrinsicHeight(measurable, width)
-    }
-
-    override fun IntrinsicMeasureScope.maxIntrinsicHeight(
-        measurable: IntrinsicMeasurable,
-        width: Int
-    ): Int {
-        return getOrUpdateTextDelegateInLayout(this)
-            .intrinsicHeightAt(width, layoutDirection)
-    }
-
-    fun drawNonExtension(
-        contentDrawScope: ContentDrawScope
-    ) {
-        return contentDrawScope.draw()
-    }
-
-    override fun ContentDrawScope.draw() {
-        params.selectionController?.draw(this)
-        drawIntoCanvas { canvas ->
-            TextPainter.paint(canvas, requireNotNull(layoutCache?.layout))
-        }
-        if (!params.placeholders.isNullOrEmpty()) {
-            drawContent()
-        }
-    }
-
-    override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
-        params.selectionController?.updateGlobalPosition(coordinates)
-    }
-}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/StaticTextSelectionModifierController.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/StaticTextSelectionModifierController.kt
deleted file mode 100644
index 02a2ced8..0000000
--- a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/StaticTextSelectionModifierController.kt
+++ /dev/null
@@ -1,327 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.newtext.text.modifiers
-
-import androidx.compose.foundation.newtext.text.copypasta.TextDragObserver
-import androidx.compose.foundation.newtext.text.copypasta.detectDragGesturesAfterLongPressWithObserver
-import androidx.compose.foundation.newtext.text.copypasta.selection.MouseSelectionObserver
-import androidx.compose.foundation.newtext.text.copypasta.selection.MultiWidgetSelectionDelegate
-import androidx.compose.foundation.newtext.text.copypasta.selection.Selectable
-import androidx.compose.foundation.newtext.text.copypasta.selection.SelectionAdjustment
-import androidx.compose.foundation.newtext.text.copypasta.selection.SelectionRegistrar
-import androidx.compose.foundation.newtext.text.copypasta.selection.hasSelection
-import androidx.compose.foundation.newtext.text.copypasta.selection.mouseSelectionDetector
-import androidx.compose.foundation.newtext.text.copypasta.textPointerIcon
-import androidx.compose.runtime.RememberObserver
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.drawscope.ContentDrawScope
-import androidx.compose.ui.input.pointer.pointerHoverIcon
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.text.TextLayoutResult
-
-internal data class StaticTextSelectionParams(
-    val layoutCoordinates: LayoutCoordinates?,
-    val textLayoutResult: TextLayoutResult?
-) {
-    companion object {
-        val Empty = StaticTextSelectionParams(null, null)
-    }
-}
-
-// This is _basically_ a Modifier.Node but moved into remember because we need to do pointerInput
-// TODO: Refactor when Modifier.pointerInput is available for delegation
-internal class StaticTextSelectionModifierController(
-    val selectionRegistrar: SelectionRegistrar,
-    val backgroundSelectionColor: Color
-) : RememberObserver {
-    private var selectable: Selectable? = null
-    private val selectableId = selectionRegistrar.nextSelectableId()
-    // TODO: Move these into Modifer.element eventually
-    private var params: StaticTextSelectionParams = StaticTextSelectionParams.Empty
-
-    val modifier: Modifier = selectionRegistrar.makeSelectionModifier(
-        selectableId = selectableId,
-        layoutCoordinates = { params.layoutCoordinates },
-        textLayoutResult = { params.textLayoutResult },
-        // TODO: Use real isInTouchMode on merge
-        isInTouchMode = true /* fake it to android hardcode */
-    )
-
-    override fun onRemembered() {
-        selectable = selectionRegistrar.subscribe(
-            MultiWidgetSelectionDelegate(
-                selectableId = selectableId,
-                coordinatesCallback = { params.layoutCoordinates },
-                layoutResultCallback = { params.textLayoutResult }
-            )
-        )
-    }
-
-    override fun onForgotten() {
-        val localSelectable = selectable
-        if (localSelectable != null) {
-            selectionRegistrar.unsubscribe(localSelectable)
-            selectable = null
-        }
-    }
-
-    override fun onAbandoned() {
-        val localSelectable = selectable
-        if (localSelectable != null) {
-            selectionRegistrar.unsubscribe(localSelectable)
-            selectable = null
-        }
-    }
-
-    fun updateTextLayout(textLayoutResult: TextLayoutResult) {
-        params = params.copy(textLayoutResult = textLayoutResult)
-    }
-
-    fun updateGlobalPosition(coordinates: LayoutCoordinates) {
-        params = params.copy(layoutCoordinates = coordinates)
-    }
-
-    fun draw(contentDrawScope: ContentDrawScope) {
-        val layoutResult = params.textLayoutResult ?: return
-        val selection = selectionRegistrar.subselections[selectableId]
-
-        if (selection != null) {
-            val start = if (!selection.handlesCrossed) {
-                selection.start.offset
-            } else {
-                selection.end.offset
-            }
-            val end = if (!selection.handlesCrossed) {
-                selection.end.offset
-            } else {
-                selection.start.offset
-            }
-
-            if (start != end) {
-                val selectionPath = layoutResult.multiParagraph.getPathForRange(start, end)
-                with(contentDrawScope) {
-                    drawPath(selectionPath, backgroundSelectionColor)
-                }
-            }
-        }
-    }
-}
-
-// this is not chained, but is a standalone factory
-@Suppress("ModifierFactoryExtensionFunction")
-private fun SelectionRegistrar.makeSelectionModifier(
-    selectableId: Long,
-    layoutCoordinates: () -> LayoutCoordinates?,
-    textLayoutResult: () -> TextLayoutResult?,
-    isInTouchMode: Boolean
-): Modifier {
-    return if (isInTouchMode) {
-        val longPressDragObserver = object : TextDragObserver {
-            /**
-             * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
-             * recalculated.
-             */
-            var lastPosition = Offset.Zero
-
-            /**
-             * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
-             * it will be zeroed out.
-             */
-            var dragTotalDistance = Offset.Zero
-
-            override fun onDown(point: Offset) {
-                // Not supported for long-press-drag.
-            }
-
-            override fun onUp() {
-                // Nothing to do.
-            }
-
-            override fun onStart(startPoint: Offset) {
-                layoutCoordinates()?.let {
-                    if (!it.isAttached) return
-
-                    if (textLayoutResult().outOfBoundary(startPoint, startPoint)) {
-                        notifySelectionUpdateSelectAll(
-                            selectableId = selectableId
-                        )
-                    } else {
-                        notifySelectionUpdateStart(
-                            layoutCoordinates = it,
-                            startPosition = startPoint,
-                            adjustment = SelectionAdjustment.Word
-                        )
-                    }
-
-                    lastPosition = startPoint
-                }
-                // selection never started
-                if (!hasSelection(selectableId)) return
-                // Zero out the total distance that being dragged.
-                dragTotalDistance = Offset.Zero
-            }
-
-            override fun onDrag(delta: Offset) {
-                layoutCoordinates()?.let {
-                    if (!it.isAttached) return
-                    // selection never started, did not consume any drag
-                    if (!hasSelection(selectableId)) return
-
-                    dragTotalDistance += delta
-                    val newPosition = lastPosition + dragTotalDistance
-
-                    if (!textLayoutResult().outOfBoundary(lastPosition, newPosition)) {
-                        // Notice that only the end position needs to be updated here.
-                        // Start position is left unchanged. This is typically important when
-                        // long-press is using SelectionAdjustment.WORD or
-                        // SelectionAdjustment.PARAGRAPH that updates the start handle position from
-                        // the dragBeginPosition.
-                        val consumed = notifySelectionUpdate(
-                            layoutCoordinates = it,
-                            previousPosition = lastPosition,
-                            newPosition = newPosition,
-                            isStartHandle = false,
-                            adjustment = SelectionAdjustment.CharacterWithWordAccelerate
-                        )
-                        if (consumed) {
-                            lastPosition = newPosition
-                            dragTotalDistance = Offset.Zero
-                        }
-                    }
-                }
-            }
-
-            override fun onStop() {
-                if (hasSelection(selectableId)) {
-                    notifySelectionUpdateEnd()
-                }
-            }
-
-            override fun onCancel() {
-                if (hasSelection(selectableId)) {
-                    notifySelectionUpdateEnd()
-                }
-            }
-        }
-        Modifier.pointerInput(longPressDragObserver) {
-            detectDragGesturesAfterLongPressWithObserver(
-                longPressDragObserver
-            )
-        }
-    } else {
-        val mouseSelectionObserver = object : MouseSelectionObserver {
-            var lastPosition = Offset.Zero
-
-            override fun onExtend(downPosition: Offset): Boolean {
-                layoutCoordinates()?.let { layoutCoordinates ->
-                    if (!layoutCoordinates.isAttached) return false
-                    val consumed = notifySelectionUpdate(
-                        layoutCoordinates = layoutCoordinates,
-                        newPosition = downPosition,
-                        previousPosition = lastPosition,
-                        isStartHandle = false,
-                        adjustment = SelectionAdjustment.None
-                    )
-                    if (consumed) {
-                        lastPosition = downPosition
-                    }
-                    return hasSelection(selectableId)
-                }
-                return false
-            }
-
-            override fun onExtendDrag(dragPosition: Offset): Boolean {
-                layoutCoordinates()?.let { layoutCoordinates ->
-                    if (!layoutCoordinates.isAttached) return false
-                    if (!hasSelection(selectableId)) return false
-
-                    val consumed = notifySelectionUpdate(
-                        layoutCoordinates = layoutCoordinates,
-                        newPosition = dragPosition,
-                        previousPosition = lastPosition,
-                        isStartHandle = false,
-                        adjustment = SelectionAdjustment.None
-                    )
-
-                    if (consumed) {
-                        lastPosition = dragPosition
-                    }
-                }
-                return true
-            }
-
-            override fun onStart(
-                downPosition: Offset,
-                adjustment: SelectionAdjustment
-            ): Boolean {
-                layoutCoordinates()?.let {
-                    if (!it.isAttached) return false
-
-                    notifySelectionUpdateStart(
-                        layoutCoordinates = it,
-                        startPosition = downPosition,
-                        adjustment = adjustment
-                    )
-
-                    lastPosition = downPosition
-                    return hasSelection(selectableId)
-                }
-
-                return false
-            }
-
-            override fun onDrag(
-                dragPosition: Offset,
-                adjustment: SelectionAdjustment
-            ): Boolean {
-                layoutCoordinates()?.let {
-                    if (!it.isAttached) return false
-                    if (!hasSelection(selectableId)) return false
-
-                    val consumed = notifySelectionUpdate(
-                        layoutCoordinates = it,
-                        previousPosition = lastPosition,
-                        newPosition = dragPosition,
-                        isStartHandle = false,
-                        adjustment = adjustment
-                    )
-                    if (consumed) {
-                        lastPosition = dragPosition
-                    }
-                }
-                return true
-            }
-        }
-        Modifier.pointerInput(mouseSelectionObserver) {
-            mouseSelectionDetector(mouseSelectionObserver)
-        }.pointerHoverIcon(textPointerIcon)
-    }
-}
-
-private fun TextLayoutResult?.outOfBoundary(start: Offset, end: Offset): Boolean {
-    this ?: return false
-
-    val lastOffset = layoutInput.text.text.length
-    val rawStartOffset = getOffsetForPosition(start)
-    val rawEndOffset = getOffsetForPosition(end)
-
-    return rawStartOffset >= lastOffset - 1 && rawEndOffset >= lastOffset - 1 ||
-        rawStartOffset < 0 && rawEndOffset < 0
-}
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextAnnotatedStringElement.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextAnnotatedStringElement.kt
new file mode 100644
index 0000000..1204125
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextAnnotatedStringElement.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.newtext.text.modifiers
+
+import androidx.compose.foundation.newtext.text.DefaultMinLines
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.Placeholder
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.style.TextOverflow
+
+@ExperimentalComposeUiApi
+internal class TextAnnotatedStringElement(
+    private val text: AnnotatedString,
+    private val style: TextStyle,
+    private val fontFamilyResolver: FontFamily.Resolver,
+    private val onTextLayout: ((TextLayoutResult) -> Unit)? = null,
+    private val overflow: TextOverflow = TextOverflow.Clip,
+    private val softWrap: Boolean = true,
+    private val maxLines: Int = Int.MAX_VALUE,
+    private val minLines: Int = DefaultMinLines,
+    private val placeholders: List<AnnotatedString.Range<Placeholder>>? = null,
+    private val onPlaceholderLayout: ((List<Rect?>) -> Unit)? = null,
+    private val selectionController: SelectionController? = null
+) : ModifierNodeElement<TextAnnotatedStringNode>(inspectorInfo = debugInspectorInfo { }) {
+    override fun create(): TextAnnotatedStringNode = TextAnnotatedStringNode(
+        text,
+        style,
+        fontFamilyResolver,
+        onTextLayout,
+        overflow,
+        softWrap,
+        maxLines,
+        minLines,
+        placeholders,
+        onPlaceholderLayout,
+        selectionController
+    )
+
+    override fun update(node: TextAnnotatedStringNode): TextAnnotatedStringNode {
+        node.doInvalidations(
+            textChanged = node.updateText(
+                text = text
+            ),
+            layoutChanged = node.updateLayoutRelatedArgs(
+                style = style,
+                placeholders = placeholders,
+                minLines = minLines,
+                maxLines = maxLines,
+                softWrap = softWrap,
+                fontFamilyResolver = fontFamilyResolver,
+                overflow = overflow
+            ),
+            callbacksChanged = node.updateCallbacks(
+                onTextLayout = onTextLayout,
+                onPlaceholderLayout = onPlaceholderLayout,
+                selectionController = selectionController
+            )
+        )
+        return node
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+
+        if (other !is TextAnnotatedStringElement) return false
+
+        // these three are most likely to actually change
+        if (text != other.text) return false
+        if (style != other.style) return false
+        if (placeholders != other.placeholders) return false
+
+        // these are equally unlikely to change
+        if (fontFamilyResolver != other.fontFamilyResolver) return false
+        if (onTextLayout != other.onTextLayout) return false
+        if (overflow != other.overflow) return false
+        if (softWrap != other.softWrap) return false
+        if (maxLines != other.maxLines) return false
+        if (minLines != other.minLines) return false
+
+        // these never change, but check anyway for correctness
+        if (onPlaceholderLayout != other.onPlaceholderLayout) return false
+        if (selectionController != other.selectionController) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = super.hashCode()
+        result = 31 * result + text.hashCode()
+        result = 31 * result + style.hashCode()
+        result = 31 * result + fontFamilyResolver.hashCode()
+        result = 31 * result + (onTextLayout?.hashCode() ?: 0)
+        result = 31 * result + overflow.hashCode()
+        result = 31 * result + softWrap.hashCode()
+        result = 31 * result + maxLines
+        result = 31 * result + minLines
+        result = 31 * result + (placeholders?.hashCode() ?: 0)
+        result = 31 * result + (onPlaceholderLayout?.hashCode() ?: 0)
+        result = 31 * result + (selectionController?.hashCode() ?: 0)
+        return result
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextAnnotatedStringNode.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextAnnotatedStringNode.kt
new file mode 100644
index 0000000..164c4fb
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextAnnotatedStringNode.kt
@@ -0,0 +1,344 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.newtext.text.modifiers
+
+import androidx.compose.foundation.newtext.text.DefaultMinLines
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.layout.FirstBaseline
+import androidx.compose.ui.layout.IntrinsicMeasurable
+import androidx.compose.ui.layout.IntrinsicMeasureScope
+import androidx.compose.ui.layout.LastBaseline
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.SemanticsModifierNode
+import androidx.compose.ui.node.invalidateDraw
+import androidx.compose.ui.node.invalidateLayer
+import androidx.compose.ui.node.invalidateMeasurements
+import androidx.compose.ui.node.invalidateSemantics
+import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.getTextLayoutResult
+import androidx.compose.ui.semantics.text
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.Placeholder
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextPainter
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import kotlin.math.roundToInt
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal class TextAnnotatedStringNode(
+    private var text: AnnotatedString,
+    private var style: TextStyle,
+    private var fontFamilyResolver: FontFamily.Resolver,
+    private var onTextLayout: ((TextLayoutResult) -> Unit)? = null,
+    private var overflow: TextOverflow = TextOverflow.Clip,
+    private var softWrap: Boolean = true,
+    private var maxLines: Int = Int.MAX_VALUE,
+    private var minLines: Int = DefaultMinLines,
+    private var placeholders: List<AnnotatedString.Range<Placeholder>>? = null,
+    private var onPlaceholderLayout: ((List<Rect?>) -> Unit)? = null,
+    private var selectionController: SelectionController? = null
+) : Modifier.Node(), LayoutModifierNode, DrawModifierNode, SemanticsModifierNode {
+    private var baselineCache: Map<AlignmentLine, Int>? = null
+
+    private var _layoutCache: MultiParagraphLayoutCache? = null
+    private val layoutCache: MultiParagraphLayoutCache
+        get() {
+            if (_layoutCache == null) {
+                _layoutCache = MultiParagraphLayoutCache(
+                    text,
+                    style,
+                    fontFamilyResolver,
+                    overflow,
+                    softWrap,
+                    maxLines,
+                    minLines,
+                    placeholders
+                )
+            }
+            return _layoutCache!!
+        }
+
+    private fun getLayoutCache(density: Density): MultiParagraphLayoutCache {
+        return layoutCache.also { it.density = density }
+    }
+
+    fun updateText(text: AnnotatedString): Boolean {
+        if (this.text == text) return false
+        this.text = text
+        return true
+    }
+
+    fun updateLayoutRelatedArgs(
+        style: TextStyle,
+        placeholders: List<AnnotatedString.Range<Placeholder>>?,
+        minLines: Int,
+        maxLines: Int,
+        softWrap: Boolean,
+        fontFamilyResolver: FontFamily.Resolver,
+        overflow: TextOverflow
+    ): Boolean {
+        var changed = false
+        if (this.style != style) {
+            this.style = style
+            changed = true
+        }
+        if (this.placeholders != placeholders) {
+            this.placeholders = placeholders
+            changed = true
+        }
+
+        if (this.minLines != minLines) {
+            this.minLines = minLines
+            changed = true
+        }
+
+        if (this.maxLines != maxLines) {
+            this.maxLines != maxLines
+            changed = true
+        }
+
+        if (this.softWrap != softWrap) {
+            this.softWrap = softWrap
+            changed = true
+        }
+
+        if (this.fontFamilyResolver != fontFamilyResolver) {
+            this.fontFamilyResolver = fontFamilyResolver
+            changed = true
+        }
+
+        if (this.overflow != overflow) {
+            this.overflow = overflow
+            changed = true
+        }
+
+        return changed
+    }
+
+    fun updateCallbacks(
+        onTextLayout: ((TextLayoutResult) -> Unit)?,
+        onPlaceholderLayout: ((List<Rect?>) -> Unit)?,
+        selectionController: SelectionController?
+    ): Boolean {
+        var changed = false
+
+        if (this.onTextLayout != onTextLayout) {
+            this.onTextLayout = onTextLayout
+            changed = true
+        }
+
+        if (this.onPlaceholderLayout != onPlaceholderLayout) {
+            this.onPlaceholderLayout = onPlaceholderLayout
+            changed = true
+        }
+
+        if (this.selectionController != selectionController) {
+            this.selectionController = selectionController
+            changed = true
+        }
+        return changed
+    }
+
+    fun doInvalidations(
+        textChanged: Boolean,
+        layoutChanged: Boolean,
+        callbacksChanged: Boolean
+    ) {
+        if (textChanged) {
+            _semanticsConfiguration = null
+            invalidateSemantics()
+        }
+
+        if (textChanged || layoutChanged || callbacksChanged) {
+            layoutCache.update(
+                text = text,
+                style = style,
+                fontFamilyResolver = fontFamilyResolver,
+                overflow = overflow,
+                softWrap = softWrap,
+                maxLines = maxLines,
+                minLines = minLines,
+                placeholders = placeholders
+            )
+            invalidateMeasurements()
+            invalidateDraw()
+        }
+    }
+
+    private var _semanticsConfiguration: SemanticsConfiguration? = null
+
+    private var semanticsTextLayoutResult: ((MutableList<TextLayoutResult>) -> Boolean)? = null
+
+    private fun generateSemantics(text: AnnotatedString): SemanticsConfiguration {
+        var localSemanticsTextLayoutResult = semanticsTextLayoutResult
+        if (localSemanticsTextLayoutResult == null) {
+            localSemanticsTextLayoutResult = { textLayoutResult ->
+                val layout = layoutCache.layoutOrNull?.also {
+                    textLayoutResult.add(it)
+                }
+                layout != null
+            }
+            semanticsTextLayoutResult = localSemanticsTextLayoutResult
+        }
+        return SemanticsConfiguration().also {
+            it.isMergingSemanticsOfDescendants = false
+            it.isClearingSemantics = false
+            it.text = text
+            it.getTextLayoutResult(action = localSemanticsTextLayoutResult)
+        }
+    }
+
+    override val semanticsConfiguration: SemanticsConfiguration
+        get() {
+            var localSemantics = _semanticsConfiguration
+            if (localSemantics == null) {
+                localSemantics = generateSemantics(text)
+                _semanticsConfiguration = localSemantics
+            }
+            return localSemantics
+        }
+
+    fun measureNonExtension(
+        measureScope: MeasureScope,
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        return measureScope.measure(measurable, constraints)
+    }
+
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        val layoutCache = getLayoutCache(this)
+
+        val didChangeLayout = layoutCache.layoutWithConstraints(constraints, layoutDirection)
+        val textLayoutResult = layoutCache.layout
+
+        // ensure measure restarts when hasStaleResolvedFonts by reading in measure
+        textLayoutResult.multiParagraph.intrinsics.hasStaleResolvedFonts
+
+        if (didChangeLayout) {
+            invalidateLayer()
+            onTextLayout?.invoke(textLayoutResult)
+            selectionController?.updateTextLayout(textLayoutResult)
+            baselineCache = mapOf(
+                FirstBaseline to textLayoutResult.firstBaseline.roundToInt(),
+                LastBaseline to textLayoutResult.lastBaseline.roundToInt()
+            )
+        }
+
+        // first share the placeholders
+        onPlaceholderLayout?.invoke(textLayoutResult.placeholderRects)
+
+        // then allow children to measure _inside_ our final box, with the above placeholders
+        val placeable = measurable.measure(
+            Constraints.fixed(
+                textLayoutResult.size.width,
+                textLayoutResult.size.height
+            )
+        )
+
+        return layout(
+            textLayoutResult.size.width,
+            textLayoutResult.size.height,
+            baselineCache!!
+        ) {
+            // this is basically a graphicsLayer
+            placeable.place(0, 0)
+        }
+    }
+
+    fun minIntrinsicWidthNonExtension(
+        intrinsicMeasureScope: IntrinsicMeasureScope,
+        measurable: IntrinsicMeasurable,
+        height: Int
+    ): Int {
+        return intrinsicMeasureScope.minIntrinsicWidth(measurable, height)
+    }
+
+    override fun IntrinsicMeasureScope.minIntrinsicWidth(
+        measurable: IntrinsicMeasurable,
+        height: Int
+    ): Int {
+        return getLayoutCache(this).minIntrinsicWidth
+    }
+
+    fun minIntrinsicHeightNonExtension(
+        intrinsicMeasureScope: IntrinsicMeasureScope,
+        measurable: IntrinsicMeasurable,
+        width: Int
+    ): Int {
+        return intrinsicMeasureScope.minIntrinsicHeight(measurable, width)
+    }
+
+    override fun IntrinsicMeasureScope.minIntrinsicHeight(
+        measurable: IntrinsicMeasurable,
+        width: Int
+    ): Int = getLayoutCache(this).intrinsicHeightAt(width, layoutDirection)
+
+    fun maxIntrinsicWidthNonExtension(
+        intrinsicMeasureScope: IntrinsicMeasureScope,
+        measurable: IntrinsicMeasurable,
+        height: Int
+    ): Int = intrinsicMeasureScope.maxIntrinsicWidth(measurable, height)
+
+    override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+        measurable: IntrinsicMeasurable,
+        height: Int
+    ): Int = getLayoutCache(this).maxIntrinsicWidth
+
+    fun maxIntrinsicHeightNonExtension(
+        intrinsicMeasureScope: IntrinsicMeasureScope,
+        measurable: IntrinsicMeasurable,
+        width: Int
+    ): Int = intrinsicMeasureScope.maxIntrinsicHeight(measurable, width)
+
+    override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+        measurable: IntrinsicMeasurable,
+        width: Int
+    ): Int = getLayoutCache(this).intrinsicHeightAt(width, layoutDirection)
+
+    fun drawNonExtension(
+        contentDrawScope: ContentDrawScope
+    ) {
+        return contentDrawScope.draw()
+    }
+
+    override fun ContentDrawScope.draw() {
+        selectionController?.draw(this)
+        drawIntoCanvas { canvas ->
+            TextPainter.paint(canvas, requireNotNull(layoutCache.layout))
+        }
+        if (!placeholders.isNullOrEmpty()) {
+            drawContent()
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextStringSimpleElement.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextStringSimpleElement.kt
new file mode 100644
index 0000000..2e2ed43
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextStringSimpleElement.kt
@@ -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.compose.foundation.newtext.text.modifiers
+
+import androidx.compose.foundation.newtext.text.DefaultMinLines
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.style.TextOverflow
+
+@ExperimentalComposeUiApi
+internal class TextStringSimpleElement(
+    private val text: String,
+    private val style: TextStyle,
+    private val fontFamilyResolver: FontFamily.Resolver,
+    private val overflow: TextOverflow = TextOverflow.Clip,
+    private val softWrap: Boolean = true,
+    private val maxLines: Int = Int.MAX_VALUE,
+    private val minLines: Int = DefaultMinLines,
+) : ModifierNodeElement<TextStringSimpleNode>(inspectorInfo = debugInspectorInfo { }) {
+    override fun create(): TextStringSimpleNode = TextStringSimpleNode(
+        text,
+        style,
+        fontFamilyResolver,
+        overflow,
+        softWrap,
+        maxLines,
+        minLines
+    )
+
+    override fun update(node: TextStringSimpleNode): TextStringSimpleNode {
+        node.doInvalidations(
+            textChanged = node.updateText(
+                text = text
+            ),
+            layoutChanged = node.updateLayoutRelatedArgs(
+                style = style,
+                minLines = minLines,
+                maxLines = maxLines,
+                softWrap = softWrap,
+                fontFamilyResolver = fontFamilyResolver,
+                overflow = overflow
+            )
+        )
+        return node
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+
+        if (other !is TextStringSimpleElement) return false
+
+        // these three are most likely to actually change
+        if (text != other.text) return false
+        if (style != other.style) return false
+
+        // these are equally unlikely to change
+        if (fontFamilyResolver != other.fontFamilyResolver) return false
+        if (overflow != other.overflow) return false
+        if (softWrap != other.softWrap) return false
+        if (maxLines != other.maxLines) return false
+        if (minLines != other.minLines) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = super.hashCode()
+        result = 31 * result + text.hashCode()
+        result = 31 * result + style.hashCode()
+        result = 31 * result + fontFamilyResolver.hashCode()
+        result = 31 * result + overflow.hashCode()
+        result = 31 * result + softWrap.hashCode()
+        result = 31 * result + maxLines
+        result = 31 * result + minLines
+        return result
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextStringSimpleNode.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextStringSimpleNode.kt
new file mode 100644
index 0000000..a90ff59
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextStringSimpleNode.kt
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.newtext.text.modifiers
+
+import androidx.compose.foundation.newtext.text.DefaultMinLines
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.Fill
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.layout.FirstBaseline
+import androidx.compose.ui.layout.IntrinsicMeasurable
+import androidx.compose.ui.layout.IntrinsicMeasureScope
+import androidx.compose.ui.layout.LastBaseline
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.SemanticsModifierNode
+import androidx.compose.ui.node.invalidateDraw
+import androidx.compose.ui.node.invalidateLayer
+import androidx.compose.ui.node.invalidateMeasurements
+import androidx.compose.ui.node.invalidateSemantics
+import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.getTextLayoutResult
+import androidx.compose.ui.semantics.text
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import kotlin.math.roundToInt
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal class TextStringSimpleNode(
+    private var text: String,
+    private var style: TextStyle,
+    private var fontFamilyResolver: FontFamily.Resolver,
+    private var overflow: TextOverflow = TextOverflow.Clip,
+    private var softWrap: Boolean = true,
+    private var maxLines: Int = Int.MAX_VALUE,
+    private var minLines: Int = DefaultMinLines
+) : Modifier.Node(), LayoutModifierNode, DrawModifierNode, SemanticsModifierNode {
+    private var baselineCache: Map<AlignmentLine, Int>? = null
+
+    private var _layoutCache: ParagraphLayoutCache? = null
+    private val layoutCache: ParagraphLayoutCache
+        get() {
+            if (_layoutCache == null) {
+                _layoutCache = ParagraphLayoutCache(
+                    text,
+                    style,
+                    fontFamilyResolver,
+                    overflow,
+                    softWrap,
+                    maxLines,
+                    minLines,
+                )
+            }
+            return _layoutCache!!
+        }
+
+    private fun getLayoutCache(density: Density): ParagraphLayoutCache {
+        return layoutCache.also { it.density = density }
+    }
+
+    fun updateText(text: String): Boolean {
+        if (this.text == text) return false
+        this.text = text
+        return true
+    }
+
+    fun updateLayoutRelatedArgs(
+        style: TextStyle,
+        minLines: Int,
+        maxLines: Int,
+        softWrap: Boolean,
+        fontFamilyResolver: FontFamily.Resolver,
+        overflow: TextOverflow
+    ): Boolean {
+        var changed = false
+        if (this.style != style) {
+            this.style = style
+            changed = true
+        }
+
+        if (this.minLines != minLines) {
+            this.minLines = minLines
+            changed = true
+        }
+
+        if (this.maxLines != maxLines) {
+            this.maxLines != maxLines
+            changed = true
+        }
+
+        if (this.softWrap != softWrap) {
+            this.softWrap = softWrap
+            changed = true
+        }
+
+        if (this.fontFamilyResolver != fontFamilyResolver) {
+            this.fontFamilyResolver = fontFamilyResolver
+            changed = true
+        }
+
+        if (this.overflow != overflow) {
+            this.overflow = overflow
+            changed = true
+        }
+
+        return changed
+    }
+
+    fun doInvalidations(
+        textChanged: Boolean,
+        layoutChanged: Boolean
+    ) {
+        if (textChanged) {
+            _semanticsConfiguration = null
+            invalidateSemantics()
+        }
+
+        if (textChanged || layoutChanged) {
+            layoutCache.update(
+                text = text,
+                style = style,
+                fontFamilyResolver = fontFamilyResolver,
+                overflow = overflow,
+                softWrap = softWrap,
+                maxLines = maxLines,
+                minLines = minLines
+            )
+            invalidateMeasurements()
+            invalidateDraw()
+        }
+    }
+
+    private var _semanticsConfiguration: SemanticsConfiguration? = null
+
+    private var semanticsTextLayoutResult: ((MutableList<TextLayoutResult>) -> Boolean)? = null
+
+    private fun generateSemantics(text: String): SemanticsConfiguration {
+        var localSemanticsTextLayoutResult = semanticsTextLayoutResult
+        if (localSemanticsTextLayoutResult == null) {
+            localSemanticsTextLayoutResult = { textLayoutResult ->
+                val layout = layoutCache.slowCreateTextLayoutResultOrNull()?.also {
+                    textLayoutResult.add(it)
+                }
+                layout != null
+                false
+            }
+            semanticsTextLayoutResult = localSemanticsTextLayoutResult
+        }
+        return SemanticsConfiguration().also {
+            it.isMergingSemanticsOfDescendants = false
+            it.isClearingSemantics = false
+            it.text = AnnotatedString(text)
+            it.getTextLayoutResult(action = localSemanticsTextLayoutResult)
+        }
+    }
+
+    override val semanticsConfiguration: SemanticsConfiguration
+        get() {
+            var localSemantics = _semanticsConfiguration
+            if (localSemantics == null) {
+                localSemantics = generateSemantics(text)
+                _semanticsConfiguration = localSemantics
+            }
+            return localSemantics
+        }
+
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        val layoutCache = getLayoutCache(this)
+
+        val didChangeLayout = layoutCache.layoutWithConstraints(constraints, layoutDirection)
+        // ensure measure restarts when hasStaleResolvedFonts by reading in measure
+        layoutCache.observeFontChanges
+        val paragraph = layoutCache.paragraph!!
+        val layoutSize = layoutCache.layoutSize
+
+        if (didChangeLayout) {
+            invalidateLayer()
+            baselineCache = mapOf(
+                FirstBaseline to paragraph.firstBaseline.roundToInt(),
+                LastBaseline to paragraph.lastBaseline.roundToInt()
+            )
+        }
+
+        // then allow children to measure _inside_ our final box, with the above placeholders
+        val placeable = measurable.measure(
+            Constraints.fixed(
+                layoutSize.width,
+                layoutSize.height
+            )
+        )
+
+        return layout(
+            layoutSize.width,
+            layoutSize.height,
+            baselineCache!!
+        ) {
+            // this is basically a graphicsLayer
+            placeable.place(0, 0)
+        }
+    }
+
+    override fun IntrinsicMeasureScope.minIntrinsicWidth(
+        measurable: IntrinsicMeasurable,
+        height: Int
+    ): Int {
+        return getLayoutCache(this).minIntrinsicWidth
+    }
+
+    override fun IntrinsicMeasureScope.minIntrinsicHeight(
+        measurable: IntrinsicMeasurable,
+        width: Int
+    ): Int = getLayoutCache(this).intrinsicHeightAt(width, layoutDirection)
+
+    override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+        measurable: IntrinsicMeasurable,
+        height: Int
+    ): Int = getLayoutCache(this).maxIntrinsicWidth
+
+    override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+        measurable: IntrinsicMeasurable,
+        width: Int
+    ): Int = getLayoutCache(this).intrinsicHeightAt(width, layoutDirection)
+
+    @OptIn(ExperimentalTextApi::class)
+    override fun ContentDrawScope.draw() {
+        val localParagraph = requireNotNull(layoutCache.paragraph)
+        drawIntoCanvas { canvas ->
+            val willClip = layoutCache.didOverflow
+            if (willClip) {
+                val width = layoutCache.layoutSize.width.toFloat()
+                val height = layoutCache.layoutSize.height.toFloat()
+                val bounds = Rect(Offset.Zero, Size(width, height))
+                canvas.save()
+                canvas.clipRect(bounds)
+            }
+            try {
+                val textDecoration = style.textDecoration ?: TextDecoration.None
+                val shadow = style.shadow ?: Shadow.None
+                val drawStyle = style.drawStyle ?: Fill
+                val brush = style.brush
+                if (brush != null) {
+                    val alpha = style.alpha
+                    localParagraph.paint(
+                        canvas = canvas,
+                        brush = brush,
+                        alpha = alpha,
+                        shadow = shadow,
+                        drawStyle = drawStyle,
+                        textDecoration = textDecoration
+                    )
+                } else {
+                    val color = style.color
+                    localParagraph.paint(
+                        canvas = canvas,
+                        color = color,
+                        shadow = shadow,
+                        textDecoration = textDecoration,
+                        drawStyle = drawStyle
+                    )
+                }
+            } finally {
+                if (willClip) {
+                    canvas.restore()
+                }
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation-newtext/src/test/kotlin/androidx/compose/foundation/newtext/text/modifiers/MultiParagraphLayoutCacheTest.kt b/compose/foundation/foundation-newtext/src/test/kotlin/androidx/compose/foundation/newtext/text/modifiers/MultiParagraphLayoutCacheTest.kt
index 8615074..32ef628 100644
--- a/compose/foundation/foundation-newtext/src/test/kotlin/androidx/compose/foundation/newtext/text/modifiers/MultiParagraphLayoutCacheTest.kt
+++ b/compose/foundation/foundation-newtext/src/test/kotlin/androidx/compose/foundation/newtext/text/modifiers/MultiParagraphLayoutCacheTest.kt
@@ -33,13 +33,12 @@
     @Test(expected = IllegalStateException::class)
     fun whenMinInstrinsicWidth_withoutLayout_throws() {
         val textDelegate = MultiParagraphLayoutCache(
-            StaticTextLayoutDrawParams(
-                text = AnnotatedString(""),
-                style = TextStyle.Default,
-                fontFamilyResolver = fontFamilyResolver
-            ),
-            density = density,
-        )
+            text = AnnotatedString(""),
+            style = TextStyle.Default,
+            fontFamilyResolver = fontFamilyResolver
+        ).also {
+            it.density = density
+        }
 
         textDelegate.minIntrinsicWidth
     }
@@ -47,13 +46,12 @@
     @Test(expected = IllegalStateException::class)
     fun whenMaxIntrinsicWidth_withoutLayout_throws() {
         val textDelegate = MultiParagraphLayoutCache(
-            StaticTextLayoutDrawParams(
-                text = AnnotatedString(""),
-                style = TextStyle.Default,
-                fontFamilyResolver = fontFamilyResolver
-            ),
-            density = density,
-        )
+            text = AnnotatedString(""),
+            style = TextStyle.Default,
+            fontFamilyResolver = fontFamilyResolver
+        ).also {
+            it.density = density
+        }
 
         textDelegate.maxIntrinsicWidth
     }
diff --git a/compose/foundation/foundation-newtext/src/test/kotlin/androidx/compose/foundation/newtext/text/modifiers/StaticTextLayouDrawParamsTest.kt b/compose/foundation/foundation-newtext/src/test/kotlin/androidx/compose/foundation/newtext/text/modifiers/StaticTextLayouDrawParamsTest.kt
deleted file mode 100644
index 5bff284..0000000
--- a/compose/foundation/foundation-newtext/src/test/kotlin/androidx/compose/foundation/newtext/text/modifiers/StaticTextLayouDrawParamsTest.kt
+++ /dev/null
@@ -1,229 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.newtext.text.modifiers
-
-import androidx.compose.foundation.newtext.text.copypasta.selection.SelectionRegistrarImpl
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.Density
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.kotlin.mock
-
-@RunWith(JUnit4::class)
-class StaticTextLayouDrawParamsTest {
-    private val density = Density(density = 1f)
-    private val fontFamilyResolver = mock<FontFamily.Resolver>()
-
-    @Test
-    fun textDiffers_flipsLayoutAndSemantics() {
-        val subject = StaticTextLayoutDrawParams(
-            AnnotatedString("text"),
-            TextStyle.Default,
-            fontFamilyResolver
-        )
-        val other = subject.copy(text = AnnotatedString("other"))
-
-        val diff = subject.diff(other)
-
-        assertThat(diff.value).isEqualTo(1.toShort())
-        assertThat(diff.anyDiffs).isTrue()
-        assertThat(diff.hasLayoutDiffs).isTrue()
-        assertThat(diff.hasSemanticsDiffs).isTrue()
-        assertThat(diff.hasCallbackDiffs).isFalse()
-    }
-
-    @Test
-    fun styleDiffers_flipsLayout() {
-        val subject = StaticTextLayoutDrawParams(
-            AnnotatedString("text"),
-            TextStyle.Default,
-            fontFamilyResolver
-        )
-        val other = subject.copy(style = TextStyle(fontFeatureSettings = "other"))
-
-        val diff = subject.diff(other)
-
-        assertThat(diff.anyDiffs).isTrue()
-        assertThat(diff.hasLayoutDiffs).isTrue()
-        assertThat(diff.hasSemanticsDiffs).isFalse()
-        assertThat(diff.hasCallbackDiffs).isFalse()
-    }
-
-    @Test
-    fun ffrDiffers_flipsLayout() {
-        val subject = StaticTextLayoutDrawParams(
-            AnnotatedString("text"),
-            TextStyle.Default,
-            fontFamilyResolver
-        )
-        val other = subject.copy(fontFamilyResolver = mock<FontFamily.Resolver>())
-
-        val diff = subject.diff(other)
-
-        assertThat(diff.anyDiffs).isTrue()
-        assertThat(diff.hasLayoutDiffs).isTrue()
-        assertThat(diff.hasSemanticsDiffs).isFalse()
-        assertThat(diff.hasCallbackDiffs).isFalse()
-    }
-
-    @Test
-    fun overflow_flipsLayout() {
-        val subject = StaticTextLayoutDrawParams(
-            AnnotatedString("text"),
-            TextStyle.Default,
-            fontFamilyResolver
-        )
-        val other = subject.copy(overflow = TextOverflow.Ellipsis)
-
-        val diff = subject.diff(other)
-
-        assertThat(diff.anyDiffs).isTrue()
-        assertThat(diff.hasLayoutDiffs).isTrue()
-        assertThat(diff.hasSemanticsDiffs).isFalse()
-        assertThat(diff.hasCallbackDiffs).isFalse()
-    }
-
-    @Test
-    fun softWrap_flipsLayout() {
-        val subject = StaticTextLayoutDrawParams(
-            AnnotatedString("text"),
-            TextStyle.Default,
-            fontFamilyResolver
-        )
-        val other = subject.copy(softWrap = false)
-
-        val diff = subject.diff(other)
-
-        assertThat(diff.anyDiffs).isTrue()
-        assertThat(diff.hasLayoutDiffs).isTrue()
-        assertThat(diff.hasSemanticsDiffs).isFalse()
-        assertThat(diff.hasCallbackDiffs).isFalse()
-    }
-
-    @Test
-    fun maxLines_flipsLayout() {
-        val subject = StaticTextLayoutDrawParams(
-            AnnotatedString("text"),
-            TextStyle.Default,
-            fontFamilyResolver
-        )
-        val other = subject.copy(maxLines = 10)
-
-        val diff = subject.diff(other)
-
-        assertThat(diff.anyDiffs).isTrue()
-        assertThat(diff.hasLayoutDiffs).isTrue()
-        assertThat(diff.hasSemanticsDiffs).isFalse()
-        assertThat(diff.hasCallbackDiffs).isFalse()
-    }
-
-    @Test
-    fun minLines_flipsLayout() {
-        val subject = StaticTextLayoutDrawParams(
-            AnnotatedString("text"),
-            TextStyle.Default,
-            fontFamilyResolver
-        )
-        val other = subject.copy(minLines = 10)
-
-        val diff = subject.diff(other)
-
-        assertThat(diff.anyDiffs).isTrue()
-        assertThat(diff.hasLayoutDiffs).isTrue()
-        assertThat(diff.hasSemanticsDiffs).isFalse()
-        assertThat(diff.hasCallbackDiffs).isFalse()
-    }
-
-    @Test
-    fun placeholders_flipsLayout() {
-        val subject = StaticTextLayoutDrawParams(
-            AnnotatedString("text"),
-            TextStyle.Default,
-            fontFamilyResolver
-        )
-        val other = subject.copy(placeholders = emptyList())
-
-        val diff = subject.diff(other)
-
-        assertThat(diff.anyDiffs).isTrue()
-        assertThat(diff.hasLayoutDiffs).isTrue()
-        assertThat(diff.hasSemanticsDiffs).isFalse()
-        assertThat(diff.hasCallbackDiffs).isFalse()
-    }
-
-    @Test
-    fun onPlaceholderLayout_flipsCallbacks() {
-        val subject = StaticTextLayoutDrawParams(
-            AnnotatedString("text"),
-            TextStyle.Default,
-            fontFamilyResolver
-        )
-        val other = subject.copy(onPlaceholderLayout = { println("Do it") })
-
-        val diff = subject.diff(other)
-
-        assertThat(diff.anyDiffs).isTrue()
-        assertThat(diff.hasLayoutDiffs).isFalse()
-        assertThat(diff.hasSemanticsDiffs).isFalse()
-        assertThat(diff.hasCallbackDiffs).isTrue()
-    }
-
-    @Test
-    fun onTextLayout_flipsCallbacks() {
-        val subject = StaticTextLayoutDrawParams(
-            AnnotatedString("text"),
-            TextStyle.Default,
-            fontFamilyResolver
-        )
-        val other = subject.copy(onTextLayout = { println("Did a layout") })
-
-        val diff = subject.diff(other)
-
-        assertThat(diff.anyDiffs).isTrue()
-        assertThat(diff.hasLayoutDiffs).isFalse()
-        assertThat(diff.hasSemanticsDiffs).isFalse()
-        assertThat(diff.hasCallbackDiffs).isTrue()
-    }
-
-    @Test
-    fun onSelectionController_flipsAnyAndCallbacks() {
-        val subject = StaticTextLayoutDrawParams(
-            AnnotatedString("text"),
-            TextStyle.Default,
-            fontFamilyResolver
-        )
-        val other = subject.copy(
-            selectionController = StaticTextSelectionModifierController(
-                SelectionRegistrarImpl(),
-                Color.Black
-            )
-        )
-
-        val diff = subject.diff(other)
-
-        assertThat(diff.anyDiffs).isTrue()
-        assertThat(diff.hasLayoutDiffs).isFalse()
-        assertThat(diff.hasSemanticsDiffs).isFalse()
-        assertThat(diff.hasCallbackDiffs).isTrue()
-    }
-}
diff --git a/compose/foundation/foundation/api/current.ignore b/compose/foundation/foundation/api/current.ignore
index 90e6259..d1a4e31 100644
--- a/compose/foundation/foundation/api/current.ignore
+++ b/compose/foundation/foundation/api/current.ignore
@@ -1,4 +1,8 @@
 // Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.foundation.lazy.grid.LazyGridLayoutInfo#getMainAxisItemSpacing():
+    Added method androidx.compose.foundation.lazy.grid.LazyGridLayoutInfo.getMainAxisItemSpacing()
+
+
 RemovedClass: androidx.compose.foundation.lazy.LazyListPinningModifierKt:
     Removed class androidx.compose.foundation.lazy.LazyListPinningModifierKt
 RemovedClass: androidx.compose.foundation.lazy.layout.PinnableParentKt:
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index dae8b69..1c8281c 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -81,7 +81,7 @@
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Image(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int filterQuality);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Image(androidx.compose.ui.graphics.vector.ImageVector imageVector, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
     method @androidx.compose.runtime.Composable public static void Image(androidx.compose.ui.graphics.painter.Painter painter, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
-    method @Deprecated @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Image(androidx.compose.ui.graphics.ImageBitmap bitmap, String contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
+    method @Deprecated @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Image(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
   }
 
   @androidx.compose.runtime.Stable public interface Indication {
@@ -484,6 +484,7 @@
   @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListLayoutInfo {
     method public default int getAfterContentPadding();
     method public default int getBeforeContentPadding();
+    method public default int getMainAxisItemSpacing();
     method public default androidx.compose.foundation.gestures.Orientation getOrientation();
     method public default boolean getReverseLayout();
     method public int getTotalItemsCount();
@@ -493,6 +494,7 @@
     method public java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> getVisibleItemsInfo();
     property public default int afterContentPadding;
     property public default int beforeContentPadding;
+    property public default int mainAxisItemSpacing;
     property public default androidx.compose.foundation.gestures.Orientation orientation;
     property public default boolean reverseLayout;
     property public abstract int totalItemsCount;
@@ -505,9 +507,6 @@
   public final class LazyListMeasureKt {
   }
 
-  public final class LazyListPinnableContainerProviderKt {
-  }
-
   @androidx.compose.foundation.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
     method public default void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
     method @Deprecated public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,? extends kotlin.Unit> content);
@@ -627,6 +626,7 @@
   public sealed interface LazyGridLayoutInfo {
     method public int getAfterContentPadding();
     method public int getBeforeContentPadding();
+    method public int getMainAxisItemSpacing();
     method public androidx.compose.foundation.gestures.Orientation getOrientation();
     method public boolean getReverseLayout();
     method public int getTotalItemsCount();
@@ -636,6 +636,7 @@
     method public java.util.List<androidx.compose.foundation.lazy.grid.LazyGridItemInfo> getVisibleItemsInfo();
     property public abstract int afterContentPadding;
     property public abstract int beforeContentPadding;
+    property public abstract int mainAxisItemSpacing;
     property public abstract androidx.compose.foundation.gestures.Orientation orientation;
     property public abstract boolean reverseLayout;
     property public abstract int totalItemsCount;
@@ -709,6 +710,9 @@
   public final class LazyLayoutKt {
   }
 
+  public final class LazyLayoutPinnableItemKt {
+  }
+
   public final class LazyLayoutPrefetcher_androidKt {
   }
 
@@ -1014,6 +1018,9 @@
   public final class LongPressTextDragObserverKt {
   }
 
+  public final class PointerMoveDetectorKt {
+  }
+
   public final class StringHelpersKt {
   }
 
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 681c092..d74aeec 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -99,7 +99,7 @@
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Image(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int filterQuality);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Image(androidx.compose.ui.graphics.vector.ImageVector imageVector, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
     method @androidx.compose.runtime.Composable public static void Image(androidx.compose.ui.graphics.painter.Painter painter, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
-    method @Deprecated @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Image(androidx.compose.ui.graphics.ImageBitmap bitmap, String contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
+    method @Deprecated @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Image(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
   }
 
   @androidx.compose.runtime.Stable public interface Indication {
@@ -185,10 +185,8 @@
   }
 
   @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public interface OverscrollEffect {
-    method public suspend Object? consumePostFling(long velocity, kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public void consumePostScroll(long initialDragDelta, long overscrollDelta, int source);
-    method public suspend Object? consumePreFling(long velocity, kotlin.coroutines.Continuation<? super androidx.compose.ui.unit.Velocity>);
-    method public long consumePreScroll(long scrollDelta, int source);
+    method public suspend Object? applyToFling(long velocity, kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Velocity,? super kotlin.coroutines.Continuation<? super androidx.compose.ui.unit.Velocity>,?> performFling, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public long applyToScroll(long delta, int source, kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,androidx.compose.ui.geometry.Offset> performScroll);
     method public androidx.compose.ui.Modifier getEffectModifier();
     method public boolean isInProgress();
     property public abstract androidx.compose.ui.Modifier effectModifier;
@@ -580,6 +578,7 @@
   @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListLayoutInfo {
     method public default int getAfterContentPadding();
     method public default int getBeforeContentPadding();
+    method public default int getMainAxisItemSpacing();
     method public default androidx.compose.foundation.gestures.Orientation getOrientation();
     method public default boolean getReverseLayout();
     method public int getTotalItemsCount();
@@ -589,6 +588,7 @@
     method public java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> getVisibleItemsInfo();
     property public default int afterContentPadding;
     property public default int beforeContentPadding;
+    property public default int mainAxisItemSpacing;
     property public default androidx.compose.foundation.gestures.Orientation orientation;
     property public default boolean reverseLayout;
     property public abstract int totalItemsCount;
@@ -601,9 +601,6 @@
   public final class LazyListMeasureKt {
   }
 
-  public final class LazyListPinnableContainerProviderKt {
-  }
-
   @androidx.compose.foundation.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
     method public default void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
     method @Deprecated public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,? extends kotlin.Unit> content);
@@ -726,6 +723,7 @@
   public sealed interface LazyGridLayoutInfo {
     method public int getAfterContentPadding();
     method public int getBeforeContentPadding();
+    method public int getMainAxisItemSpacing();
     method public androidx.compose.foundation.gestures.Orientation getOrientation();
     method public boolean getReverseLayout();
     method public int getTotalItemsCount();
@@ -735,6 +733,7 @@
     method public java.util.List<androidx.compose.foundation.lazy.grid.LazyGridItemInfo> getVisibleItemsInfo();
     property public abstract int afterContentPadding;
     property public abstract int beforeContentPadding;
+    property public abstract int mainAxisItemSpacing;
     property public abstract androidx.compose.foundation.gestures.Orientation orientation;
     property public abstract boolean reverseLayout;
     property public abstract int totalItemsCount;
@@ -857,6 +856,19 @@
     method @androidx.compose.runtime.Stable public default long toSp(float);
   }
 
+  public final class LazyLayoutPinnableItemKt {
+    method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyLayoutPinnableItem(int index, androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList pinnedItemList, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
+  @androidx.compose.foundation.ExperimentalFoundationApi public final class LazyLayoutPinnedItemList implements kotlin.jvm.internal.markers.KMappedMarker java.util.List<androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList.PinnedItem> {
+    ctor public LazyLayoutPinnedItemList();
+  }
+
+  @androidx.compose.foundation.ExperimentalFoundationApi public static sealed interface LazyLayoutPinnedItemList.PinnedItem {
+    method public int getIndex();
+    property public abstract int index;
+  }
+
   @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public final class LazyLayoutPrefetchState {
     ctor public LazyLayoutPrefetchState();
     method public androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle schedulePrefetch(int index, long constraints);
@@ -902,10 +914,10 @@
   public final class LazyStaggeredGridDslKt {
     method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyHorizontalStaggeredGrid(androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells rows, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope,kotlin.Unit> content);
     method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyVerticalStaggeredGrid(androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells columns, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope,kotlin.Unit> content);
-    method @androidx.compose.foundation.ExperimentalFoundationApi public static <T> void items(androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemScope,? super T,kotlin.Unit> itemContent);
-    method @androidx.compose.foundation.ExperimentalFoundationApi public static <T> void items(androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemScope,? super T,kotlin.Unit> itemContent);
-    method @androidx.compose.foundation.ExperimentalFoundationApi public static <T> void itemsIndexed(androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
-    method @androidx.compose.foundation.ExperimentalFoundationApi public static <T> void itemsIndexed(androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method @androidx.compose.foundation.ExperimentalFoundationApi public static inline <T> void items(androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,?> contentType, optional kotlin.jvm.functions.Function1<? super T,androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan>? span, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemScope,? super T,kotlin.Unit> itemContent);
+    method @androidx.compose.foundation.ExperimentalFoundationApi public static inline <T> void items(androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,?> contentType, optional kotlin.jvm.functions.Function1<? super T,androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan>? span, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemScope,? super T,kotlin.Unit> itemContent);
+    method @androidx.compose.foundation.ExperimentalFoundationApi public static inline <T> void itemsIndexed(androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan>? span, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method @androidx.compose.foundation.ExperimentalFoundationApi public static inline <T> void itemsIndexed(androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan>? span, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
   @androidx.compose.foundation.ExperimentalFoundationApi public sealed interface LazyStaggeredGridItemInfo {
@@ -933,6 +945,7 @@
   @androidx.compose.foundation.ExperimentalFoundationApi public sealed interface LazyStaggeredGridLayoutInfo {
     method public int getAfterContentPadding();
     method public int getBeforeContentPadding();
+    method public int getMainAxisItemSpacing();
     method public androidx.compose.foundation.gestures.Orientation getOrientation();
     method public int getTotalItemsCount();
     method public int getViewportEndOffset();
@@ -941,6 +954,7 @@
     method public java.util.List<androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemInfo> getVisibleItemsInfo();
     property public abstract int afterContentPadding;
     property public abstract int beforeContentPadding;
+    property public abstract int mainAxisItemSpacing;
     property public abstract androidx.compose.foundation.gestures.Orientation orientation;
     property public abstract int totalItemsCount;
     property public abstract int viewportEndOffset;
@@ -959,8 +973,8 @@
   }
 
   @androidx.compose.foundation.ExperimentalFoundationApi public sealed interface LazyStaggeredGridScope {
-    method @androidx.compose.foundation.ExperimentalFoundationApi public void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemScope,kotlin.Unit> content);
-    method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?> contentType, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+    method @androidx.compose.foundation.ExperimentalFoundationApi public void item(optional Object? key, optional Object? contentType, optional androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan? span, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemScope,kotlin.Unit> content);
+    method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?> contentType, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan>? span, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
   }
 
   public final class LazyStaggeredGridSemanticsKt {
@@ -1010,6 +1024,17 @@
     method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
   }
 
+  @androidx.compose.foundation.ExperimentalFoundationApi public final class StaggeredGridItemSpan {
+    field public static final androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan.Companion Companion;
+  }
+
+  public static final class StaggeredGridItemSpan.Companion {
+    method public androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan getFullLine();
+    method public androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan getSingleLane();
+    property public final androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan FullLine;
+    property public final androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan SingleLane;
+  }
+
 }
 
 package androidx.compose.foundation.pager {
@@ -1260,6 +1285,7 @@
 
   public final class ClickableTextKt {
     method @androidx.compose.runtime.Composable public static void ClickableText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional boolean softWrap, optional int overflow, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> onClick);
+    method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void ClickableText(androidx.compose.ui.text.AnnotatedString text, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> onHover, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional boolean softWrap, optional int overflow, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> onClick);
   }
 
   public final class ContextMenu_androidKt {
@@ -1350,6 +1376,9 @@
   public final class LongPressTextDragObserverKt {
   }
 
+  public final class PointerMoveDetectorKt {
+  }
+
   public final class StringHelpersKt {
   }
 
diff --git a/compose/foundation/foundation/api/restricted_current.ignore b/compose/foundation/foundation/api/restricted_current.ignore
index 90e6259..d1a4e31 100644
--- a/compose/foundation/foundation/api/restricted_current.ignore
+++ b/compose/foundation/foundation/api/restricted_current.ignore
@@ -1,4 +1,8 @@
 // Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.foundation.lazy.grid.LazyGridLayoutInfo#getMainAxisItemSpacing():
+    Added method androidx.compose.foundation.lazy.grid.LazyGridLayoutInfo.getMainAxisItemSpacing()
+
+
 RemovedClass: androidx.compose.foundation.lazy.LazyListPinningModifierKt:
     Removed class androidx.compose.foundation.lazy.LazyListPinningModifierKt
 RemovedClass: androidx.compose.foundation.lazy.layout.PinnableParentKt:
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index dae8b69..1c8281c 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -81,7 +81,7 @@
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Image(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int filterQuality);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Image(androidx.compose.ui.graphics.vector.ImageVector imageVector, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
     method @androidx.compose.runtime.Composable public static void Image(androidx.compose.ui.graphics.painter.Painter painter, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
-    method @Deprecated @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Image(androidx.compose.ui.graphics.ImageBitmap bitmap, String contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
+    method @Deprecated @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Image(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
   }
 
   @androidx.compose.runtime.Stable public interface Indication {
@@ -484,6 +484,7 @@
   @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListLayoutInfo {
     method public default int getAfterContentPadding();
     method public default int getBeforeContentPadding();
+    method public default int getMainAxisItemSpacing();
     method public default androidx.compose.foundation.gestures.Orientation getOrientation();
     method public default boolean getReverseLayout();
     method public int getTotalItemsCount();
@@ -493,6 +494,7 @@
     method public java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> getVisibleItemsInfo();
     property public default int afterContentPadding;
     property public default int beforeContentPadding;
+    property public default int mainAxisItemSpacing;
     property public default androidx.compose.foundation.gestures.Orientation orientation;
     property public default boolean reverseLayout;
     property public abstract int totalItemsCount;
@@ -505,9 +507,6 @@
   public final class LazyListMeasureKt {
   }
 
-  public final class LazyListPinnableContainerProviderKt {
-  }
-
   @androidx.compose.foundation.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
     method public default void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
     method @Deprecated public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,? extends kotlin.Unit> content);
@@ -627,6 +626,7 @@
   public sealed interface LazyGridLayoutInfo {
     method public int getAfterContentPadding();
     method public int getBeforeContentPadding();
+    method public int getMainAxisItemSpacing();
     method public androidx.compose.foundation.gestures.Orientation getOrientation();
     method public boolean getReverseLayout();
     method public int getTotalItemsCount();
@@ -636,6 +636,7 @@
     method public java.util.List<androidx.compose.foundation.lazy.grid.LazyGridItemInfo> getVisibleItemsInfo();
     property public abstract int afterContentPadding;
     property public abstract int beforeContentPadding;
+    property public abstract int mainAxisItemSpacing;
     property public abstract androidx.compose.foundation.gestures.Orientation orientation;
     property public abstract boolean reverseLayout;
     property public abstract int totalItemsCount;
@@ -709,6 +710,9 @@
   public final class LazyLayoutKt {
   }
 
+  public final class LazyLayoutPinnableItemKt {
+  }
+
   public final class LazyLayoutPrefetcher_androidKt {
   }
 
@@ -1014,6 +1018,9 @@
   public final class LongPressTextDragObserverKt {
   }
 
+  public final class PointerMoveDetectorKt {
+  }
+
   public final class StringHelpersKt {
   }
 
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 e8c54a3..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
@@ -161,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/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyStaggeredGridScrollingBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyStaggeredGridScrollingBenchmark.kt
index 41f3813..c8ad021 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyStaggeredGridScrollingBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyStaggeredGridScrollingBenchmark.kt
@@ -83,6 +83,20 @@
     }
 
     @Test
+    fun scrollProgrammatically_newItemComposed_up() {
+        benchmarkRule.toggleStateBenchmark {
+            StaggeredGridRemeasureTestCase(
+                firstItemIndex = 100,
+                scrollUp = true,
+                addNewItemOnToggle = true,
+                content = testCase.content,
+                isVertical = testCase.isVertical,
+                usePointerInput = false
+            )
+        }
+    }
+
+    @Test
     fun scrollViaPointerInput_noNewItems() {
         benchmarkRule.toggleStateBenchmark {
             StaggeredGridRemeasureTestCase(
@@ -95,6 +109,20 @@
     }
 
     @Test
+    fun scrollViaPointerInput_newItemComposed_up() {
+        benchmarkRule.toggleStateBenchmark {
+            StaggeredGridRemeasureTestCase(
+                firstItemIndex = 100,
+                scrollUp = true,
+                addNewItemOnToggle = true,
+                content = testCase.content,
+                isVertical = testCase.isVertical,
+                usePointerInput = true
+            )
+        }
+    }
+
+    @Test
     fun scrollViaPointerInput_newItemComposed() {
         benchmarkRule.toggleStateBenchmark {
             StaggeredGridRemeasureTestCase(
@@ -203,6 +231,8 @@
 
 @OptIn(ExperimentalFoundationApi::class)
 class StaggeredGridRemeasureTestCase(
+    val firstItemIndex: Int = 0,
+    val scrollUp: Boolean = false,
     val addNewItemOnToggle: Boolean,
     val content: @Composable StaggeredGridRemeasureTestCase.(LazyStaggeredGridState) -> Unit,
     val isVertical: Boolean,
@@ -216,6 +246,7 @@
     private lateinit var motionEventHelper: MotionEventHelper
     private var touchSlop: Float = 0f
     private var scrollBy: Int = 0
+    private var targetItemOffset = 0
 
     @Composable
     fun FirstLargeItem() {
@@ -223,12 +254,23 @@
     }
 
     @Composable
+    fun RegularItem() {
+        Box(Modifier.requiredSize(20.dp).background(Color.Red, RoundedCornerShape(8.dp)))
+    }
+
+    @Composable
     override fun Content() {
         scrollBy = if (addNewItemOnToggle) {
             with(LocalDensity.current) { 15.dp.roundToPx() }
         } else {
             5
+        } * if (scrollUp) -1 else 1
+        targetItemOffset = if (scrollUp) {
+            with(LocalDensity.current) { 20.dp.roundToPx() + scrollBy }
+        } else {
+            scrollBy
         }
+
         view = LocalView.current
         if (!::motionEventHelper.isInitialized) motionEventHelper = MotionEventHelper(view)
         touchSlop = LocalViewConfiguration.current.touchSlop
@@ -236,21 +278,16 @@
         content(state)
     }
 
-    @Composable
-    fun RegularItem() {
-        Box(Modifier.requiredSize(20.dp).background(Color.Red, RoundedCornerShape(8.dp)))
-    }
-
     override fun beforeToggle() {
         runBlocking {
-            state.scrollToItem(0, 0)
+            state.scrollToItem(firstItemIndex, 0)
         }
         if (usePointerInput) {
             val size = if (isVertical) view.measuredHeight else view.measuredWidth
             motionEventHelper.sendEvent(MotionEvent.ACTION_DOWN, (size / 2f).toSingleAxisOffset())
             motionEventHelper.sendEvent(MotionEvent.ACTION_MOVE, touchSlop.toSingleAxisOffset())
         }
-        assertEquals(0, state.firstVisibleItemIndex)
+        assertEquals(firstItemIndex, state.firstVisibleItemIndex)
         assertEquals(0, state.firstVisibleItemScrollOffset)
     }
 
@@ -266,8 +303,11 @@
     }
 
     override fun afterToggle() {
-        assertEquals(0, state.firstVisibleItemIndex)
-        assertEquals(scrollBy, state.firstVisibleItemScrollOffset)
+        assertEquals(
+            if (scrollUp) firstItemIndex - 2 else firstItemIndex,
+            state.firstVisibleItemIndex
+        )
+        assertEquals(targetItemOffset, state.firstVisibleItemScrollOffset)
         if (usePointerInput) {
             motionEventHelper.sendEvent(MotionEvent.ACTION_UP, Offset.Zero)
         }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
index 2c7a825..2a392bb 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
@@ -50,6 +50,7 @@
     "Foundation",
     listOf(
         ComposableDemo("Draggable, Scrollable, Zoomable, Focusable") { HighLevelGesturesDemo() },
+        ComposableDemo("Overscroll") { OverscrollDemo() },
         ComposableDemo("Can scroll forward / backward") { CanScrollSample() },
         ComposableDemo("Vertical scroll") { VerticalScrollExample() },
         ComposableDemo("Controlled Scrollable Row") { ControlledScrollableRowSample() },
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/HighLevelGesturesDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/HighLevelGesturesDemo.kt
index c0bab6f..d53f3d1 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/HighLevelGesturesDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/HighLevelGesturesDemo.kt
@@ -17,15 +17,12 @@
 package androidx.compose.foundation.demos
 
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.samples.DraggableSample
 import androidx.compose.foundation.samples.FocusableSample
 import androidx.compose.foundation.samples.HoverableSample
-import androidx.compose.foundation.samples.OverscrollSample
 import androidx.compose.foundation.samples.ScrollableSample
 import androidx.compose.foundation.samples.TransformableSample
 import androidx.compose.foundation.verticalScroll
@@ -40,11 +37,7 @@
     Column(Modifier.verticalScroll(rememberScrollState())) {
         DraggableSample()
         Spacer(Modifier.height(50.dp))
-        Row {
-            ScrollableSample()
-            Spacer(Modifier.width(30.dp))
-            OverscrollSample()
-        }
+        ScrollableSample()
         Spacer(Modifier.height(50.dp))
         TransformableSample()
         Spacer(Modifier.height(50.dp))
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
index 8972e35..7092564 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
@@ -59,6 +59,7 @@
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
 import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
+import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
 import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.samples.StickyHeaderSample
@@ -972,18 +973,26 @@
             Button(onClick = { if (count != 0) count-- }) { Text(text = "--") }
         }
 
-        val state = rememberLazyStaggeredGridState()
+        val state = rememberLazyStaggeredGridState(initialFirstVisibleItemIndex = 29)
 
         LazyVerticalStaggeredGrid(
             columns = StaggeredGridCells.Fixed(3),
             modifier = Modifier.fillMaxSize(),
             state = state,
-            contentPadding = PaddingValues(vertical = 500.dp, horizontal = 20.dp),
+            contentPadding = PaddingValues(vertical = 30.dp, horizontal = 20.dp),
             horizontalArrangement = Arrangement.spacedBy(10.dp),
             verticalArrangement = Arrangement.spacedBy(10.dp),
             content = {
-                items(count) {
-                    var expanded by rememberSaveable { mutableStateOf(false) }
+                items(
+                    count,
+                    span = {
+                        if (it % 30 == 0)
+                            StaggeredGridItemSpan.FullLine
+                        else
+                            StaggeredGridItemSpan.SingleLane
+                    }
+                ) {
+                    var expanded by remember { mutableStateOf(false) }
                     val index = indices.value[it % indices.value.size]
                     val color = colors[index]
                     Box(
@@ -1059,7 +1068,10 @@
                     Checkbox(checked = selected, onCheckedChange = {
                         selectedIndexes[item] = it
                     })
-                    Spacer(Modifier.width(16.dp).height(height))
+                    Spacer(
+                        Modifier
+                            .width(16.dp)
+                            .height(height))
                     Text("Item $item")
                 }
             }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/OverscrollDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/OverscrollDemo.kt
new file mode 100644
index 0000000..df7592a
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/OverscrollDemo.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.demos
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.samples.OverscrollWithDraggable_After
+import androidx.compose.foundation.samples.OverscrollWithDraggable_Before
+import androidx.compose.foundation.samples.OverscrollSample
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@Preview
+@Composable
+fun OverscrollDemo() {
+    Column(
+        Modifier.verticalScroll(rememberScrollState()),
+        horizontalAlignment = Alignment.CenterHorizontally
+    ) {
+        OverscrollSample()
+        Spacer(Modifier.height(50.dp))
+        OverscrollWithDraggable_Before()
+        Spacer(Modifier.height(50.dp))
+        OverscrollWithDraggable_After()
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextOverflownSelection.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextOverflownSelection.kt
new file mode 100644
index 0000000..3055a47
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextOverflownSelection.kt
@@ -0,0 +1,246 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.demos.text
+
+import android.content.ClipboardManager
+import android.content.Context.CLIPBOARD_SERVICE
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.selection.DisableSelection
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.material.LocalTextStyle
+import androidx.compose.material.RadioButton
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun TextOverflowedSelectionDemo() {
+    var overflow by remember { mutableStateOf(TextOverflow.Clip) }
+    val context = LocalContext.current
+    val clipboardManager = remember(context) {
+        context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
+    }
+    var copiedText by remember { mutableStateOf("") }
+
+    DisposableEffect(clipboardManager) {
+        val listener = ClipboardManager.OnPrimaryClipChangedListener {
+            copiedText = clipboardManager.read()
+        }
+        clipboardManager.addPrimaryClipChangedListener(listener)
+        onDispose {
+            clipboardManager.removePrimaryClipChangedListener(listener)
+        }
+    }
+
+    SelectionContainer {
+        Column {
+            Row(verticalAlignment = Alignment.CenterVertically) {
+                RadioButton(
+                    selected = overflow == TextOverflow.Clip,
+                    onClick = { overflow = TextOverflow.Clip })
+                Text(text = "Clip")
+                Spacer(modifier = Modifier.width(8.dp))
+                RadioButton(
+                    selected = overflow == TextOverflow.Ellipsis,
+                    onClick = { overflow = TextOverflow.Ellipsis })
+                Text(text = "Ellipsis")
+            }
+            DisableSelection {
+                Text(text = "Softwrap false, no maxLines")
+            }
+            OverflowToggleText(
+                text = loremIpsum(Language.Latin, wordCount = 50),
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .background(Color.Green),
+                softWrap = false,
+                overflow = overflow
+            )
+            DisableSelection {
+                Text(text = "Softwrap true, maxLines 1, in a row")
+            }
+            Row {
+                Box(modifier = Modifier.weight(1f), propagateMinConstraints = false) {
+                    OverflowToggleText(
+                        text = loremIpsum(Language.Latin, wordCount = 50),
+                        modifier = Modifier
+                            .background(Color.Green),
+                        overflow = overflow,
+                        maxLines = 1
+                    )
+                }
+                Box(modifier = Modifier.weight(1f), propagateMinConstraints = false) {
+                    OverflowToggleText(
+                        text = loremIpsum(Language.Latin, wordCount = 50),
+                        modifier = Modifier
+                            .background(Color.Green),
+                        overflow = overflow,
+                        maxLines = 1
+                    )
+                }
+            }
+            DisableSelection {
+                Text(text = "Softwrap true, height constrained, in a row")
+            }
+            Row {
+                Box(modifier = Modifier.weight(1f), propagateMinConstraints = false) {
+                    OverflowToggleText(
+                        text = loremIpsum(Language.Latin, wordCount = 50),
+                        modifier = Modifier
+                            .background(Color.Green)
+                            .heightIn(max = 36.dp),
+                        overflow = overflow
+                    )
+                }
+                Box(modifier = Modifier.weight(1f), propagateMinConstraints = false) {
+                    OverflowToggleText(
+                        text = loremIpsum(Language.Latin, wordCount = 50),
+                        modifier = Modifier
+                            .background(Color.Green)
+                            .heightIn(max = 36.dp),
+                        overflow = overflow
+                    )
+                }
+            }
+            DisableSelection {
+                Text(text = "Softwrap true, maxLines 1, half width")
+            }
+            OverflowToggleText(
+                text = loremIpsum(Language.Latin, wordCount = 50),
+                modifier = Modifier
+                    .background(Color.Green)
+                    .fillMaxWidth(0.5f),
+                overflow = overflow,
+                maxLines = 1
+            )
+            DisableSelection {
+                Text(text = "Softwrap true, maxLines 1")
+            }
+            OverflowToggleText(
+                text = loremIpsum(Language.Latin, wordCount = 50),
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .background(Color.Red),
+                maxLines = 1,
+                overflow = overflow
+            )
+
+            DisableSelection {
+                Text(
+                    text = "BiDi, softwrap true, maxLines 1",
+                    modifier = Modifier.padding(top = 16.dp)
+                )
+            }
+            OverflowToggleText(
+                text = loremIpsum(
+                    Language.Latin,
+                    wordCount = 3
+                ) + loremIpsum(Language.Arabic, wordCount = 20),
+                modifier = Modifier
+                    .fillMaxWidth(),
+                maxLines = 1,
+                overflow = overflow
+            )
+
+            DisableSelection {
+                Text(text = "Copied Text", modifier = Modifier.padding(top = 16.dp))
+            }
+            TextField(value = copiedText, onValueChange = {}, modifier = Modifier.fillMaxWidth())
+        }
+    }
+}
+
+fun ClipboardManager.read(): String {
+    return primaryClip?.getItemAt(0)?.text.toString()
+}
+
+@Composable
+private fun OverflowToggleText(
+    text: String,
+    modifier: Modifier = Modifier,
+    color: Color = Color.Unspecified,
+    fontSize: TextUnit = TextUnit.Unspecified,
+    fontStyle: FontStyle? = null,
+    fontWeight: FontWeight? = null,
+    fontFamily: FontFamily? = null,
+    letterSpacing: TextUnit = TextUnit.Unspecified,
+    textDecoration: TextDecoration? = null,
+    textAlign: TextAlign? = null,
+    lineHeight: TextUnit = TextUnit.Unspecified,
+    overflow: TextOverflow = TextOverflow.Clip,
+    softWrap: Boolean = true,
+    maxLines: Int = Int.MAX_VALUE,
+    minLines: Int = 1,
+    onTextLayout: (TextLayoutResult) -> Unit = {},
+    style: TextStyle = LocalTextStyle.current
+) {
+    var toggleOverflow by remember(overflow) { mutableStateOf(overflow) }
+    Text(
+        text = text,
+        modifier = modifier.clickable {
+            toggleOverflow = when (toggleOverflow) {
+                TextOverflow.Clip -> TextOverflow.Ellipsis
+                TextOverflow.Ellipsis -> TextOverflow.Visible
+                TextOverflow.Visible -> TextOverflow.Clip
+                else -> TextOverflow.Clip
+            }
+        },
+        color = color,
+        fontSize = fontSize,
+        fontStyle = fontStyle,
+        fontWeight = fontWeight,
+        fontFamily = fontFamily,
+        letterSpacing = letterSpacing,
+        textDecoration = textDecoration,
+        textAlign = textAlign,
+        lineHeight = lineHeight,
+        overflow = toggleOverflow,
+        softWrap = if (toggleOverflow == TextOverflow.Visible) true else softWrap,
+        maxLines = if (toggleOverflow == TextOverflow.Visible) Int.MAX_VALUE else maxLines,
+        minLines = minLines,
+        onTextLayout = onTextLayout,
+        style = style
+    )
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/InteractiveText.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/InteractiveText.kt
index a730aef..b671f6b 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/InteractiveText.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/InteractiveText.kt
@@ -16,31 +16,59 @@
 
 package androidx.compose.foundation.demos.text
 
-import androidx.compose.foundation.text.ClickableText
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.ClickableText
+import androidx.compose.material.Button
+import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
 
-@Preview
+@OptIn(ExperimentalFoundationApi::class)
 @Composable
 fun InteractiveTextDemo() {
-    TextOnClick()
-}
+    val clickedOffset = remember { mutableStateOf<Int?>(null) }
+    val hoveredOffset = remember { mutableStateOf<Int?>(null) }
+    val numOnHoverInvocations = remember { mutableStateOf(0) }
+    Column(
+        modifier = Modifier.padding(horizontal = 10.dp)
+    ) {
+        Text(text = "ClickableText onHover", style = MaterialTheme.typography.h6)
 
-@Preview
-@Composable
-fun TextOnClick() {
-    val clickedOffset = remember { mutableStateOf(-1) }
-    Column {
-        Text("Clicked Offset: ${clickedOffset.value}")
+        Text(text = "Click/Hover the lorem ipsum text below.")
+        Text(text = "Clicked offset: ${clickedOffset.value ?: "No click yet"}")
+        Text(text = "Hovered offset: ${hoveredOffset.value ?: "Not hovering"}")
+        Text(text = "Number of onHover invocations: ${numOnHoverInvocations.value}")
+
         ClickableText(
-            text = AnnotatedString("Click Me")
+            text = AnnotatedString(loremIpsum(wordCount = 30)),
+            modifier = Modifier.border(Dp.Hairline, Color.Black),
+            style = MaterialTheme.typography.body1,
+            onHover = {
+                numOnHoverInvocations.value = numOnHoverInvocations.value + 1
+                hoveredOffset.value = it
+            }
         ) { offset ->
             clickedOffset.value = offset
         }
+
+        Button(
+            onClick = {
+                clickedOffset.value = null
+                hoveredOffset.value = null
+                numOnHoverInvocations.value = 0
+            }
+        ) {
+            Text(text = "Reset Offsets/Counter")
+        }
     }
-}
\ No newline at end of file
+}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LetterSpacingDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LetterSpacingDemo.kt
new file mode 100644
index 0000000..654faea
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LetterSpacingDemo.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.demos.text
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.LocalTextStyle
+import androidx.compose.material.Slider
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+private const val text = "The quick brown fox jumps over the lazy dog"
+@Preview
+@Composable
+fun LetterSpacingDemo() {
+    Column(
+        Modifier.padding(horizontal = 16.dp)
+    ) {
+        var letterSpacing: Float by remember { mutableStateOf(0.0f) }
+        var fontSize: Float by remember { mutableStateOf(12f) }
+
+        Text("LetterSpacing: ${letterSpacing.toString().take(4)}.sp")
+        Slider(
+            value = letterSpacing,
+            onValueChange = { letterSpacing = it },
+            valueRange = -100f..100f,
+        )
+        Text("fontSize: ${fontSize.toString().take(4)}.sp")
+        Slider(
+            value = fontSize,
+            onValueChange = { fontSize = it },
+            valueRange = 5f..100f
+        )
+        AnnotatedText(letterSpacing, fontSize)
+    }
+}
+
+@Composable
+fun AnnotatedText(letterSpacing: Float, fontSize: Float) {
+    var textLayoutResult: TextLayoutResult? by remember { mutableStateOf(null) }
+    Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
+        Text(text,
+            modifier = Modifier
+                .fillMaxWidth(0.5f) /* only half the screen, to allow negative em */
+                .drawTextMetrics(textLayoutResult, null),
+            style = LocalTextStyle.current.copy(
+                letterSpacing = letterSpacing.sp,
+                fontSize = fontSize.sp),
+            onTextLayout = { textLayoutResult = it }
+        )
+    }
+}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LoremIpsum.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LoremIpsum.kt
index b28fb70..cf1da10 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LoremIpsum.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LoremIpsum.kt
@@ -28,7 +28,7 @@
     language: Language = Language.Latin,
     wordCount: Int = language.words.size
 ): String =
-    language.words.joinToString(separator = " ", limit = wordCount)
+    language.words.joinToString(separator = " ", limit = wordCount, truncated = "")
 
 private val LatinLipsum = """
     Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque a egestas nisi. Aenean
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
new file mode 100644
index 0000000..b191349a
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/MemoryAllocs.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.demos.text
+
+import 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
+ * pressure.
+ *
+ * Emulate recompose when string loads
+ */
+@Composable
+fun MemoryAllocsSetText() {
+    Column {
+        Preamble("""
+            @Composable
+            fun SetText(text: State<String>) {
+                Text(text.value)
+            }""".trimIndent()
+        )
+        SetText(textToggler())
+    }
+}
+
+/**
+ * These demos are for using the memory profiler to observe initial compo and recompo memory
+ * pressure.
+ *
+ * Emulate calling text when string loads
+ */
+@Composable
+fun MemoryAllocsIfNotEmptyText() {
+    Column {
+        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)
+    }
+}
+
+@Composable
+private fun SetText(text: State<String>) {
+    Text(text.value)
+}
+
+@Composable
+private fun textToggler(): State<String> = produceState("") {
+    while (true) {
+        withFrameMillis {
+            value = if (value.isEmpty()) {
+                "This text and empty string swap every frame"
+            } else {
+                ""
+            }
+        }
+    }
+}
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 7f26faf..836cda4 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
@@ -44,6 +44,9 @@
                         ComposableDemo("Ellipsize and letterspacing") {
                             EllipsizeWithLetterSpacing()
                         },
+                        ComposableDemo("Letterspacing") {
+                            LetterSpacingDemo()
+                        }
                     )
                 ),
                 DemoCategory(
@@ -114,6 +117,14 @@
             listOf(
                 ComposableDemo("Text selection") { TextSelectionDemo() },
                 ComposableDemo("Text selection sample") { TextSelectionSample() },
+                ComposableDemo("Overflowed Selection") { TextOverflowedSelectionDemo() },
+            )
+        ),
+        DemoCategory(
+            "\uD83D\uDD75️️️ Memory allocs",
+            listOf(
+                ComposableDemo("\uD83D\uDD75️ SetText") { MemoryAllocsSetText() },
+                ComposableDemo("\uD83D\uDD75️ IfNotEmptyText") { MemoryAllocsIfNotEmptyText() }
             )
         )
     )
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt
index 072596d..1a1e3f6 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt
@@ -23,6 +23,9 @@
 import androidx.compose.foundation.OverscrollEffect
 import androidx.compose.foundation.background
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollableDefaults
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
 import androidx.compose.foundation.gestures.rememberScrollableState
 import androidx.compose.foundation.gestures.scrollable
 import androidx.compose.foundation.layout.Box
@@ -61,37 +64,35 @@
     // on the scrollable container.
     class OffsetOverscrollEffect(val scope: CoroutineScope) : OverscrollEffect {
         private val overscrollOffset = Animatable(0f)
-        override fun consumePreScroll(
-            scrollDelta: Offset,
-            source: NestedScrollSource
+
+        override fun applyToScroll(
+            delta: Offset,
+            source: NestedScrollSource,
+            performScroll: (Offset) -> Offset
         ): Offset {
             // in pre scroll we relax the overscroll if needed
             // relaxation: when we are in progress of the overscroll and user scrolls in the
             // different direction = substract the overscroll first
-            val sameDirection = sign(scrollDelta.y) == sign(overscrollOffset.value)
-            return if (abs(overscrollOffset.value) > 0.5 && !sameDirection) {
+            val sameDirection = sign(delta.y) == sign(overscrollOffset.value)
+            val consumedByPreScroll = if (abs(overscrollOffset.value) > 0.5 && !sameDirection) {
                 val prevOverscrollValue = overscrollOffset.value
-                val newOverscrollValue = overscrollOffset.value + scrollDelta.y
+                val newOverscrollValue = overscrollOffset.value + delta.y
                 if (sign(prevOverscrollValue) != sign(newOverscrollValue)) {
                     // sign changed, coerce to start scrolling and exit
                     scope.launch { overscrollOffset.snapTo(0f) }
-                    Offset(x = 0f, y = scrollDelta.y + prevOverscrollValue)
+                    Offset(x = 0f, y = delta.y + prevOverscrollValue)
                 } else {
                     scope.launch {
-                        overscrollOffset.snapTo(overscrollOffset.value + scrollDelta.y)
+                        overscrollOffset.snapTo(overscrollOffset.value + delta.y)
                     }
-                    scrollDelta.copy(x = 0f)
+                    delta.copy(x = 0f)
                 }
             } else {
                 Offset.Zero
             }
-        }
-
-        override fun consumePostScroll(
-            initialDragDelta: Offset,
-            overscrollDelta: Offset,
-            source: NestedScrollSource
-        ) {
+            val leftForScroll = delta - consumedByPreScroll
+            val consumedByScroll = performScroll(leftForScroll)
+            val overscrollDelta = leftForScroll - consumedByScroll
             // if it is a drag, not a fling, add the delta left to our over scroll value
             if (abs(overscrollDelta.y) > 0.5 && source == NestedScrollSource.Drag) {
                 scope.launch {
@@ -99,21 +100,25 @@
                     overscrollOffset.snapTo(overscrollOffset.value + overscrollDelta.y * 0.1f)
                 }
             }
+            return consumedByPreScroll + consumedByScroll
         }
 
-        override suspend fun consumePreFling(velocity: Velocity): Velocity = Velocity.Zero
-
-        override suspend fun consumePostFling(velocity: Velocity) {
+        override suspend fun applyToFling(
+            velocity: Velocity,
+            performFling: suspend (Velocity) -> Velocity
+        ) {
+            val consumed = performFling(velocity)
             // when the fling happens - we just gradually animate our overscroll to 0
+            val remaining = velocity - consumed
             overscrollOffset.animateTo(
                 targetValue = 0f,
-                initialVelocity = velocity.y,
+                initialVelocity = remaining.y,
                 animationSpec = spring()
             )
         }
 
         override val isInProgress: Boolean
-            get() = overscrollOffset.isRunning
+            get() = overscrollOffset.value != 0f
 
         // as we're building an offset modifiers, let's offset of our value we calculated
         override val effectModifier: Modifier = Modifier.offset {
@@ -154,4 +159,80 @@
                 .overscroll(overscroll)
         )
     }
-}
\ No newline at end of file
+}
+
+@Sampled
+@Composable
+fun OverscrollWithDraggable_Before() {
+    var dragPosition by remember { mutableStateOf(0f) }
+    val minPosition = -1000f
+    val maxPosition = 1000f
+
+    val draggableState = rememberDraggableState { delta ->
+        val newPosition = (dragPosition + delta).coerceIn(minPosition, maxPosition)
+        dragPosition = newPosition
+    }
+
+    Box(
+        Modifier
+            .size(100.dp)
+            .draggable(draggableState, orientation = Orientation.Horizontal),
+        contentAlignment = Alignment.Center
+    ) {
+        Text("Drag position $dragPosition")
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Sampled
+@Composable
+fun OverscrollWithDraggable_After() {
+    var dragPosition by remember { mutableStateOf(0f) }
+    val minPosition = -1000f
+    val maxPosition = 1000f
+
+    val overscrollEffect = ScrollableDefaults.overscrollEffect()
+
+    val draggableState = rememberDraggableState { delta ->
+        // Horizontal, so convert the delta to a horizontal offset
+        val deltaAsOffset = Offset(delta, 0f)
+        // Wrap the original logic inside applyToScroll
+        overscrollEffect.applyToScroll(deltaAsOffset, NestedScrollSource.Drag) { remainingOffset ->
+            val remainingDelta = remainingOffset.x
+            val newPosition = (dragPosition + remainingDelta).coerceIn(minPosition, maxPosition)
+            // Calculate how much delta we have consumed
+            val consumed = newPosition - dragPosition
+            dragPosition = newPosition
+            // Return how much offset we consumed, so that we can show overscroll for what is left
+            Offset(consumed, 0f)
+        }
+    }
+
+    Box(
+        Modifier
+            // Draw overscroll on the box
+            .overscroll(overscrollEffect)
+            .size(100.dp)
+            .draggable(
+                draggableState,
+                orientation = Orientation.Horizontal,
+                onDragStopped = {
+                    overscrollEffect.applyToFling(Velocity(it, 0f)) { velocity ->
+                        if (dragPosition == minPosition || dragPosition == maxPosition) {
+                            // If we are at the min / max bound, give overscroll all of the velocity
+                            Velocity.Zero
+                        } else {
+                            // If we aren't at the min / max bound, consume all of the velocity so
+                            // overscroll won't show. Normally in this case something like
+                            // Modifier.scrollable would use the velocity to update the scroll state
+                            // with a fling animation, but just do nothing to keep this simpler.
+                            velocity
+                        }
+                    }
+                }
+            ),
+        contentAlignment = Alignment.Center
+    ) {
+        Text("Drag position $dragPosition")
+    }
+}
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/TransformableSample.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/TransformableSample.kt
index 51d0537..b0f117c 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/TransformableSample.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/TransformableSample.kt
@@ -44,14 +44,18 @@
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
-import kotlinx.coroutines.launch
+import kotlin.math.max
 import kotlin.math.roundToInt
+import kotlinx.coroutines.launch
 
 @Sampled
 @Composable
 fun TransformableSample() {
     Box(
-        Modifier.size(200.dp).clipToBounds().background(Color.LightGray)
+        Modifier
+            .size(200.dp)
+            .clipToBounds()
+            .background(Color.LightGray)
     ) {
         // set up all transformation states
         var scale by remember { mutableStateOf(1f) }
@@ -61,7 +65,8 @@
         // let's create a modifier state to specify how to update our UI state defined above
         val state = rememberTransformableState { zoomChange, offsetChange, rotationChange ->
             // note: scale goes by factor, not an absolute difference, so we need to multiply it
-            scale *= zoomChange
+            // for this example, we don't allow downscaling, so cap it to 1f
+            scale = max(scale * zoomChange, 1f)
             rotation += rotationChange
             offset += offsetChange
         }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt
index e1837d2..7a60556 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt
@@ -33,6 +33,8 @@
 import androidx.compose.ui.test.getUnclippedBoundsInRoot
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import org.junit.Rule
 
@@ -119,6 +121,15 @@
             assertTopPositionInRootIsEqualTo(expectedStart)
         }
 
+    fun SemanticsNodeInteraction.assertAxisBounds(
+        offset: DpOffset,
+        size: DpSize
+    ) =
+        assertMainAxisStartPositionInRootIsEqualTo(offset.y)
+            .assertCrossAxisStartPositionInRootIsEqualTo(offset.x)
+            .assertMainAxisSizeIsEqualTo(size.height)
+            .assertCrossAxisSizeIsEqualTo(size.width)
+
     fun PaddingValues(
         mainAxis: Dp = 0.dp,
         crossAxis: Dp = 0.dp
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt
index 5992475..37cd6eb 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt
@@ -32,6 +32,7 @@
 import androidx.compose.testutils.assertDoesNotContainColor
 import androidx.compose.testutils.assertPixels
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.MotionDurationScale
 import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.focus.FocusManager
 import androidx.compose.ui.focus.FocusRequester
@@ -41,6 +42,7 @@
 import androidx.compose.ui.graphics.toPixelMap
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onRoot
@@ -50,7 +52,6 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
-import androidx.testutils.AnimationDurationScaleRule
 import com.google.common.truth.Truth.assertThat
 import kotlin.math.roundToInt
 import org.junit.Before
@@ -71,12 +72,13 @@
         private val BackgroundColor = Color.White
     }
 
-    @get:Rule
-    val rule = createComposeRule()
+    private val motionDurationScale = object : MotionDurationScale {
+        override var scaleFactor: Float by mutableStateOf(1f)
+    }
 
+    @OptIn(ExperimentalTestApi::class)
     @get:Rule
-    val animationScaleRule: AnimationDurationScaleRule =
-        AnimationDurationScaleRule.createForAllTests(1f)
+    val rule = createComposeRule(effectContext = motionDurationScale)
 
     /**
      * Converts pxPerFrame to dps per second. The frame delay is 16ms, which means there are
@@ -103,7 +105,7 @@
     }
 
     @Test
-    fun animationDisabled() {
+    fun doesNotAnimate_whenZeroIterations() {
         rule.setContent {
             TestMarqueeContent(
                 Modifier.basicMarqueeWithTestParams(
@@ -121,6 +123,63 @@
         }
     }
 
+    @Suppress("UnnecessaryOptInAnnotation")
+    @OptIn(ExperimentalTestApi::class)
+    @Test fun animates_whenAnimationsDisabledBySystem() {
+        motionDurationScale.scaleFactor = 0f
+
+        rule.setContent {
+            TestMarqueeContent(
+                Modifier.basicMarqueeWithTestParams(
+                    iterations = 1,
+                    spacing = MarqueeSpacing(0.toDp())
+                )
+            )
+        }
+
+        rule.onRoot().captureToImage()
+            .assertPixels(expectedSize = IntSize(100, 100)) { Color1 }
+
+        // First stage of animation: show all the content.
+        repeat(30) { frameNum ->
+            rule.mainClock.advanceTimeByFrame()
+            rule.waitForIdle()
+            val image = rule.onRoot().captureToImage()
+            val edge1 = image.findFirstColorEdge(Color1, Color2)
+            val edge2 = image.findFirstColorEdge(Color2, Color1)
+            val expectedEdge1 = 100 - (frameNum * 10)
+            val expectedEdge2 = 100 - ((frameNum - 10) * 10)
+
+            when {
+                frameNum == 0 -> {
+                    assertThat(edge1).isEqualTo(-1)
+                    assertThat(edge2).isEqualTo(-1)
+                }
+
+                frameNum < 10 -> {
+                    assertThat(edge1).isEqualTo(expectedEdge1)
+                    assertThat(edge2).isEqualTo(-1)
+                }
+
+                frameNum == 10 -> {
+                    assertThat(edge1).isEqualTo(-1)
+                    assertThat(edge2).isEqualTo(-1)
+                }
+
+                frameNum < 20 -> {
+                    assertThat(edge1).isEqualTo(-1)
+                    assertThat(edge2).isEqualTo(expectedEdge2)
+                }
+
+                else -> {
+                    // Nothing should happen after the animation finishes.
+                    assertThat(edge1).isEqualTo(-1)
+                    assertThat(edge2).isEqualTo(-1)
+                }
+            }
+        }
+    }
+
     @Test
     fun animates_singleIteration_noSpace() {
         rule.setContent {
@@ -1076,7 +1135,6 @@
                 edgeStartX = x
             } else if (pixel == right) {
                 return if (edgeStartX >= 0) {
-                    println("OMG found edge: $edgeStartX - $x")
                     ((edgeStartX.toFloat() + x.toFloat()) / 2f).roundToInt()
                 } else {
                     // Never found the start of the edge.
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt
index 2564e4b..a1d1b2f 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation
 
+import android.os.Build
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.gestures.draggable
@@ -209,6 +210,10 @@
     @OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun clickableTest_clickWithEnterKey() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         var counter = 0
         val focusRequester = FocusRequester()
@@ -237,6 +242,10 @@
     @OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun clickableTest_clickWithNumPadEnterKey() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         var counter = 0
         val focusRequester = FocusRequester()
@@ -265,6 +274,10 @@
     @OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun clickableTest_clickWithDPadCenter() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         var counter = 0
         val focusRequester = FocusRequester()
@@ -1277,6 +1290,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun clickableTest_interactionSource_focus_inKeyboardMode() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         lateinit var scope: CoroutineScope
@@ -1926,6 +1943,10 @@
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun clickableTest_enterKey_emitsIndication() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
@@ -1972,6 +1993,10 @@
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun clickableTest_numPadEnterKey_emitsIndication() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
@@ -2018,6 +2043,10 @@
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun clickableTest_dpadCenter_emitsIndication() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
@@ -2100,6 +2129,10 @@
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun clickableTest_doubleEnterKey_emitsFurtherInteractions() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
@@ -2160,6 +2193,10 @@
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun clickableTest_repeatKeyEvents_doNotEmitFurtherInteractions() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
@@ -2219,6 +2256,10 @@
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun clickableTest_interruptedClick_emitsCancelIndication() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
index c205a4a..9b004e2 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
@@ -366,11 +366,10 @@
 
         rule.runOnIdle {
             val offset = Offset(0f, 5f)
-            controller.consumePostScroll(
-                initialDragDelta = offset,
-                overscrollDelta = offset,
+            controller.applyToScroll(
+                offset,
                 source = NestedScrollSource.Drag
-            )
+            ) { Offset.Zero }
             // we have to disable further invalidation requests as otherwise while the overscroll
             // effect is considered active (as it is in a pulled state) this will infinitely
             // schedule next invalidation right from the drawing. this will make our test infra
@@ -406,17 +405,23 @@
         rule.runOnIdle {
             repeat(2) {
                 val offset = Offset(-10f, -10f)
-                assertThat(
-                    effect.consumePreScroll(offset, NestedScrollSource.Drag)
-                ).isEqualTo(Offset.Zero)
-                effect.consumePostScroll(offset, offset, NestedScrollSource.Drag)
+                var offsetConsumed: Offset? = null
+
+                effect.applyToScroll(offset, NestedScrollSource.Drag) {
+                    offsetConsumed = offset - it
+                    Offset.Zero
+                }
+                assertThat(offsetConsumed).isEqualTo(Offset.Zero)
             }
             val velocity = Velocity(-5f, -5f)
             runBlocking {
-                assertThat(
-                    effect.consumePreFling(velocity)
-                ).isEqualTo(Velocity.Zero)
-                effect.consumePostFling(velocity)
+                var velocityConsumed: Velocity? = null
+
+                effect.applyToFling(velocity) {
+                    velocityConsumed = velocity - it
+                    Velocity.Zero
+                }
+                assertThat(velocityConsumed!!).isEqualTo(Velocity.Zero)
             }
         }
     }
@@ -433,18 +438,24 @@
         rule.runOnIdle {
             repeat(2) {
                 val offset = Offset(0f, 10f)
-                assertThat(
-                    effect.consumePreScroll(offset, NestedScrollSource.Drag)
-                ).isEqualTo(Offset.Zero)
-                effect.consumePostScroll(offset, offset, NestedScrollSource.Drag)
+                var offsetConsumed: Offset? = null
+
+                effect.applyToScroll(offset, NestedScrollSource.Drag) {
+                    offsetConsumed = offset - it
+                    Offset.Zero
+                }
+                assertThat(offsetConsumed).isEqualTo(Offset.Zero)
             }
 
             val velocity = Velocity(0f, 5f)
             runBlocking {
-                assertThat(
-                    effect.consumePreFling(velocity)
-                ).isEqualTo(Velocity.Zero)
-                effect.consumePostFling(velocity)
+                var velocityConsumed: Velocity? = null
+
+                effect.applyToFling(velocity) {
+                    velocityConsumed = velocity - it
+                    Velocity.Zero
+                }
+                assertThat(velocityConsumed!!).isEqualTo(Velocity.Zero)
             }
         }
     }
@@ -648,32 +659,32 @@
 
         var preFlingVelocity = Velocity.Zero
 
-        override fun consumePreScroll(
-            scrollDelta: Offset,
-            source: NestedScrollSource
+        override fun applyToScroll(
+            delta: Offset,
+            source: NestedScrollSource,
+            performScroll: (Offset) -> Offset
         ): Offset {
-            lastPreScrollDelta = scrollDelta
+            lastPreScrollDelta = delta
             preScrollSource = source
 
-            return if (consumePreCycles) scrollDelta / 10f else Offset.Zero
-        }
+            val consumed = if (consumePreCycles) delta / 10f else Offset.Zero
 
-        override fun consumePostScroll(
-            initialDragDelta: Offset,
-            overscrollDelta: Offset,
-            source: NestedScrollSource
-        ) {
-            lastInitialDragDelta = initialDragDelta
-            lastOverscrollDelta = overscrollDelta
+            val consumedByScroll = performScroll(delta - consumed)
+
+            lastInitialDragDelta = delta
+            lastOverscrollDelta = delta - consumedByScroll - consumed
             lastNestedScrollSource = source
+
+            return delta - lastOverscrollDelta
         }
 
-        override suspend fun consumePreFling(velocity: Velocity): Velocity {
+        override suspend fun applyToFling(
+            velocity: Velocity,
+            performFling: suspend (Velocity) -> Velocity
+        ) {
             preFlingVelocity = velocity
-            return if (consumePreCycles) velocity / 10f else Velocity.Zero
-        }
-
-        override suspend fun consumePostFling(velocity: Velocity) {
+            val consumed = if (consumePreCycles) velocity / 10f else Velocity.Zero
+            performFling(velocity - consumed)
             lastVelocity = velocity
         }
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
index 5baa306..2345c6e 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
@@ -2552,31 +2552,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
-private val NoOpOverscrollEffect = object : OverscrollEffect {
-
-    override fun consumePreScroll(
-        scrollDelta: Offset,
-        source: NestedScrollSource
-    ): Offset = Offset.Zero
-
-    override fun consumePostScroll(
-        initialDragDelta: Offset,
-        overscrollDelta: Offset,
-        source: NestedScrollSource
-    ) {
-    }
-
-    override suspend fun consumePreFling(velocity: Velocity): Velocity = Velocity.Zero
-
-    override suspend fun consumePostFling(velocity: Velocity) {}
-
-    override val isInProgress: Boolean
-        get() = false
-
-    override val effectModifier: Modifier get() = Modifier
-}
-
 // Very low tolerance on the difference
 internal val VelocityTrackerCalculationThreshold = 1
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/StretchOverscrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/StretchOverscrollTest.kt
index 288ab0b..76e5125 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/StretchOverscrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/StretchOverscrollTest.kt
@@ -17,15 +17,19 @@
 package androidx.compose.foundation
 
 import android.os.Build
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.ScrollableDefaults
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.scrollable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.WithTouchSlop
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalViewConfiguration
-import androidx.compose.ui.platform.ViewConfiguration
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -36,11 +40,11 @@
 import androidx.test.filters.SdkSuppress
 import androidx.testutils.AnimationDurationScaleRule
 import com.google.common.truth.Truth.assertThat
-import kotlin.math.abs
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalFoundationApi::class)
 @MediumTest
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
 @RunWith(AndroidJUnit4::class)
@@ -53,39 +57,213 @@
         AnimationDurationScaleRule.createForAllTests(1f)
 
     @Test
-    fun stretchOverscroll_whenPulled_consumesOppositePreScroll() {
-        val color = listOf(Color.Red, Color.Yellow, Color.Blue, Color.Green)
-        val lazyState = LazyListState()
-        animationScaleRule.setAnimationDurationScale(1f)
-        var viewConfiguration: ViewConfiguration? = null
-        rule.setContent {
-            viewConfiguration = LocalViewConfiguration.current
-            LazyRow(
-                state = lazyState,
-                modifier = Modifier.size(300.dp).testTag(OverscrollBox)
-            ) {
-                items(10) { index ->
-                    Box(Modifier.size(50.dp, 300.dp).background(color[index % color.size]))
-                }
-            }
-        }
+    fun stretchOverscroll_whenPulled_consumesOppositePreScroll_pullLeft() {
+        val state = setStretchOverscrollContent(Orientation.Horizontal)
 
         rule.onNodeWithTag(OverscrollBox).performTouchInput {
             down(center)
-            moveBy(Offset(-(200f + (viewConfiguration?.touchSlop ?: 0f)), 0f))
-            // pull in the opposite direction. Since we pulled overscroll with positive delta
-            // it will consume negative delta before scroll happens
-            // assert in the ScrollableState lambda will check it
+            // Stretch by 200
+            moveBy(Offset(-200f, 0f))
+            // Pull 200 in the opposite direction - because we had 200 pixels of stretch before,
+            // this should only relax the existing overscroll, and not dispatch anything to the
+            // state
             moveBy(Offset(200f, 0f))
-            up()
         }
 
         rule.runOnIdle {
-            // no scroll happened as it was consumed by the overscroll logic
-            assertThat(abs(lazyState.firstVisibleItemScrollOffset)).isLessThan(2) // round error
-            assertThat(lazyState.firstVisibleItemIndex).isEqualTo(0)
+            // All 200 should have been consumed by overscroll that was relaxing the existing
+            // stretch
+            assertThat(state.scrollPosition).isEqualTo(0f)
         }
     }
+
+    @Test
+    fun stretchOverscroll_whenPulledWithSmallDelta_doesNotConsumesOppositePreScroll_pullLeft() {
+        val state = setStretchOverscrollContent(Orientation.Horizontal)
+
+        rule.onNodeWithTag(OverscrollBox).performTouchInput {
+            down(center)
+            // Try and stretch by 0.4f
+            moveBy(Offset(-0.4f, 0f))
+            // Pull 200 in the opposite direction - overscroll should have ignored the 0.4f, and
+            // so all 200 should be dispatched to the state with nothing being consumed
+            moveBy(Offset(200f, 0f))
+        }
+
+        rule.runOnIdle {
+            // All 200 should be dispatched directly to the state
+            assertThat(state.scrollPosition).isEqualTo(200f)
+        }
+    }
+
+    @Test
+    fun stretchOverscroll_whenPulled_consumesOppositePreScroll_pullTop() {
+        val state = setStretchOverscrollContent(Orientation.Vertical)
+
+        rule.onNodeWithTag(OverscrollBox).performTouchInput {
+            down(center)
+            // Stretch by 200
+            moveBy(Offset(0f, -200f))
+            // Pull 200 in the opposite direction - because we had 200 pixels of stretch before,
+            // this should only relax the existing overscroll, and not dispatch anything to the
+            // state
+            moveBy(Offset(0f, 200f))
+        }
+
+        rule.runOnIdle {
+            // All 200 should have been consumed by overscroll that was relaxing the existing
+            // stretch
+            assertThat(state.scrollPosition).isEqualTo(0f)
+        }
+    }
+
+    @Test
+    fun stretchOverscroll_whenPulledWithSmallDelta_doesNotConsumesOppositePreScroll_pullTop() {
+        val state = setStretchOverscrollContent(Orientation.Vertical)
+
+        rule.onNodeWithTag(OverscrollBox).performTouchInput {
+            down(center)
+            // Try and stretch by 0.4f
+            moveBy(Offset(0f, -0.4f))
+            // Pull 200 in the opposite direction - overscroll should have ignored the 0.4f, and
+            // so all 200 should be dispatched to the state with nothing being consumed
+            moveBy(Offset(0f, 200f))
+        }
+
+        rule.runOnIdle {
+            // All 200 should be dispatched directly to the state
+            assertThat(state.scrollPosition).isEqualTo(200f)
+        }
+    }
+
+    @Test
+    fun stretchOverscroll_whenPulled_consumesOppositePreScroll_pullRight() {
+        val state = setStretchOverscrollContent(Orientation.Horizontal)
+
+        rule.onNodeWithTag(OverscrollBox).performTouchInput {
+            down(center)
+            // Stretch by 200 (the max scroll value is 1000)
+            moveBy(Offset(1200f, 0f))
+            // Pull 200 in the opposite direction - because we had 200 pixels of stretch before,
+            // this should only relax the existing overscroll, and not dispatch anything to the
+            // state
+            moveBy(Offset(-200f, 0f))
+        }
+
+        rule.runOnIdle {
+            // All -200 should have been consumed by overscroll that was relaxing the existing
+            // stretch
+            assertThat(state.scrollPosition).isEqualTo(1000f)
+        }
+    }
+
+    @Test
+    fun stretchOverscroll_whenPulledWithSmallDelta_doesNotConsumesOppositePreScroll_pullRight() {
+        val state = setStretchOverscrollContent(Orientation.Horizontal)
+
+        rule.onNodeWithTag(OverscrollBox).performTouchInput {
+            down(center)
+            // Try and stretch by 0.4f (the max scroll value is 1000)
+            moveBy(Offset(1000.4f, 0f))
+            // Pull 200 in the opposite direction - overscroll should have ignored the 0.4f, and
+            // so all -200 should be dispatched to the state with nothing being consumed
+            moveBy(Offset(-200f, 0f))
+        }
+
+        rule.runOnIdle {
+            // All -200 should be dispatched directly to the state
+            assertThat(state.scrollPosition).isEqualTo(800f)
+        }
+    }
+
+    @Test
+    fun stretchOverscroll_whenPulled_consumesOppositePreScroll_pullBottom() {
+        val state = setStretchOverscrollContent(Orientation.Vertical)
+
+        rule.onNodeWithTag(OverscrollBox).performTouchInput {
+            down(center)
+            // Stretch by 200 (the max scroll value is 1000)
+            moveBy(Offset(0f, 1200f))
+            // Pull 200 in the opposite direction - because we had 200 pixels of stretch before,
+            // this should only relax the existing overscroll, and not dispatch anything to the
+            // state
+            moveBy(Offset(0f, -200f))
+        }
+
+        rule.runOnIdle {
+            // All -200 should have been consumed by overscroll that was relaxing the existing
+            // stretch
+            assertThat(state.scrollPosition).isEqualTo(1000f)
+        }
+    }
+
+    @Test
+    fun stretchOverscroll_whenPulledWithSmallDelta_doesNotConsumesOppositePreScroll_pullBottom() {
+        val state = setStretchOverscrollContent(Orientation.Vertical)
+
+        rule.onNodeWithTag(OverscrollBox).performTouchInput {
+            down(center)
+            // Try and stretch by 0.4f (the max scroll value is 1000)
+            moveBy(Offset(0f, 1000.4f))
+            // Pull 200 in the opposite direction - overscroll should have ignored the 0.4f, and
+            // so all -200 should be dispatched to the state with nothing being consumed
+            moveBy(Offset(0f, -200f))
+        }
+
+        rule.runOnIdle {
+            // All -200 should be dispatched directly to the state
+            assertThat(state.scrollPosition).isEqualTo(800f)
+        }
+    }
+
+    private fun setStretchOverscrollContent(orientation: Orientation): TestScrollableState {
+        animationScaleRule.setAnimationDurationScale(1f)
+        val state = TestScrollableState()
+        rule.setContent {
+            WithTouchSlop(touchSlop = 0f) {
+                val overscroll = ScrollableDefaults.overscrollEffect()
+                Box(
+                    Modifier
+                        .testTag(OverscrollBox)
+                        .size(100.dp)
+                        .scrollable(
+                            state = state,
+                            orientation = orientation,
+                            overscrollEffect = overscroll
+                        )
+                        .overscroll(overscroll)
+                )
+            }
+        }
+        return state
+    }
+}
+
+/**
+ * Returns a default [ScrollableState] with a [scrollPosition] clamped between 0f and 1000f.
+ */
+private class TestScrollableState : ScrollableState {
+    var scrollPosition by mutableStateOf(0f)
+        private set
+
+    // Using ScrollableState here instead of ScrollState as ScrollState will automatically round to
+    // an int, and we need to assert floating point values
+    private val scrollableState = ScrollableState {
+        val newPosition = (scrollPosition + it).coerceIn(0f, 1000f)
+        val consumed = newPosition - scrollPosition
+        scrollPosition = newPosition
+        consumed
+    }
+
+    override suspend fun scroll(
+        scrollPriority: MutatePriority,
+        block: suspend ScrollScope.() -> Unit
+    ) = scrollableState.scroll(scrollPriority, block)
+
+    override fun dispatchRawDelta(delta: Float) = scrollableState.dispatchRawDelta(delta)
+
+    override val isScrollInProgress: Boolean
+        get() = scrollableState.isScrollInProgress
 }
 
 private const val OverscrollBox = "box"
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TransformableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TransformableTest.kt
index 3fb977b..2713153 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TransformableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TransformableTest.kt
@@ -159,6 +159,35 @@
     }
 
     @Test
+    fun transformable_panWithOneFinger() {
+        var cumulativePan = Offset.Zero
+        var touchSlop = 0f
+
+        setTransformableContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            Modifier.transformable(
+                state = rememberTransformableState { _, pan, _ ->
+                    cumulativePan += pan
+                }
+            )
+        }
+
+        val expected = Offset(50f + touchSlop, 0f)
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput {
+            down(1, center)
+            moveBy(1, expected)
+            up(1)
+        }
+
+        rule.mainClock.advanceTimeBy(milliseconds = 1000)
+
+        rule.runOnIdle {
+            assertWithMessage("Should have panned 20/10").that(cumulativePan).isEqualTo(expected)
+        }
+    }
+
+    @Test
     fun transformable_rotate() {
         var cumulativeRotation = 0f
 
@@ -290,8 +319,10 @@
         val state = TransformableState { zoom, _, _ ->
             cumulativeScale *= zoom
         }
+        var slop: Float = 0f
 
         setTransformableContent {
+            slop = LocalViewConfiguration.current.touchSlop
             Modifier.transformable(state = state)
         }
 
@@ -302,7 +333,7 @@
         rule.onNodeWithTag(TEST_TAG).performTouchInput {
             down(pointerId = 1, center)
             down(pointerId = 2, center + Offset(10f, 10f))
-            moveBy(2, Offset(20f, 20f))
+            moveBy(2, Offset(slop * 2, slop * 2))
         }
 
         assertThat(state.isTransformInProgress).isEqualTo(true)
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
new file mode 100644
index 0000000..a2ce246
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
@@ -0,0 +1,666 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.list.assertIsNotPlaced
+import androidx.compose.foundation.lazy.list.assertIsPlaced
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.LocalPinnableContainer
+import androidx.compose.ui.layout.PinnableContainer
+import androidx.compose.ui.layout.PinnableContainer.PinnedHandle
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@MediumTest
+class LazyGridPinnableContainerTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private var pinnableContainer: PinnableContainer? = null
+
+    private val itemSizePx = 10
+    private var itemSize = Dp.Unspecified
+
+    private val composed = mutableSetOf<Int>()
+
+    @Before
+    fun setup() {
+        itemSize = with(rule.density) { itemSizePx.toDp() }
+    }
+
+    @Composable
+    fun Item(index: Int) {
+        Box(
+            Modifier
+                .size(itemSize)
+                .testTag("$index")
+        )
+        DisposableEffect(index) {
+            composed.add(index)
+            onDispose {
+                composed.remove(index)
+            }
+        }
+    }
+
+    @Test
+    fun pinnedItemIsComposedAndPlacedWhenScrolledOut() {
+        val state = LazyGridState()
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(1)
+            runBlocking {
+                state.scrollToItem(3)
+            }
+        }
+
+        rule.waitUntil {
+            // not visible items were disposed
+            !composed.contains(0)
+        }
+
+        rule.runOnIdle {
+            // item 1 is still pinned
+            assertThat(composed).contains(1)
+        }
+
+        rule.onNodeWithTag("1")
+            .assertExists()
+            .assertIsNotDisplayed()
+            .assertIsPlaced()
+    }
+
+    @Test
+    fun itemsBetweenPinnedAndCurrentVisibleAreNotComposed() {
+        val state = LazyGridState()
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(4)
+            }
+        }
+
+        rule.waitUntil {
+            // not visible items were disposed
+            !composed.contains(0)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).doesNotContain(0)
+            assertThat(composed).contains(1)
+            assertThat(composed).doesNotContain(2)
+            assertThat(composed).doesNotContain(3)
+            assertThat(composed).contains(4)
+        }
+    }
+
+    @Test
+    fun pinnedItemAfterVisibleOnesIsComposedAndPlacedWhenScrolledOut() {
+        val state = LazyGridState()
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 4) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(4)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible items to be disposed
+            !composed.contains(1)
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+            assertThat(composed).contains(5)
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(0)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible items to be disposed
+            !composed.contains(5)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(0)
+            assertThat(composed).contains(1)
+            assertThat(composed).doesNotContain(2)
+            assertThat(composed).doesNotContain(3)
+            assertThat(composed).contains(4)
+            assertThat(composed).doesNotContain(5)
+        }
+    }
+
+    @Test
+    fun pinnedItemCanBeUnpinned() {
+        val state = LazyGridState()
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        val handle = rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(3)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible items to be disposed
+            !composed.contains(0)
+        }
+
+        rule.runOnIdle {
+            handle.release()
+        }
+
+        rule.waitUntil {
+            // wait for unpinned item to be disposed
+            !composed.contains(1)
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsNotPlaced()
+    }
+
+    @Test
+    fun pinnedItemIsStillPinnedWhenReorderedAndNotVisibleAnymore() {
+        val state = LazyGridState()
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 3),
+                state = state
+            ) {
+                items(list, key = { it }) { index ->
+                    if (index == 2) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).containsExactly(0, 1, 2)
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            list = listOf(0, 3, 4, 1, 2)
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(1)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).containsExactly(0, 3, 4, 2) // 2 is pinned
+        }
+
+        rule.onNodeWithTag("2")
+            .assertIsPlaced()
+    }
+
+    @Test
+    fun unpinnedWhenLazyGridStateChanges() {
+        var state by mutableStateOf(LazyGridState(firstVisibleItemIndex = 2))
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 2) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(3)
+            runBlocking {
+                state.scrollToItem(0)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(3)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(2)
+            state = LazyGridState()
+        }
+
+        rule.waitUntil {
+            // wait for pinned item to be disposed
+            !composed.contains(2)
+        }
+
+        rule.onNodeWithTag("2")
+            .assertIsNotPlaced()
+    }
+
+    @Test
+    fun pinAfterLazyGridStateChange() {
+        var state by mutableStateOf(LazyGridState())
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 0) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            state = LazyGridState()
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(1)
+            runBlocking {
+                state.scrollToItem(2)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(1)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(0)
+        }
+    }
+
+    @Test
+    fun itemsArePinnedBasedOnGlobalIndexes() {
+        val state = LazyGridState(firstVisibleItemIndex = 3)
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                repeat(100) { index ->
+                    item {
+                        if (index == 3) {
+                            pinnableContainer = LocalPinnableContainer.current
+                        }
+                        Item(index)
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(4)
+            runBlocking {
+                state.scrollToItem(6)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(4)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(3)
+        }
+
+        rule.onNodeWithTag("3")
+            .assertExists()
+            .assertIsNotDisplayed()
+            .assertIsPlaced()
+    }
+
+    @Test
+    fun pinnedItemIsRemovedWhenNotVisible() {
+        val state = LazyGridState(3)
+        var itemCount by mutableStateOf(10)
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(itemCount) { index ->
+                    if (index == 3) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+            assertThat(composed).contains(4)
+            runBlocking {
+                state.scrollToItem(0)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(4)
+        }
+
+        rule.runOnIdle {
+            itemCount = 3
+        }
+
+        rule.waitUntil {
+            // wait for pinned item to be disposed
+            !composed.contains(3)
+        }
+
+        rule.onNodeWithTag("3")
+            .assertIsNotPlaced()
+    }
+
+    @Test
+    fun pinnedItemIsRemovedWhenVisible() {
+        val state = LazyGridState(0)
+        var items by mutableStateOf(listOf(0, 1, 2))
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(items) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            items = listOf(0, 2)
+        }
+
+        rule.waitUntil {
+            // wait for pinned item to be disposed
+            !composed.contains(1)
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsNotPlaced()
+    }
+
+    @Test
+    fun pinnedMultipleTimes() {
+        val state = LazyGridState(0)
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        val handles = mutableListOf<PinnedHandle>()
+        rule.runOnIdle {
+            handles.add(requireNotNull(pinnableContainer).pin())
+            handles.add(requireNotNull(pinnableContainer).pin())
+        }
+
+        rule.runOnIdle {
+            // pinned 3 times in total
+            handles.add(requireNotNull(pinnableContainer).pin())
+            assertThat(composed).contains(0)
+            runBlocking {
+                state.scrollToItem(3)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(0)
+        }
+
+        while (handles.isNotEmpty()) {
+            rule.runOnIdle {
+                assertThat(composed).contains(1)
+                handles.removeFirst().release()
+            }
+        }
+
+        rule.waitUntil {
+            // wait for pinned item to be disposed
+            !composed.contains(1)
+        }
+    }
+
+    @Test
+    fun pinningIsPropagatedToParentContainer() {
+        var parentPinned = false
+        val parentContainer = object : PinnableContainer {
+            override fun pin(): PinnedHandle {
+                parentPinned = true
+                return PinnedHandle { parentPinned = false }
+            }
+        }
+        // Arrange.
+        rule.setContent {
+            CompositionLocalProvider(LocalPinnableContainer provides parentContainer) {
+                LazyVerticalGrid(GridCells.Fixed(1)) {
+                    item {
+                        pinnableContainer = LocalPinnableContainer.current
+                        Box(Modifier.size(itemSize))
+                    }
+                }
+            }
+        }
+
+        val handle = rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(parentPinned).isTrue()
+            handle.release()
+        }
+
+        rule.runOnIdle {
+            assertThat(parentPinned).isFalse()
+        }
+    }
+
+    @Test
+    fun parentContainerChange_pinningIsMaintained() {
+        var parent1Pinned = false
+        val parent1Container = object : PinnableContainer {
+            override fun pin(): PinnedHandle {
+                parent1Pinned = true
+                return PinnedHandle { parent1Pinned = false }
+            }
+        }
+        var parent2Pinned = false
+        val parent2Container = object : PinnableContainer {
+            override fun pin(): PinnedHandle {
+                parent2Pinned = true
+                return PinnedHandle { parent2Pinned = false }
+            }
+        }
+        var parentContainer by mutableStateOf<PinnableContainer>(parent1Container)
+        // Arrange.
+        rule.setContent {
+            CompositionLocalProvider(LocalPinnableContainer provides parentContainer) {
+                LazyVerticalGrid(GridCells.Fixed(1)) {
+                    item {
+                        pinnableContainer = LocalPinnableContainer.current
+                        Box(Modifier.size(itemSize))
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(parent1Pinned).isTrue()
+            assertThat(parent2Pinned).isFalse()
+            parentContainer = parent2Container
+        }
+
+        rule.runOnIdle {
+            assertThat(parent1Pinned).isFalse()
+            assertThat(parent2Pinned).isTrue()
+        }
+    }
+}
\ No newline at end of file
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 dd1e23b..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
@@ -38,7 +38,6 @@
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 
@@ -245,13 +244,31 @@
     }
 
     @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()
         assertThat(state.canScrollBackward).isFalse()
     }
 
-    @Ignore("b/259608530")
     @Test
     fun canScrollBackward() = runBlocking {
         withContext(Dispatchers.Main + AutoTestFrameClock()) {
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 7f7e5da..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
@@ -19,20 +19,15 @@
 import androidx.compose.animation.core.FloatSpringSpec
 import androidx.compose.foundation.AutoTestFrameClock
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.border
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.animateScrollBy
 import androidx.compose.foundation.lazy.grid.isEqualTo
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
 import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
@@ -139,7 +134,32 @@
             state.animateScrollToItem(100)
         }
         rule.waitForIdle()
-        assertThat(state.firstVisibleItemIndex).isEqualTo(90)
+        assertThat(state.firstVisibleItemIndex).isEqualTo(91)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
+    fun animateScrollToItem_toFullSpan() {
+        runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(50, 10)
+        }
+        rule.waitForIdle()
+        assertThat(state.firstVisibleItemIndex).isEqualTo(50)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+    }
+
+    @Test
+    fun animateScrollToItem_toFullSpan_andBack() {
+        runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(50, 10)
+        }
+        rule.waitForIdle()
+
+        runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(45, 0)
+        }
+
+        assertThat(state.firstVisibleItemIndex).isEqualTo(44)
         assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
     }
 
@@ -168,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,
@@ -232,13 +272,22 @@
             state = state,
             modifier = Modifier.axisSize(itemSizeDp * 2, itemSizeDp * 5)
         ) {
-            items(100) {
+            items(
+                count = 100,
+                span = {
+                    // mark a span to check scroll through
+                    if (it == 50)
+                        StaggeredGridItemSpan.FullLine
+                    else
+                        StaggeredGridItemSpan.SingleLane
+                }
+            ) {
                 BasicText(
                     "$it",
                     Modifier
                         .mainAxisSize(itemSizeDp)
                         .testTag("$it")
-                        .border(1.dp, Color.Black)
+                        .debugBorder()
                 )
             }
         }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
index c718b43..a17afd8 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
@@ -121,7 +121,7 @@
                 state = state
             ) {
                 items(100) {
-                    Spacer(Modifier.mainAxisSize(itemSizeDp).testTag("$it"))
+                    Spacer(Modifier.mainAxisSize(itemSizeDp).testTag("$it").debugBorder())
                 }
             }
         }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLaneInfoTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLaneInfoTest.kt
new file mode 100644
index 0000000..aac67a3
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLaneInfoTest.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.compose.foundation.lazy.staggeredgrid
+
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertEquals
+import org.junit.Test
+
+class LazyStaggeredGridLaneInfoTest {
+    private val laneInfo = LazyStaggeredGridLaneInfo()
+
+    @Test
+    fun emptySpan_unset() {
+        assertEquals(LazyStaggeredGridLaneInfo.Unset, laneInfo.getLane(0))
+    }
+
+    @Test
+    fun setLane() {
+        laneInfo.setLane(0, 42)
+        laneInfo.setLane(1, 0)
+
+        assertEquals(42, laneInfo.getLane(0))
+        assertEquals(0, laneInfo.getLane(1))
+    }
+
+    @Test
+    fun setLane_beyondBound() {
+        val bound = laneInfo.upperBound()
+        laneInfo.setLane(bound - 1, 42)
+        laneInfo.setLane(bound, 42)
+
+        assertEquals(42, laneInfo.getLane(bound - 1))
+        assertEquals(42, laneInfo.getLane(bound))
+    }
+
+    @Test
+    fun setLane_largeNumber() {
+        laneInfo.setLane(Int.MAX_VALUE / 2, 42)
+
+        assertEquals(42, laneInfo.getLane(Int.MAX_VALUE / 2))
+    }
+
+    @Test
+    fun setLane_decreaseBound() {
+        laneInfo.setLane(Int.MAX_VALUE / 2, 42)
+        laneInfo.setLane(0, 42)
+
+        assertEquals(-1, laneInfo.getLane(Int.MAX_VALUE / 2))
+        assertEquals(42, laneInfo.getLane(0))
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun setLane_negative() {
+        laneInfo.setLane(-1, 0)
+    }
+
+    @Test
+    fun setLaneGaps() {
+        laneInfo.setLane(0, 0)
+        laneInfo.setLane(1, 1)
+        laneInfo.setGaps(0, intArrayOf(42, 24))
+        laneInfo.setGaps(1, intArrayOf(12, 21))
+
+        assertThat(laneInfo.getGaps(0)).asList().isEqualTo(listOf(42, 24))
+        assertThat(laneInfo.getGaps(1)).asList().isEqualTo(listOf(12, 21))
+    }
+
+    @Test
+    fun missingLaneGaps() {
+        laneInfo.setLane(42, 0)
+        laneInfo.setGaps(0, intArrayOf(42, 24))
+
+        assertThat(laneInfo.getGaps(42)).isNull()
+    }
+
+    @Test
+    fun clearLaneGaps() {
+        laneInfo.setLane(42, 0)
+        laneInfo.setGaps(42, intArrayOf(42, 24))
+
+        assertThat(laneInfo.getGaps(42)).isNotNull()
+
+        laneInfo.setGaps(42, null)
+        assertThat(laneInfo.getGaps(42)).isNull()
+    }
+
+    @Test
+    fun resetOnLaneInfoContentMove() {
+        laneInfo.setLane(0, 0)
+        laneInfo.setGaps(0, intArrayOf(42, 24))
+
+        laneInfo.setLane(Int.MAX_VALUE / 2, 1)
+
+        laneInfo.setGaps(0, null)
+        assertThat(laneInfo.getGaps(0)).isNull()
+    }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..5796438
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt
@@ -0,0 +1,668 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.staggeredgrid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.list.assertIsNotPlaced
+import androidx.compose.foundation.lazy.list.assertIsPlaced
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.LocalPinnableContainer
+import androidx.compose.ui.layout.PinnableContainer
+import androidx.compose.ui.layout.PinnableContainer.PinnedHandle
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@OptIn(ExperimentalFoundationApi::class)
+@MediumTest
+class LazyStaggeredGridPinnableContainerTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private var pinnableContainer: PinnableContainer? = null
+
+    private val itemSizePx = 10
+    private var itemSize = Dp.Unspecified
+
+    private val composed = mutableSetOf<Int>()
+
+    @Before
+    fun setup() {
+        itemSize = with(rule.density) { itemSizePx.toDp() }
+    }
+
+    @Composable
+    fun Item(index: Int) {
+        Box(
+            Modifier
+                .size(itemSize)
+                .testTag("$index")
+        )
+        DisposableEffect(index) {
+            composed.add(index)
+            onDispose {
+                composed.remove(index)
+            }
+        }
+    }
+
+    @Test
+    fun pinnedItemIsComposedAndPlacedWhenScrolledOut() {
+        val state = LazyStaggeredGridState()
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(1)
+            runBlocking {
+                state.scrollToItem(3)
+            }
+        }
+
+        rule.waitUntil {
+            // not visible items were disposed
+            !composed.contains(0)
+        }
+
+        rule.runOnIdle {
+            // item 1 is still pinned
+            assertThat(composed).contains(1)
+        }
+
+        rule.onNodeWithTag("1")
+            .assertExists()
+            .assertIsNotDisplayed()
+            .assertIsPlaced()
+    }
+
+    @Test
+    fun itemsBetweenPinnedAndCurrentVisibleAreNotComposed() {
+        val state = LazyStaggeredGridState()
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(4)
+            }
+        }
+
+        rule.waitUntil {
+            // not visible items were disposed
+            !composed.contains(0)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).doesNotContain(0)
+            assertThat(composed).contains(1)
+            assertThat(composed).doesNotContain(2)
+            assertThat(composed).doesNotContain(3)
+            assertThat(composed).contains(4)
+        }
+    }
+
+    @Test
+    fun pinnedItemAfterVisibleOnesIsComposedAndPlacedWhenScrolledOut() {
+        val state = LazyStaggeredGridState()
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 4) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(4)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible items to be disposed
+            !composed.contains(1)
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+            assertThat(composed).contains(5)
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(0)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible items to be disposed
+            !composed.contains(5)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(0)
+            assertThat(composed).contains(1)
+            assertThat(composed).doesNotContain(2)
+            assertThat(composed).doesNotContain(3)
+            assertThat(composed).contains(4)
+            assertThat(composed).doesNotContain(5)
+        }
+    }
+
+    @Test
+    fun pinnedItemCanBeUnpinned() {
+        val state = LazyStaggeredGridState()
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        val handle = rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(3)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible items to be disposed
+            !composed.contains(0)
+        }
+
+        rule.runOnIdle {
+            handle.release()
+        }
+
+        rule.waitUntil {
+            // wait for unpinned item to be disposed
+            !composed.contains(1)
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsNotPlaced()
+    }
+
+    @Test
+    fun pinnedItemIsStillPinnedWhenReorderedAndNotVisibleAnymore() {
+        val state = LazyStaggeredGridState()
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 3),
+                state = state
+            ) {
+                items(list, key = { it }) { index ->
+                    if (index == 2) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).containsExactly(0, 1, 2)
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            list = listOf(0, 3, 4, 1, 2)
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(1)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).containsExactly(0, 3, 4, 2) // 2 is pinned
+        }
+
+        rule.onNodeWithTag("2")
+            .assertIsPlaced()
+    }
+
+    @Test
+    fun unpinnedWhenLazyStaggeredGridStateChanges() {
+        var state by mutableStateOf(LazyStaggeredGridState(initialFirstVisibleItemIndex = 2))
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 2) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(3)
+            runBlocking {
+                state.scrollToItem(0)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(3)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(2)
+            state = LazyStaggeredGridState()
+        }
+
+        rule.waitUntil {
+            // wait for pinned item to be disposed
+            !composed.contains(2)
+        }
+
+        rule.onNodeWithTag("2")
+            .assertIsNotPlaced()
+    }
+
+    @Test
+    fun pinAfterLazyStaggeredGridStateChange() {
+        var state by mutableStateOf(LazyStaggeredGridState())
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 0) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            state = LazyStaggeredGridState()
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(1)
+            runBlocking {
+                state.scrollToItem(2)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(1)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(0)
+        }
+    }
+
+    @Test
+    fun itemsArePinnedBasedOnGlobalIndexes() {
+        val state = LazyStaggeredGridState(initialFirstVisibleItemIndex = 3)
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                repeat(100) { index ->
+                    item {
+                        if (index == 3) {
+                            pinnableContainer = LocalPinnableContainer.current
+                        }
+                        Item(index)
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(4)
+            runBlocking {
+                state.scrollToItem(6)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(4)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(3)
+        }
+
+        rule.onNodeWithTag("3")
+            .assertExists()
+            .assertIsNotDisplayed()
+            .assertIsPlaced()
+    }
+
+    @Test
+    fun pinnedItemIsRemovedWhenNotVisible() {
+        val state = LazyStaggeredGridState(3)
+        var itemCount by mutableStateOf(10)
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(itemCount) { index ->
+                    if (index == 3) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+            assertThat(composed).contains(4)
+            runBlocking {
+                state.scrollToItem(0)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(4)
+        }
+
+        rule.runOnIdle {
+            itemCount = 3
+        }
+
+        rule.waitUntil {
+            // wait for pinned item to be disposed
+            !composed.contains(3)
+        }
+
+        rule.onNodeWithTag("3")
+            .assertIsNotPlaced()
+    }
+
+    @Test
+    fun pinnedItemIsRemovedWhenVisible() {
+        val state = LazyStaggeredGridState(0)
+        var items by mutableStateOf(listOf(0, 1, 2))
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(items) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            items = listOf(0, 2)
+        }
+
+        rule.waitUntil {
+            // wait for pinned item to be disposed
+            !composed.contains(1)
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsNotPlaced()
+    }
+
+    @Test
+    fun pinnedMultipleTimes() {
+        val state = LazyStaggeredGridState(0)
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        val handles = mutableListOf<PinnedHandle>()
+        rule.runOnIdle {
+            handles.add(requireNotNull(pinnableContainer).pin())
+            handles.add(requireNotNull(pinnableContainer).pin())
+        }
+
+        rule.runOnIdle {
+            // pinned 3 times in total
+            handles.add(requireNotNull(pinnableContainer).pin())
+            assertThat(composed).contains(0)
+            runBlocking {
+                state.scrollToItem(3)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(0)
+        }
+
+        while (handles.isNotEmpty()) {
+            rule.runOnIdle {
+                assertThat(composed).contains(1)
+                handles.removeFirst().release()
+            }
+        }
+
+        rule.waitUntil {
+            // wait for pinned item to be disposed
+            !composed.contains(1)
+        }
+    }
+
+    @Test
+    fun pinningIsPropagatedToParentContainer() {
+        var parentPinned = false
+        val parentContainer = object : PinnableContainer {
+            override fun pin(): PinnedHandle {
+                parentPinned = true
+                return PinnedHandle { parentPinned = false }
+            }
+        }
+        // Arrange.
+        rule.setContent {
+            CompositionLocalProvider(LocalPinnableContainer provides parentContainer) {
+                LazyVerticalStaggeredGrid(StaggeredGridCells.Fixed(1)) {
+                    item {
+                        pinnableContainer = LocalPinnableContainer.current
+                        Box(Modifier.size(itemSize))
+                    }
+                }
+            }
+        }
+
+        val handle = rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(parentPinned).isTrue()
+            handle.release()
+        }
+
+        rule.runOnIdle {
+            assertThat(parentPinned).isFalse()
+        }
+    }
+
+    @Test
+    fun parentContainerChange_pinningIsMaintained() {
+        var parent1Pinned = false
+        val parent1Container = object : PinnableContainer {
+            override fun pin(): PinnedHandle {
+                parent1Pinned = true
+                return PinnedHandle { parent1Pinned = false }
+            }
+        }
+        var parent2Pinned = false
+        val parent2Container = object : PinnableContainer {
+            override fun pin(): PinnedHandle {
+                parent2Pinned = true
+                return PinnedHandle { parent2Pinned = false }
+            }
+        }
+        var parentContainer by mutableStateOf<PinnableContainer>(parent1Container)
+        // Arrange.
+        rule.setContent {
+            CompositionLocalProvider(LocalPinnableContainer provides parentContainer) {
+                LazyVerticalStaggeredGrid(StaggeredGridCells.Fixed(1)) {
+                    item {
+                        pinnableContainer = LocalPinnableContainer.current
+                        Box(Modifier.size(itemSize))
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(parent1Pinned).isTrue()
+            assertThat(parent2Pinned).isFalse()
+            parentContainer = parent2Container
+        }
+
+        rule.runOnIdle {
+            assertThat(parent1Pinned).isFalse()
+            assertThat(parent2Pinned).isTrue()
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt
index ffe5acc..224a1cc 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt
@@ -33,7 +33,9 @@
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsNotDisplayed
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.runBlocking
@@ -521,6 +523,133 @@
         waitForPrefetch(9)
     }
 
+    @Test
+    fun fullSpanIsPrefetchedCorrectly() {
+        val nodeConstraints = mutableMapOf<Int, Constraints>()
+        rule.setContent {
+            state = rememberLazyStaggeredGridState()
+            LazyStaggeredGrid(
+                2,
+                Modifier.mainAxisSize(itemsSizeDp * 5f).crossAxisSize(itemsSizeDp * 2f),
+                state,
+            ) {
+                items(
+                    count = 100,
+                    span = {
+                        if (it % 10 == 0)
+                            StaggeredGridItemSpan.FullLine
+                        else
+                            StaggeredGridItemSpan.SingleLane
+                    }
+                ) {
+                    DisposableEffect(it) {
+                        activeNodes.add(it)
+                        onDispose {
+                            activeNodes.remove(it)
+                            activeMeasuredNodes.remove(it)
+                        }
+                    }
+                    Spacer(
+                        Modifier
+                            .border(Dp.Hairline, Color.Black)
+                            .testTag("$it")
+                            .layout { measurable, constraints ->
+                                val placeable = measurable.measure(constraints)
+                                activeMeasuredNodes.add(it)
+                                nodeConstraints.put(it, constraints)
+                                layout(placeable.width, placeable.height) {
+                                    placeable.place(0, 0)
+                                }
+                            }
+                            .mainAxisSize(if (it == 0) itemsSizeDp else itemsSizeDp * 2)
+                    )
+                }
+            }
+        }
+
+        // ┌─┬─┐
+        // │5├─┤
+        // ├─┤6│
+        // │7├─┤
+        // ├─┤8│
+        // └─┴─┘
+
+        state.scrollBy(itemsSizeDp * 5f)
+        assertThat(activeNodes).contains(9)
+
+        waitForPrefetch(10)
+        val expectedConstraints = if (vertical) {
+            Constraints.fixedWidth(itemsSizePx * 2)
+        } else {
+            Constraints.fixedHeight(itemsSizePx * 2)
+        }
+        assertThat(nodeConstraints[10]).isEqualTo(expectedConstraints)
+    }
+
+    @Test
+    fun fullSpanIsPrefetchedCorrectly_scrollingBack() {
+        rule.setContent {
+            state = rememberLazyStaggeredGridState()
+            LazyStaggeredGrid(
+                2,
+                Modifier.mainAxisSize(itemsSizeDp * 5f),
+                state,
+            ) {
+                items(
+                    count = 100,
+                    span = {
+                        if (it % 10 == 0)
+                            StaggeredGridItemSpan.FullLine
+                        else
+                            StaggeredGridItemSpan.SingleLane
+                    }
+                ) {
+                    DisposableEffect(it) {
+                        activeNodes.add(it)
+                        onDispose {
+                            activeNodes.remove(it)
+                            activeMeasuredNodes.remove(it)
+                        }
+                    }
+                    Spacer(
+                        Modifier
+                            .mainAxisSize(if (it == 0) itemsSizeDp else itemsSizeDp * 2)
+                            .border(Dp.Hairline, Color.Black)
+                            .testTag("$it")
+                            .layout { measurable, constraints ->
+                                val placeable = measurable.measure(constraints)
+                                activeMeasuredNodes.add(it)
+                                layout(placeable.width, placeable.height) {
+                                    placeable.place(0, 0)
+                                }
+                            }
+                    )
+                }
+            }
+        }
+
+        state.scrollBy(itemsSizeDp * 13f + 2.dp)
+        rule.waitForIdle()
+
+        // second scroll to make sure subcompose marks elements as /not/ active
+        state.scrollBy(2.dp)
+        rule.waitForIdle()
+
+        // ┌──┬──┐
+        // │11│12│
+        // ├──┼──┤
+        // │13│14│
+        // ├──┼──┤
+        // └──┴──┘
+
+        assertThat(activeNodes).contains(11)
+        assertThat(activeNodes).doesNotContain(10)
+
+        state.scrollBy(-1.dp)
+
+        waitForPrefetch(10)
+    }
+
     private fun waitForPrefetch(index: Int) {
         rule.waitUntil {
             activeNodes.contains(index) && activeMeasuredNodes.contains(index)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt
index 49c2a4e..d52aa77 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt
@@ -18,17 +18,14 @@
 
 import androidx.compose.foundation.AutoTestFrameClock
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.border
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
@@ -204,6 +201,16 @@
         assertThat(state.canScrollBackward).isTrue()
     }
 
+    @Test
+    fun sctollToItem_fullSpan() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(49, 10)
+        }
+
+        assertThat(state.firstVisibleItemIndex).isEqualTo(49)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+    }
+
     @Composable
     private fun TestContent() {
         // |-|-|
@@ -220,13 +227,22 @@
             state = state,
             modifier = Modifier.axisSize(itemSizeDp * 2, itemSizeDp * 5)
         ) {
-            items(100) {
+            items(
+                count = 100,
+                span = {
+                    if (it == 50) {
+                        StaggeredGridItemSpan.FullLine
+                    } else {
+                        StaggeredGridItemSpan.SingleLane
+                    }
+                }
+            ) {
                 BasicText(
                     "$it",
                     Modifier
                         .mainAxisSize(itemSizeDp * ((it % 2) + 1))
                         .testTag("$it")
-                        .border(1.dp, Color.Black)
+                        .debugBorder()
                 )
             }
         }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemanticTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemanticTest.kt
index d9ba110..3ee1508 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemanticTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemanticTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.text.BasicText
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
@@ -97,7 +98,7 @@
                     .crossAxisSize(itemSizeDp * 2)
             ) {
                 items(items = List(ItemCount) { it }, key = { key(it) }) {
-                    Spacer(Modifier.testTag(tag(it)).mainAxisSize(itemSizeDp))
+                    BasicText("$it", Modifier.testTag(tag(it)).mainAxisSize(itemSizeDp))
                 }
             }
         }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSpansTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSpansTest.kt
deleted file mode 100644
index 63621ea..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSpansTest.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.lazy.staggeredgrid
-
-import kotlin.test.assertEquals
-import org.junit.Test
-
-class LazyStaggeredGridSpansTest {
-    private val spans = LazyStaggeredGridSpans()
-
-    @Test
-    fun emptySpan_unset() {
-        assertEquals(LazyStaggeredGridSpans.Unset, spans.getSpan(0))
-    }
-
-    @Test
-    fun setSpan() {
-        spans.setSpan(0, 42)
-        spans.setSpan(1, 0)
-
-        assertEquals(42, spans.getSpan(0))
-        assertEquals(0, spans.getSpan(1))
-    }
-
-    @Test
-    fun setSpan_beyondBound() {
-        val bound = spans.upperBound()
-        spans.setSpan(bound - 1, 42)
-        spans.setSpan(bound, 42)
-
-        assertEquals(42, spans.getSpan(bound - 1))
-        assertEquals(42, spans.getSpan(bound))
-    }
-
-    @Test
-    fun setSpan_largeNumber() {
-        spans.setSpan(Int.MAX_VALUE / 2, 42)
-
-        assertEquals(42, spans.getSpan(Int.MAX_VALUE / 2))
-    }
-
-    @Test
-    fun setSpan_decreaseBound() {
-        spans.setSpan(Int.MAX_VALUE / 2, 42)
-        spans.setSpan(0, 42)
-
-        assertEquals(-1, spans.getSpan(Int.MAX_VALUE / 2))
-        assertEquals(42, spans.getSpan(0))
-    }
-
-    @Test(expected = IllegalArgumentException::class)
-    fun setSpan_negative() {
-        spans.setSpan(-1, 0)
-    }
-}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
index 7eec8ab..01cadd0 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
@@ -35,10 +35,13 @@
 import androidx.compose.ui.test.assertCountEquals
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
 import androidx.compose.ui.test.junit4.StateRestorationTester
 import androidx.compose.ui.test.onChildren
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
@@ -851,7 +854,7 @@
     fun resizingItems_maintainsScrollingRange() {
         val state = LazyStaggeredGridState()
         var itemSizes by mutableStateOf(
-            List(20) {
+            List(10) {
                 itemSizeDp * (it % 4 + 1)
             }
         )
@@ -867,7 +870,7 @@
                     .testTag(LazyStaggeredGridTag)
                     .border(1.dp, Color.Red),
             ) {
-                items(20) {
+                items(itemSizes.size) {
                     Box(
                         Modifier
                             .axisSize(
@@ -884,24 +887,24 @@
         }
 
         rule.onNodeWithTag(LazyStaggeredGridTag)
-            .scrollMainAxisBy(itemSizeDp * 20)
+            .scrollMainAxisBy(itemSizeDp * 10)
 
-        rule.onNodeWithTag("18")
-            .assertMainAxisSizeIsEqualTo(itemSizes[18])
+        rule.onNodeWithTag("8")
+            .assertMainAxisSizeIsEqualTo(itemSizes[8])
 
-        rule.onNodeWithTag("19")
-            .assertMainAxisSizeIsEqualTo(itemSizes[19])
+        rule.onNodeWithTag("9")
+            .assertMainAxisSizeIsEqualTo(itemSizes[9])
 
         itemSizes = itemSizes.reversed()
 
-        rule.onNodeWithTag("18")
+        rule.onNodeWithTag("8")
             .assertIsDisplayed()
 
-        rule.onNodeWithTag("19")
+        rule.onNodeWithTag("9")
             .assertIsDisplayed()
 
         rule.onNodeWithTag(LazyStaggeredGridTag)
-            .scrollMainAxisBy(-itemSizeDp * 20)
+            .scrollMainAxisBy(-itemSizeDp * 10)
 
         rule.onNodeWithTag("0")
             .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
@@ -996,10 +999,16 @@
 
         // check that scrolling back and forth doesn't crash
         rule.onNodeWithTag(LazyStaggeredGridTag)
-            .scrollMainAxisBy(10000.dp)
+            .scrollMainAxisBy(1000.dp)
 
         rule.onNodeWithTag(LazyStaggeredGridTag)
-            .scrollMainAxisBy(-10000.dp)
+            .scrollMainAxisBy(-1000.dp)
+
+        rule.onNodeWithTag("${Int.MAX_VALUE / 2}")
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("${Int.MAX_VALUE / 2 + 1}")
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
     }
 
     @Test
@@ -1294,4 +1303,394 @@
             }
         }
     }
+
+    @Test
+    fun fullSpan_fillsAllCrossAxisSpace() {
+        val state = LazyStaggeredGridState()
+        state.prefetchingEnabled = false
+        rule.setContentWithTestViewConfiguration {
+            LazyStaggeredGrid(
+                3,
+                Modifier
+                    .testTag(LazyStaggeredGridTag)
+                    .crossAxisSize(itemSizeDp * 3)
+                    .mainAxisSize(itemSizeDp * 10),
+                state
+            ) {
+                item(span = StaggeredGridItemSpan.FullLine) {
+                    Box(Modifier.testTag("0").mainAxisSize(itemSizeDp))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertMainAxisSizeIsEqualTo(itemSizeDp)
+            .assertCrossAxisSizeIsEqualTo(itemSizeDp * 3)
+            .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+    }
+
+    @Test
+    fun fullSpan_leavesEmptyGapsWithOtherItems() {
+        val state = LazyStaggeredGridState()
+        state.prefetchingEnabled = false
+        rule.setContentWithTestViewConfiguration {
+            LazyStaggeredGrid(
+                3,
+                Modifier
+                    .testTag(LazyStaggeredGridTag)
+                    .crossAxisSize(itemSizeDp * 3)
+                    .mainAxisSize(itemSizeDp * 10),
+                state
+            ) {
+                items(2) {
+                    Box(Modifier.testTag("$it").mainAxisSize(itemSizeDp))
+                }
+
+                item(span = StaggeredGridItemSpan.FullLine) {
+                    Box(Modifier.testTag("full").mainAxisSize(itemSizeDp))
+                }
+            }
+        }
+
+        // ┌─┬─┬─┐
+        // │0│1│#│
+        // ├─┴─┴─┤
+        // │full │
+        // └─────┘
+        rule.onNodeWithTag("0")
+            .assertAxisBounds(
+                DpOffset(0.dp, 0.dp),
+                DpSize(itemSizeDp, itemSizeDp)
+            )
+
+        rule.onNodeWithTag("1")
+            .assertAxisBounds(
+                DpOffset(itemSizeDp, 0.dp),
+                DpSize(itemSizeDp, itemSizeDp)
+            )
+
+        rule.onNodeWithTag("full")
+            .assertAxisBounds(
+                DpOffset(0.dp, itemSizeDp),
+                DpSize(itemSizeDp * 3, itemSizeDp)
+            )
+    }
+
+    @Test
+    fun fullSpan_leavesGapsBetweenItems() {
+        val state = LazyStaggeredGridState()
+        state.prefetchingEnabled = false
+        rule.setContentWithTestViewConfiguration {
+            LazyStaggeredGrid(
+                3,
+                Modifier
+                    .testTag(LazyStaggeredGridTag)
+                    .crossAxisSize(itemSizeDp * 3)
+                    .mainAxisSize(itemSizeDp * 10),
+                state
+            ) {
+                items(3) {
+                    Box(Modifier.testTag("$it").mainAxisSize(itemSizeDp + itemSizeDp * it / 2))
+                }
+
+                item(span = StaggeredGridItemSpan.FullLine) {
+                    Box(Modifier.testTag("full").mainAxisSize(itemSizeDp))
+                }
+            }
+        }
+
+        // ┌───┬───┬───┐
+        // │ 0 │ 1 │ 2 │
+        // ├───┤   │   │
+        // │   └───┤   │
+        // ├───────┴───┤
+        // │   full    │
+        // └───────────┘
+        rule.onNodeWithTag("0")
+            .assertAxisBounds(
+                DpOffset(0.dp, 0.dp),
+                DpSize(itemSizeDp, itemSizeDp)
+            )
+
+        rule.onNodeWithTag("1")
+            .assertAxisBounds(
+                DpOffset(itemSizeDp, 0.dp),
+                DpSize(itemSizeDp, itemSizeDp * 1.5f)
+            )
+
+        rule.onNodeWithTag("2")
+            .assertAxisBounds(
+                DpOffset(itemSizeDp * 2, 0.dp),
+                DpSize(itemSizeDp, itemSizeDp * 2f)
+            )
+
+        rule.onNodeWithTag("full")
+            .assertAxisBounds(
+                DpOffset(0.dp, itemSizeDp * 2f),
+                DpSize(itemSizeDp * 3, itemSizeDp)
+            )
+    }
+
+    @Test
+    fun fullSpan_scrollsCorrectly() {
+        val state = LazyStaggeredGridState()
+        state.prefetchingEnabled = false
+        rule.setContentWithTestViewConfiguration {
+            LazyStaggeredGrid(
+                3,
+                Modifier
+                    .testTag(LazyStaggeredGridTag)
+                    .crossAxisSize(itemSizeDp * 3)
+                    .mainAxisSize(itemSizeDp * 2),
+                state
+            ) {
+                items(3) {
+                    Box(
+                        Modifier
+                            .testTag("$it")
+                            .mainAxisSize(itemSizeDp + itemSizeDp * it / 2)
+                    )
+                }
+
+                item(span = StaggeredGridItemSpan.FullLine) {
+                    Box(Modifier.testTag("full").mainAxisSize(itemSizeDp))
+                }
+
+                items(3) {
+                    Box(
+                        Modifier
+                            .testTag("${it + 3}")
+                            .mainAxisSize(itemSizeDp + itemSizeDp * it / 2)
+                    )
+                }
+
+                item(span = StaggeredGridItemSpan.FullLine) {
+                    Box(Modifier.testTag("full-2").mainAxisSize(itemSizeDp))
+                }
+            }
+        }
+
+        // ┌───┬───┬───┐
+        // │ 0 │ 1 │ 2 │
+        // ├───┤   │   │
+        // │   └───┤   │
+        // ├───────┴───┤ <-- scroll offset
+        // │   full    │
+        // ├───┬───┬───┤
+        // │ 3 │ 4 │ 5 │
+        // ├───┤   │   │ <-- end of screen
+        // │   └───┤   │
+        // ├───────┴───┤
+        // │   full-2  │
+        // └───────────┘
+        rule.onNodeWithTag(LazyStaggeredGridTag)
+            .scrollMainAxisBy(itemSizeDp * 2f)
+
+        rule.onNodeWithTag("full")
+            .assertAxisBounds(
+                DpOffset(0.dp, 0.dp),
+                DpSize(itemSizeDp * 3, itemSizeDp)
+            )
+
+        assertThat(state.firstVisibleItemIndex).isEqualTo(3)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
+    fun fullSpan_scrollsCorrectly_pastFullSpan() {
+        val state = LazyStaggeredGridState()
+        state.prefetchingEnabled = false
+        rule.setContentWithTestViewConfiguration {
+            LazyStaggeredGrid(
+                3,
+                Modifier
+                    .testTag(LazyStaggeredGridTag)
+                    .crossAxisSize(itemSizeDp * 3)
+                    .mainAxisSize(itemSizeDp * 2),
+                state
+            ) {
+                repeat(10) { repeatIndex ->
+                    items(3) {
+                        Box(
+                            Modifier
+                                .testTag("${repeatIndex * 3 + it}")
+                                .mainAxisSize(itemSizeDp + itemSizeDp * it / 2)
+                        )
+                    }
+
+                    item(span = StaggeredGridItemSpan.FullLine) {
+                        Box(Modifier.testTag("full-$repeatIndex").mainAxisSize(itemSizeDp))
+                    }
+                }
+            }
+        }
+
+        // ┌───┬───┬───┐
+        // │ 0 │ 1 │ 2 │
+        // ├───┤   │   │
+        // │   └───┤   │
+        // ├───────┴───┤
+        // │   full-0  │
+        // ├───┬───┬───┤  <-- scroll offset
+        // │ 3 │ 4 │ 5 │
+        // ├───┤   │   │
+        // │   └───┤   │
+        // ├───────┴───┤  <-- end of screen
+        // │   full-1  │
+        // └───────────┘
+        rule.onNodeWithTag(LazyStaggeredGridTag)
+            .scrollMainAxisBy(itemSizeDp * 3f)
+
+        rule.onNodeWithTag("3")
+            .assertAxisBounds(
+                DpOffset(0.dp, 0.dp),
+                DpSize(itemSizeDp, itemSizeDp)
+            )
+
+        rule.onNodeWithTag("4")
+            .assertAxisBounds(
+                DpOffset(itemSizeDp, 0.dp),
+                DpSize(itemSizeDp, itemSizeDp * 1.5f)
+            )
+
+        rule.onNodeWithTag("5")
+            .assertAxisBounds(
+                DpOffset(itemSizeDp * 2, 0.dp),
+                DpSize(itemSizeDp, itemSizeDp * 2)
+            )
+
+        assertThat(state.firstVisibleItemIndex).isEqualTo(4)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
+    fun fullSpan_scrollsCorrectly_pastFullSpan_andBack() {
+        val state = LazyStaggeredGridState()
+        state.prefetchingEnabled = false
+        rule.setContentWithTestViewConfiguration {
+            LazyStaggeredGrid(
+                3,
+                Modifier
+                    .testTag(LazyStaggeredGridTag)
+                    .crossAxisSize(itemSizeDp * 3)
+                    .mainAxisSize(itemSizeDp * 2),
+                state
+            ) {
+                repeat(10) { repeatIndex ->
+                    items(3) {
+                        Box(
+                            Modifier
+                                .testTag("${repeatIndex * 3 + it}")
+                                .mainAxisSize(itemSizeDp + itemSizeDp * it / 2)
+                        )
+                    }
+
+                    item(span = StaggeredGridItemSpan.FullLine) {
+                        Box(Modifier.testTag("full-$repeatIndex").mainAxisSize(itemSizeDp))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyStaggeredGridTag)
+            .scrollMainAxisBy(itemSizeDp * 3f)
+
+        rule.onNodeWithTag(LazyStaggeredGridTag)
+            .scrollMainAxisBy(-itemSizeDp * 3f)
+
+        // ┌───┬───┬───┐  <-- scroll offset
+        // │ 0 │ 1 │ 2 │
+        // ├───┤   │   │
+        // │   └───┤   │
+        // ├───────┴───┤  <-- end of screen
+        // │   full-0  │
+        // ├───┬───┬───┤
+        // │ 3 │ 4 │ 5 │
+        // ├───┤   │   │
+        // │   └───┤   │
+        // ├───────┴───┤
+        // │   full-1  │
+        // └───────────┘
+
+        rule.onNodeWithTag("0")
+            .assertAxisBounds(
+                DpOffset(0.dp, 0.dp),
+                DpSize(itemSizeDp, itemSizeDp)
+            )
+
+        rule.onNodeWithTag("1")
+            .assertAxisBounds(
+                DpOffset(itemSizeDp, 0.dp),
+                DpSize(itemSizeDp, itemSizeDp * 1.5f)
+            )
+
+        rule.onNodeWithTag("2")
+            .assertAxisBounds(
+                DpOffset(itemSizeDp * 2, 0.dp),
+                DpSize(itemSizeDp, itemSizeDp * 2)
+            )
+
+        assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
+    fun fullSpan_scrollsCorrectly_multipleFullSpans() {
+        val state = LazyStaggeredGridState()
+        state.prefetchingEnabled = false
+        rule.setContentWithTestViewConfiguration {
+            LazyStaggeredGrid(
+                3,
+                Modifier
+                    .testTag(LazyStaggeredGridTag)
+                    .crossAxisSize(itemSizeDp * 3)
+                    .mainAxisSize(itemSizeDp * 2),
+                state
+            ) {
+                items(10, span = { StaggeredGridItemSpan.FullLine }) {
+                    Box(
+                        Modifier
+                            .testTag("$it")
+                            .mainAxisSize(itemSizeDp)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyStaggeredGridTag)
+            .scrollMainAxisBy(itemSizeDp * 3f)
+
+        rule.onNodeWithTag("3")
+            .assertAxisBounds(
+                DpOffset(0.dp, 0.dp),
+                DpSize(itemSizeDp * 3, itemSizeDp)
+            )
+
+        rule.onNodeWithTag("4")
+            .assertAxisBounds(
+                DpOffset(0.dp, itemSizeDp),
+                DpSize(itemSizeDp * 3, itemSizeDp)
+            )
+
+        assertThat(state.firstVisibleItemIndex).isEqualTo(3)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+
+        rule.onNodeWithTag(LazyStaggeredGridTag)
+            .scrollMainAxisBy(itemSizeDp * 10f)
+
+        rule.onNodeWithTag("8")
+            .assertAxisBounds(
+                DpOffset(0.dp, 0.dp),
+                DpSize(itemSizeDp * 3, itemSizeDp)
+            )
+
+        rule.onNodeWithTag("9")
+            .assertAxisBounds(
+                DpOffset(0.dp, itemSizeDp),
+                DpSize(itemSizeDp * 3, itemSizeDp)
+            )
+
+        assertThat(state.firstVisibleItemIndex).isEqualTo(8)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
 }
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/PageLayoutPositionOnScrollingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PageLayoutPositionOnScrollingTest.kt
new file mode 100644
index 0000000..8d25cb7
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PageLayoutPositionOnScrollingTest.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.snapping.MinFlingVelocityDp
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.test.filters.LargeTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+internal class PageLayoutPositionOnScrollingTest(
+    val config: ParamConfig
+) : BasePagerTest(config) {
+
+    @Test
+    fun swipeForwardAndBackward_verifyPagesAreLaidOutCorrectly() {
+        // Arrange
+        val state = PagerState()
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+        val delta = pagerSize * 0.4f * scrollForwardSign
+
+        // Act and Assert - forward
+        repeat(DefaultAnimationRepetition) {
+            rule.onNodeWithTag(it.toString()).assertIsDisplayed()
+            confirmPageIsInCorrectPosition(it)
+            rule.onNodeWithTag(it.toString()).performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
+                    delta
+                )
+            }
+            rule.waitForIdle()
+        }
+
+        // Act - backward
+        repeat(DefaultAnimationRepetition) {
+            val countDown = DefaultAnimationRepetition - it
+            rule.onNodeWithTag(countDown.toString()).assertIsDisplayed()
+            confirmPageIsInCorrectPosition(countDown)
+            rule.onNodeWithTag(countDown.toString()).performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
+                    delta * -1f
+                )
+            }
+            rule.waitForIdle()
+        }
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() = mutableListOf<ParamConfig>().apply {
+            for (orientation in TestOrientation) {
+                for (pageSpacing in TestPageSpacing) {
+                    for (reverseLayout in TestReverseLayout) {
+                        for (layoutDirection in TestLayoutDirection) {
+                            for (contentPadding in testContentPaddings(orientation)) {
+                                add(
+                                    ParamConfig(
+                                        orientation = orientation,
+                                        pageSpacing = pageSpacing,
+                                        mainAxisContentPadding = contentPadding,
+                                        reverseLayout = reverseLayout,
+                                        layoutDirection = layoutDirection
+                                    )
+                                )
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
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..6d85e72 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
@@ -37,75 +39,6 @@
 ) : BasePagerTest(config) {
 
     @Test
-    fun swipePageTowardsEdge_shouldNotMove() {
-        // Arrange
-        val state = PagerState()
-        createPager(state = state, modifier = Modifier.fillMaxSize())
-        val delta = pagerSize * 0.4f * scrollForwardSign
-
-        // Act - backward
-        rule.onNodeWithTag("0").performTouchInput {
-            swipeWithVelocityAcrossMainAxis(
-                with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
-                delta * -1.0f
-            )
-        }
-        rule.waitForIdle()
-
-        // Assert
-        rule.onNodeWithTag("0").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(0)
-
-        // Act - forward
-        onPager().performTouchInput {
-            swipeWithVelocityAcrossMainAxis(
-                with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
-                delta
-            )
-        }
-        rule.waitForIdle()
-
-        // Assert
-        rule.onNodeWithTag("1").assertIsDisplayed()
-        confirmPageIsInCorrectPosition(1)
-    }
-
-    @Test
-    fun swipeForwardAndBackward_verifyPagesAreLaidOutCorrectly() {
-        // Arrange
-        val state = PagerState()
-        createPager(state = state, modifier = Modifier.fillMaxSize())
-        val delta = pagerSize * 0.4f * scrollForwardSign
-
-        // Act and Assert - forward
-        repeat(DefaultAnimationRepetition) {
-            rule.onNodeWithTag(it.toString()).assertIsDisplayed()
-            confirmPageIsInCorrectPosition(it)
-            rule.onNodeWithTag(it.toString()).performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
-                    delta
-                )
-            }
-            rule.waitForIdle()
-        }
-
-        // Act - backward
-        repeat(DefaultAnimationRepetition) {
-            val countDown = DefaultAnimationRepetition - it
-            rule.onNodeWithTag(countDown.toString()).assertIsDisplayed()
-            confirmPageIsInCorrectPosition(countDown)
-            rule.onNodeWithTag(countDown.toString()).performTouchInput {
-                swipeWithVelocityAcrossMainAxis(
-                    with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
-                    delta * -1f
-                )
-            }
-            rule.waitForIdle()
-        }
-    }
-
-    @Test
     fun swipeWithLowVelocity_shouldBounceBack() {
         // Arrange
         val state = PagerState(5)
@@ -175,6 +108,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,27 +173,118 @@
         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}")
         fun params() = mutableListOf<ParamConfig>().apply {
             for (orientation in TestOrientation) {
-                for (reverseLayout in TestReverseLayout) {
-                    for (layoutDirection in TestLayoutDirection) {
-                        for (pageSpacing in TestPageSpacing) {
-                            for (contentPadding in testContentPaddings(orientation)) {
-                                add(
-                                    ParamConfig(
-                                        orientation = orientation,
-                                        reverseLayout = reverseLayout,
-                                        layoutDirection = layoutDirection,
-                                        pageSpacing = pageSpacing,
-                                        mainAxisContentPadding = contentPadding
-                                    )
-                                )
-                            }
-                        }
-                    }
+                for (pageSpacing in TestPageSpacing) {
+                    add(
+                        ParamConfig(
+                            orientation = orientation,
+                            pageSpacing = pageSpacing
+                        )
+                    )
                 }
             }
         }
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/PagerSwipeEdgeTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerSwipeEdgeTest.kt
new file mode 100644
index 0000000..5dd952a
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerSwipeEdgeTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.snapping.MinFlingVelocityDp
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.test.filters.LargeTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+internal class PagerSwipeEdgeTest(
+    val config: ParamConfig
+) : BasePagerTest(config) {
+
+    @Test
+    fun swipePageTowardsEdge_shouldNotMove() {
+        // Arrange
+        val state = PagerState()
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+        val delta = pagerSize * 0.4f * scrollForwardSign
+
+        // Act - backward
+        rule.onNodeWithTag("0").performTouchInput {
+            swipeWithVelocityAcrossMainAxis(
+                with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
+                delta * -1.0f
+            )
+        }
+        rule.waitForIdle()
+
+        // Assert
+        rule.onNodeWithTag("0").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(0)
+
+        // Act - forward
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(
+                with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
+                delta
+            )
+        }
+        rule.waitForIdle()
+
+        // Assert
+        rule.onNodeWithTag("1").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(1)
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() = mutableListOf<ParamConfig>().apply {
+            for (orientation in TestOrientation) {
+                for (reverseLayout in TestReverseLayout) {
+                    for (layoutDirection in TestLayoutDirection) {
+                        add(
+                            ParamConfig(
+                                orientation = orientation,
+                                reverseLayout = reverseLayout,
+                                layoutDirection = layoutDirection
+                            )
+                        )
+                    }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
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/selection/SelectableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/selection/SelectableTest.kt
index 179bbd1..a4a49a9 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/selection/SelectableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/selection/SelectableTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation.selection
 
+import android.os.Build
 import androidx.compose.foundation.TapIndicationDelay
 import androidx.compose.foundation.interaction.FocusInteraction
 import androidx.compose.foundation.interaction.HoverInteraction
@@ -531,6 +532,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun selectableTest_interactionSource_focus_inKeyboardMode() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         lateinit var scope: CoroutineScope
@@ -633,6 +638,10 @@
     @OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun selectableTest_clickWithEnterKey() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         var counter = 0
         val focusRequester = FocusRequester()
@@ -661,6 +670,10 @@
     @OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun selectableTest_clickWithNumPadEnterKey() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         var counter = 0
         val focusRequester = FocusRequester()
@@ -689,6 +702,10 @@
     @OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun selectableTest_clickWithDPadCenter() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         var counter = 0
         val focusRequester = FocusRequester()
@@ -717,6 +734,10 @@
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun selectableTest_enterKey_emitsIndication() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
@@ -764,6 +785,10 @@
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun selectableTest_numPadEnterKey_emitsIndication() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
@@ -811,6 +836,10 @@
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun selectableTest_dpadCenter_emitsIndication() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
@@ -895,6 +924,10 @@
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun selectableTest_doubleEnterKey_emitsFurtherInteractions() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
@@ -956,6 +989,10 @@
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun selectableTest_repeatKeyEvents_doNotEmitFurtherInteractions() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
@@ -1018,6 +1055,10 @@
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun selectableTest_interruptedClick_emitsCancelIndication() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/selection/ToggleableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/selection/ToggleableTest.kt
index 38d0d22..f48a9ee 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/selection/ToggleableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/selection/ToggleableTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation.selection
 
+import android.os.Build
 import androidx.compose.foundation.TapIndicationDelay
 import androidx.compose.foundation.interaction.FocusInteraction
 import androidx.compose.foundation.interaction.HoverInteraction
@@ -623,6 +624,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun toggleableTest_interactionSource_focus_inKeyboardMode() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         lateinit var scope: CoroutineScope
@@ -824,6 +829,10 @@
     @OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun toggleableTest_clickWithEnterKey() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val focusRequester = FocusRequester()
         var toggled by mutableStateOf(false)
@@ -853,6 +862,10 @@
     @OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun toggleableTest_clickWithNumPadEnterKey() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val focusRequester = FocusRequester()
         var toggled by mutableStateOf(false)
@@ -882,6 +895,10 @@
     @OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun toggleableTest_clickWithDpadCenter() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val focusRequester = FocusRequester()
         var toggled by mutableStateOf(false)
@@ -911,6 +928,10 @@
     @OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun toggleableTest_clickWithEnterKey_triStateToggleable() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val focusRequester = FocusRequester()
         var toggled by mutableStateOf(false)
@@ -940,6 +961,10 @@
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun toggleableTest_enterKey_emitsIndication() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
@@ -987,6 +1012,10 @@
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun toggleableTest_numPadEnterKey_emitsIndication() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
@@ -1034,6 +1063,10 @@
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun toggleableTest_dpadCenter_emitsIndication() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
@@ -1118,6 +1151,10 @@
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun toggleableTest_doubleEnterKey_emitsFurtherInteractions() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
@@ -1179,6 +1216,10 @@
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun toggleableTest_repeatKeyEvents_doNotEmitFurtherInteractions() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
@@ -1239,6 +1280,10 @@
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun toggleableTest_interruptedClick_emitsCancelIndication() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/ClickableTextTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/ClickableTextTest.kt
index 1ffef6c..8bba29a2 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/ClickableTextTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/ClickableTextTest.kt
@@ -16,19 +16,26 @@
 
 package androidx.compose.foundation.text
 
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performMouseInput
 import androidx.compose.ui.text.AnnotatedString
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.nhaarman.mockitokotlin2.any
+import com.nhaarman.mockitokotlin2.argWhere
+import com.nhaarman.mockitokotlin2.inOrder
 import com.nhaarman.mockitokotlin2.mock
 import com.nhaarman.mockitokotlin2.times
 import com.nhaarman.mockitokotlin2.verify
+import com.nhaarman.mockitokotlin2.verifyZeroInteractions
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -79,4 +86,40 @@
             verify(onClick2, times(1)).invoke(any())
         }
     }
+
+    @OptIn(ExperimentalFoundationApi::class, ExperimentalTestApi::class)
+    @Test
+    fun onhover_callback() {
+        val onHover: (Int?) -> Unit = mock()
+        val onClick: (Int) -> Unit = mock()
+        rule.setContent {
+            ClickableText(
+                modifier = Modifier.testTag("clickableText"),
+                text = AnnotatedString("android"),
+                onHover = onHover,
+                onClick = onClick,
+            )
+        }
+
+        rule.onNodeWithTag("clickableText")
+            .performMouseInput {
+                moveTo(Offset(-1f, -1f), 0) // outside bounds
+                moveTo(Offset(1f, 1f), 0) // inside bounds
+                moveTo(Offset(-1f, -1f), 0) // outside bounds again
+                moveTo(Offset(1f, 1f), 0) // inside bounds again
+                moveTo(Offset(1f, 2f), 0) // move but stay on the same character
+                moveTo(Offset(50f, 1f), 0) // move to different character
+            }
+
+        rule.runOnIdle {
+            onHover.inOrder {
+                verify().invoke(0) // first enter
+                verify().invoke(null) // first exit
+                verify().invoke(0) // second enter
+                verify().invoke(argWhere { it > 0 }) // move to different character
+                verifyNoMoreInteractions()
+            }
+            verifyZeroInteractions(onClick)
+        }
+    }
 }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt
index cb9ca4f..13415a1 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt
@@ -55,6 +55,7 @@
 import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.unit.dp
 import androidx.test.filters.MediumTest
+import androidx.test.filters.RequiresDevice
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -94,6 +95,7 @@
     private val ListTag = "list"
     private val keyboardHelper = KeyboardHelper(rule)
 
+    @RequiresDevice // b/266736632
     @Test
     fun test() {
         rule.setContent {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests.kt
index 1e01af6..1c6dfb1 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests.kt
@@ -67,7 +67,8 @@
         text: String,
         modifier: Modifier,
         style: TextStyle,
-        onTextLayout: (TextLayoutResult) -> Unit
+        onTextLayout: (TextLayoutResult) -> Unit,
+        maxLines: Int
     )
 
     @Test
@@ -272,8 +273,9 @@
         text: String,
         modifier: Modifier,
         style: TextStyle = TextStyle.Default,
-        onTextLayout: (TextLayoutResult) -> Unit = {}
-    ) = TestContent(text, modifier, style, onTextLayout)
+        onTextLayout: (TextLayoutResult) -> Unit = {},
+        maxLines: Int = Int.MAX_VALUE
+    ) = TestContent(text, modifier, style, onTextLayout, maxLines)
 
     protected fun checkMagnifierAppears_whileHandleTouched(handle: Handle) {
         rule.setContent {
@@ -486,6 +488,35 @@
             .of(magnifierInitialPosition.y + lineHeight)
     }
 
+    protected fun checkMagnifierAsHandleGoesOutOfBoundsUsingMaxLines(handle: Handle) {
+        var lineHeight = 0f
+        rule.setContent {
+            Content(
+                "aaaa aaaa aaaa\naaaa aaaa aaaa",
+                Modifier
+                    // Center the text to give the magnifier lots of room to move.
+                    .fillMaxSize()
+                    .wrapContentSize()
+                    .testTag(tag),
+                onTextLayout = { lineHeight = it.getLineBottom(0) - it.getLineTop(0) },
+                maxLines = 1
+            )
+        }
+
+        showHandle(handle)
+
+        // Touch the handle to show the magnifier.
+        rule.onNode(isSelectionHandle(handle))
+            .performTouchInput { down(center) }
+
+        // Drag the handle down - the magnifier should follow.
+        val dragDistance = Offset(0f, lineHeight)
+        rule.onNode(isSelectionHandle(handle))
+            .performTouchInput { movePastSlopBy(dragDistance) }
+
+        assertNoMagnifierExists()
+    }
+
     protected fun checkMagnifierDoesNotFollowHandleVerticallyWithinLine(handle: Handle) {
         val dragDistance = Offset(0f, 1f)
         rule.setContent {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
index 3d29611..28acdca 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
@@ -16,21 +16,20 @@
 
 package androidx.compose.foundation.text.selection
 
-import androidx.activity.ComponentActivity
 import androidx.compose.foundation.text.InternalFoundationTextApi
 import androidx.compose.foundation.text.TEST_FONT_FAMILY
 import androidx.compose.foundation.text.TextDelegate
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.createFontFamilyResolver
 import androidx.compose.ui.text.style.ResolvedTextDirection
+import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.LayoutDirection
@@ -42,122 +41,114 @@
 import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.mock
 import com.nhaarman.mockitokotlin2.whenever
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 class MultiWidgetSelectionDelegateTest {
-    @get:Rule
-    val composeTestRule = createAndroidComposeRule<ComponentActivity>()
 
     private val fontFamily = TEST_FONT_FAMILY
     private val context = InstrumentationRegistry.getInstrumentation().context
     private val defaultDensity = Density(density = 1f)
-    @OptIn(ExperimentalTextApi::class)
     private val fontFamilyResolver = createFontFamilyResolver(context)
 
     @Test
     fun getHandlePosition_StartHandle_invalid() {
-        composeTestRule.setContent {
-            val text = "hello world\n"
-            val fontSize = 20.sp
+        val text = "hello world\n"
+        val fontSize = 20.sp
 
-            val layoutResult = simpleTextLayout(
-                text = text,
-                fontSize = fontSize,
-                density = defaultDensity
-            )
+        val layoutResult = simpleTextLayout(
+            text = text,
+            fontSize = fontSize,
+            density = defaultDensity
+        )
 
-            val layoutCoordinates = mock<LayoutCoordinates>()
-            whenever(layoutCoordinates.isAttached).thenReturn(true)
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
 
-            val selectableId = 1L
-            val selectable = MultiWidgetSelectionDelegate(
-                selectableId = selectableId,
-                coordinatesCallback = { layoutCoordinates },
-                layoutResultCallback = { layoutResult }
-            )
+        val selectableId = 1L
+        val selectable = MultiWidgetSelectionDelegate(
+            selectableId = selectableId,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
 
-            val selectableInvalidId = 2L
-            val startOffset = text.indexOf('h')
-            val endOffset = text.indexOf('o')
+        val selectableInvalidId = 2L
+        val startOffset = text.indexOf('h')
+        val endOffset = text.indexOf('o')
 
-            val selection = Selection(
-                start = Selection.AnchorInfo(
-                    direction = ResolvedTextDirection.Ltr,
-                    offset = startOffset,
-                    selectableId = selectableInvalidId
-                ),
-                end = Selection.AnchorInfo(
-                    direction = ResolvedTextDirection.Ltr,
-                    offset = endOffset,
-                    selectableId = selectableInvalidId
-                ),
-                handlesCrossed = false
-            )
+        val selection = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = startOffset,
+                selectableId = selectableInvalidId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = endOffset,
+                selectableId = selectableInvalidId
+            ),
+            handlesCrossed = false
+        )
 
-            // Act.
-            val coordinates = selectable.getHandlePosition(
-                selection = selection,
-                isStartHandle = true
-            )
+        // Act.
+        val coordinates = selectable.getHandlePosition(
+            selection = selection,
+            isStartHandle = true
+        )
 
-            // Assert.
-            assertThat(coordinates).isEqualTo(Offset.Zero)
-        }
+        // Assert.
+        assertThat(coordinates).isEqualTo(Offset.Zero)
     }
 
     @Test
     fun getHandlePosition_EndHandle_invalid() {
-        composeTestRule.setContent {
-            val text = "hello world\n"
-            val fontSize = 20.sp
+        val text = "hello world\n"
+        val fontSize = 20.sp
 
-            val layoutResult = simpleTextLayout(
-                text = text,
-                fontSize = fontSize,
-                density = defaultDensity
-            )
+        val layoutResult = simpleTextLayout(
+            text = text,
+            fontSize = fontSize,
+            density = defaultDensity
+        )
 
-            val layoutCoordinates = mock<LayoutCoordinates>()
-            whenever(layoutCoordinates.isAttached).thenReturn(true)
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
 
-            val selectableId = 1L
-            val selectable = MultiWidgetSelectionDelegate(
-                selectableId = selectableId,
-                coordinatesCallback = { layoutCoordinates },
-                layoutResultCallback = { layoutResult }
-            )
+        val selectableId = 1L
+        val selectable = MultiWidgetSelectionDelegate(
+            selectableId = selectableId,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
 
-            val selectableInvalidId = 2L
-            val startOffset = text.indexOf('h')
-            val endOffset = text.indexOf('o')
+        val selectableInvalidId = 2L
+        val startOffset = text.indexOf('h')
+        val endOffset = text.indexOf('o')
 
-            val selection = Selection(
-                start = Selection.AnchorInfo(
-                    direction = ResolvedTextDirection.Ltr,
-                    offset = startOffset,
-                    selectableId = selectableInvalidId
-                ),
-                end = Selection.AnchorInfo(
-                    direction = ResolvedTextDirection.Ltr,
-                    offset = endOffset,
-                    selectableId = selectableInvalidId
-                ),
-                handlesCrossed = false
-            )
+        val selection = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = startOffset,
+                selectableId = selectableInvalidId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = endOffset,
+                selectableId = selectableInvalidId
+            ),
+            handlesCrossed = false
+        )
 
-            // Act.
-            val coordinates = selectable.getHandlePosition(
-                selection = selection,
-                isStartHandle = false
-            )
+        // Act.
+        val coordinates = selectable.getHandlePosition(
+            selection = selection,
+            isStartHandle = false
+        )
 
-            // Assert.
-            assertThat(coordinates).isEqualTo(Offset.Zero)
-        }
+        // Assert.
+        assertThat(coordinates).isEqualTo(Offset.Zero)
     }
 
     @Test
@@ -522,6 +513,58 @@
     }
 
     @Test
+    fun getHandlePosition_EndHandle_not_cross_ltr_overflowed() {
+        val text = "hello\nworld"
+        val fontSize = 20.sp
+        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
+
+        val layoutResult = simpleTextLayout(
+            text = text,
+            fontSize = fontSize,
+            density = defaultDensity,
+            maxLines = 1
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectableId = 1L
+        val selectable = MultiWidgetSelectionDelegate(
+            selectableId = selectableId,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val startOffset = text.indexOf('h')
+        val endOffset = text.indexOf('r')
+
+        val selection = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = startOffset,
+                selectableId = selectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = endOffset,
+                selectableId = selectableId
+            ),
+            handlesCrossed = false
+        )
+
+        // Act.
+        val coordinates = selectable.getHandlePosition(
+            selection = selection,
+            isStartHandle = false
+        )
+
+        // Assert.
+        assertThat(coordinates).isEqualTo(
+            Offset(fontSizeInPx * 5, fontSizeInPx) // the last offset in the first line
+        )
+    }
+
+    @Test
     fun getHandlePosition_EndHandle_cross_ltr() {
         val text = "hello world\n"
         val fontSize = 20.sp
@@ -573,6 +616,58 @@
     }
 
     @Test
+    fun getHandlePosition_EndHandle_cross_ltr_overflowed() {
+        val text = "hello\nworld"
+        val fontSize = 20.sp
+        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
+
+        val layoutResult = simpleTextLayout(
+            text = text,
+            fontSize = fontSize,
+            density = defaultDensity,
+            maxLines = 1
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectableId = 1L
+        val selectable = MultiWidgetSelectionDelegate(
+            selectableId = selectableId,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val startOffset = text.indexOf('r')
+        val endOffset = text.indexOf('w')
+
+        val selection = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = startOffset,
+                selectableId = selectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Ltr,
+                offset = endOffset,
+                selectableId = selectableId
+            ),
+            handlesCrossed = true
+        )
+
+        // Act.
+        val coordinates = selectable.getHandlePosition(
+            selection = selection,
+            isStartHandle = false
+        )
+
+        // Assert.
+        assertThat(coordinates).isEqualTo(
+            Offset((fontSizeInPx * 5), fontSizeInPx)
+        )
+    }
+
+    @Test
     fun getHandlePosition_EndHandle_not_cross_rtl() {
         val text = "\u05D0\u05D1\u05D2 \u05D3\u05D4\u05D5\n"
         val fontSize = 20.sp
@@ -675,6 +770,106 @@
     }
 
     @Test
+    fun getHandlePosition_EndHandle_not_cross_rtl_overflowed() {
+        val text = "\u05D0\u05D1\u05D2\n\u05D3\u05D4\u05D5"
+        val fontSize = 20.sp
+        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
+
+        val layoutResult = simpleTextLayout(
+            text = text,
+            fontSize = fontSize,
+            density = defaultDensity,
+            maxLines = 1
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectableId = 1L
+        val selectable = MultiWidgetSelectionDelegate(
+            selectableId = selectableId,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val startOffset = text.indexOf('\u05D1')
+        val endOffset = text.indexOf('\u05D5')
+
+        val selection = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Rtl,
+                offset = startOffset,
+                selectableId = selectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Rtl,
+                offset = endOffset,
+                selectableId = selectableId
+            ),
+            handlesCrossed = false
+        )
+
+        // Act.
+        val coordinates = selectable.getHandlePosition(
+            selection = selection,
+            isStartHandle = false
+        )
+
+        // Assert.
+        assertThat(coordinates).isEqualTo(Offset(0f, fontSizeInPx))
+    }
+
+    @Test
+    fun getHandlePosition_EndHandle_cross_rtl_overflowed() {
+        val text = "\u05D0\u05D1\u05D2\n\u05D3\u05D4\u05D5"
+        val fontSize = 20.sp
+        val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
+
+        val layoutResult = simpleTextLayout(
+            text = text,
+            fontSize = fontSize,
+            density = defaultDensity,
+            maxLines = 1
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectableId = 1L
+        val selectable = MultiWidgetSelectionDelegate(
+            selectableId = selectableId,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val startOffset = text.indexOf('\u05D5')
+        val endOffset = text.indexOf('\u05D3')
+
+        val selection = Selection(
+            start = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Rtl,
+                offset = startOffset,
+                selectableId = selectableId
+            ),
+            end = Selection.AnchorInfo(
+                direction = ResolvedTextDirection.Rtl,
+                offset = endOffset,
+                selectableId = selectableId
+            ),
+            handlesCrossed = true
+        )
+
+        // Act.
+        val coordinates = selectable.getHandlePosition(
+            selection = selection,
+            isStartHandle = false
+        )
+
+        // Assert.
+        assertThat(coordinates).isEqualTo(Offset((fontSizeInPx * 3), fontSizeInPx))
+    }
+
+    @Test
     fun getHandlePosition_EndHandle_not_cross_bidi() {
         val textLtr = "Hello"
         val textRtl = "\u05D0\u05D1\u05D2"
@@ -965,8 +1160,7 @@
         val lineRange = selectable.getRangeOfLineContaining(0)
 
         // Assert.
-        assertThat(lineRange.start).isEqualTo(0)
-        assertThat(lineRange.end).isEqualTo(5)
+        assertThat(lineRange).isEqualTo(TextRange(0, 5))
     }
 
     @Test
@@ -991,8 +1185,7 @@
         val lineRange = selectable.getRangeOfLineContaining(7)
 
         // Assert.
-        assertThat(lineRange.start).isEqualTo(6)
-        assertThat(lineRange.end).isEqualTo(11)
+        assertThat(lineRange).isEqualTo(TextRange(6, 11))
     }
 
     @Test
@@ -1017,8 +1210,7 @@
         val lineRange = selectable.getRangeOfLineContaining(-1)
 
         // Assert.
-        assertThat(lineRange.start).isEqualTo(0)
-        assertThat(lineRange.end).isEqualTo(5)
+        assertThat(lineRange).isEqualTo(TextRange(0, 5))
     }
 
     @Test
@@ -1043,8 +1235,7 @@
         val lineRange = selectable.getRangeOfLineContaining(Int.MAX_VALUE)
 
         // Assert.
-        assertThat(lineRange.start).isEqualTo(6)
-        assertThat(lineRange.end).isEqualTo(11)
+        assertThat(lineRange).isEqualTo(TextRange(6, 11))
     }
 
     @Test
@@ -1069,8 +1260,7 @@
         val lineRange = selectable.getRangeOfLineContaining(5)
 
         // Assert.
-        assertThat(lineRange.start).isEqualTo(0)
-        assertThat(lineRange.end).isEqualTo(5)
+        assertThat(lineRange).isEqualTo(TextRange(0, 5))
     }
 
     @Test
@@ -1095,8 +1285,7 @@
         val lineRange = selectable.getRangeOfLineContaining(5)
 
         // Assert.
-        assertThat(lineRange.start).isEqualTo(0)
-        assertThat(lineRange.end).isEqualTo(0)
+        assertThat(lineRange).isEqualTo(TextRange.Zero)
     }
 
     @Test
@@ -1121,8 +1310,532 @@
         val lineRange = selectable.getRangeOfLineContaining(6)
 
         // Assert.
-        assertThat(lineRange.start).isEqualTo(6)
-        assertThat(lineRange.end).isEqualTo(6)
+        assertThat(lineRange).isEqualTo(TextRange(6, 6))
+    }
+
+    @Test
+    fun getRangeOfLineContaining_overflowed_returnsLastVisibleLine() {
+        val text = "hello\nworld"
+
+        val layoutResult = simpleTextLayout(
+            text = text,
+            density = defaultDensity,
+            maxLines = 1
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        // Act.
+        val lineRange = selectable.getRangeOfLineContaining(6)
+
+        // Assert.
+        assertThat(lineRange).isEqualTo(TextRange(0, 5))
+    }
+
+    @Test
+    fun getRangeOfLineContaining_overflowedDueToMaxHeight_returnsLastVisibleLine() {
+        val text = "hello\nworld"
+        val fontSize = 20.sp
+
+        val layoutResult = simpleTextLayout(
+            text = text,
+            density = defaultDensity,
+            fontSize = fontSize,
+            constraints = Constraints(maxHeight = with(defaultDensity) { fontSize.roundToPx() } * 1)
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        // Act.
+        val lineRange = selectable.getRangeOfLineContaining(6)
+
+        // Assert.
+        assertThat(lineRange).isEqualTo(TextRange(0, 5))
+    }
+
+    @Test
+    fun getLastVisibleOffset_everythingVisible_returnsTextLength() {
+        val text = "hello\nworld"
+
+        val layoutResult = constrainedTextLayout(
+            text = text
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+        assertThat(lastVisibleOffset).isEqualTo(text.length)
+    }
+
+    @Test
+    fun getLastVisibleOffset_changesWhenTextLayoutChanges() {
+        val text = "hello\nworld"
+
+        var layoutResult = constrainedTextLayout(text = text)
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        assertThat(selectable.getLastVisibleOffset()).isEqualTo(text.length)
+
+        layoutResult = constrainedTextLayout(text = "$text$text")
+
+        assertThat(selectable.getLastVisibleOffset()).isEqualTo(text.length * 2)
+    }
+
+    // start = maxLines 1
+    // start = clip
+    // start = enabled soft wrap
+    @Test
+    fun getLastVisibleOffset_maxLines1_clip_enabledSoftwrap_multiLineContent() {
+        val text = "hello\nworld"
+
+        val layoutResult = constrainedTextLayout(
+            text = text,
+            overflow = TextOverflow.Clip,
+            maxLines = 1,
+            softWrap = true
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+        assertThat(lastVisibleOffset).isEqualTo(5)
+    }
+
+    @Test
+    fun getLastVisibleOffset_maxLines1_clip_enabledSoftwrap_singleLineContent() {
+        val text = "hello world"
+
+        val layoutResult = constrainedTextLayout(
+            text = text,
+            maxLines = 1,
+            overflow = TextOverflow.Clip,
+            softWrap = true,
+            widthInCharacters = 10
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+        assertThat(lastVisibleOffset).isEqualTo(5)
+    }
+
+    // start = disabled soft wrap
+    @Test
+    fun getLastVisibleOffset_maxLines1_clip_disabledSoftwrap_multiLineContent() {
+        val text = "hello\nworld"
+
+        val layoutResult = constrainedTextLayout(
+            text = text,
+            maxLines = 1,
+            overflow = TextOverflow.Clip,
+            softWrap = false
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+        assertThat(lastVisibleOffset).isEqualTo(5)
+    }
+
+    @Test
+    fun getLastVisibleOffset_maxLines1_clip_disabledSoftwrap_singleLineContent() {
+        val text = "hello world ".repeat(10)
+
+        val layoutResult = constrainedTextLayout(
+            text = text,
+            maxLines = 1,
+            overflow = TextOverflow.Clip,
+            softWrap = false,
+            widthInCharacters = 10
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+        assertThat(lastVisibleOffset).isEqualTo(text.length)
+    }
+
+    // start = ellipsis
+    // start = enabled soft wrap
+    @Test
+    fun getLastVisibleOffset_maxLines1_ellipsis_enabledSoftwrap_multiLineContent() {
+        val text = "hello\nworld"
+
+        val layoutResult = constrainedTextLayout(
+            text = text,
+            overflow = TextOverflow.Ellipsis,
+            maxLines = 1,
+            softWrap = true,
+            widthInCharacters = 4
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+        assertThat(lastVisibleOffset).isEqualTo(3)
+    }
+
+    @Test
+    fun getLastVisibleOffset_maxLines1_ellipsis_enabledSoftwrap_singleLineContent() {
+        val text = "hello world ".repeat(10)
+
+        val layoutResult = constrainedTextLayout(
+            text = text,
+            maxLines = 1,
+            overflow = TextOverflow.Ellipsis,
+            softWrap = true,
+            widthInCharacters = 10
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+        assertThat(lastVisibleOffset).isEqualTo(9)
+    }
+
+    // start = disabled soft wrap
+    @Test
+    fun getLastVisibleOffset_maxLines1_ellipsis_disabledSoftwrap_multiLineContent() {
+        val text = "hello\nworld"
+
+        val layoutResult = constrainedTextLayout(
+            text = text,
+            maxLines = 1,
+            overflow = TextOverflow.Ellipsis,
+            softWrap = false,
+            widthInCharacters = 5
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+        // first line will include an ellipsis before line break
+        assertThat(lastVisibleOffset).isEqualTo(4)
+    }
+
+    @Test
+    fun getLastVisibleOffset_maxLines1_ellipsis_disabledSoftwrap_singleLineContent() {
+        val text = "hello world ".repeat(10)
+
+        val layoutResult = constrainedTextLayout(
+            text = text,
+            maxLines = 1,
+            overflow = TextOverflow.Ellipsis,
+            softWrap = false,
+            widthInCharacters = 20
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+        assertThat(lastVisibleOffset).isEqualTo(19)
+    }
+
+    // start = height constrained
+    // start = clip
+    // start = enabled soft wrap
+    @Test
+    fun getLastVisibleOffset_limitHeight_clip_enabledSoftwrap_multiLineContent() {
+        val text = "hello\nworld"
+
+        val layoutResult = constrainedTextLayout(
+            text = text,
+            overflow = TextOverflow.Clip,
+            softWrap = true,
+            maxHeightInLines = 1
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+        assertThat(lastVisibleOffset).isEqualTo(5)
+    }
+
+    @Test
+    fun getLastVisibleOffset_limitHeight_clip_enabledSoftwrap_singleLineContent() {
+        val text = "helloworld helloworld helloworld"
+
+        val layoutResult = constrainedTextLayout(
+            text = text,
+            overflow = TextOverflow.Clip,
+            softWrap = true,
+            widthInCharacters = 10,
+            maxHeightInLines = 2
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+        assertThat(lastVisibleOffset).isEqualTo(21)
+    }
+
+    // start = disabled soft wrap
+    @Test
+    fun getLastVisibleOffset_limitHeight_clip_disabledSoftwrap_multiLineContent() {
+        val text = "hello\nworld"
+
+        val layoutResult = constrainedTextLayout(
+            text = text,
+            overflow = TextOverflow.Clip,
+            softWrap = false,
+            maxHeightInLines = 1
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+        assertThat(lastVisibleOffset).isEqualTo(5)
+    }
+
+    @Test
+    fun getLastVisibleOffset_limitHeight_clip_disabledSoftwrap_singleLineContent() {
+        val text = "hello world ".repeat(10)
+
+        val layoutResult = constrainedTextLayout(
+            text = text,
+            overflow = TextOverflow.Clip,
+            softWrap = false,
+            widthInCharacters = 10,
+            maxHeightInLines = 1
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+        assertThat(lastVisibleOffset).isEqualTo(text.length)
+    }
+
+    // start = ellipsis
+    // start = enabled soft wrap
+    @Test
+    fun getLastVisibleOffset_limitHeight_ellipsis_enabledSoftwrap_multiLineContent() {
+        val text = "hello\nworld\nhello\nworld"
+
+        val layoutResult = constrainedTextLayout(
+            text = text,
+            overflow = TextOverflow.Ellipsis,
+            softWrap = true,
+            widthInCharacters = 10,
+            maxHeightInLines = 2,
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+        assertThat(lastVisibleOffset).isEqualTo(11)
+    }
+
+    @Test
+    fun getLastVisibleOffset_limitHeight_ellipsis_enabledSoftwrap_singleLineContent() {
+        val text = "hello world ".repeat(10)
+
+        val layoutResult = constrainedTextLayout(
+            text = text,
+            overflow = TextOverflow.Ellipsis,
+            softWrap = true,
+            widthInCharacters = 10,
+            maxHeightInLines = 1
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+        assertThat(lastVisibleOffset).isEqualTo(9)
+    }
+
+    // start = disabled soft wrap
+    @Test
+    fun getLastVisibleOffset_limitHeight_ellipsis_disabledSoftwrap_multiLineContent() {
+        val text = "hello\nworld"
+
+        val layoutResult = constrainedTextLayout(
+            text = text,
+            overflow = TextOverflow.Ellipsis,
+            softWrap = false,
+            maxHeightInLines = 1
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+        // first line will include an ellipsis before line break
+        assertThat(lastVisibleOffset).isEqualTo(5)
+    }
+
+    @Test
+    fun getLastVisibleOffset_limitHeight_ellipsis_disabledSoftwrap_singleLineContent() {
+        val text = "hello world ".repeat(10)
+
+        val layoutResult = constrainedTextLayout(
+            text = text,
+            overflow = TextOverflow.Ellipsis,
+            softWrap = false,
+            widthInCharacters = 20,
+            maxHeightInLines = 1
+        )
+
+        val layoutCoordinates = mock<LayoutCoordinates>()
+        whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+        val selectable = MultiWidgetSelectionDelegate(
+            1,
+            coordinatesCallback = { layoutCoordinates },
+            layoutResultCallback = { layoutResult }
+        )
+
+        val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+        assertThat(lastVisibleOffset).isEqualTo(19)
     }
 
     @Test
@@ -2647,7 +3360,9 @@
     private fun simpleTextLayout(
         text: String = "",
         fontSize: TextUnit = TextUnit.Unspecified,
-        density: Density
+        density: Density,
+        maxLines: Int = Int.MAX_VALUE,
+        constraints: Constraints = Constraints()
     ): TextLayoutResult {
         val spanStyle = SpanStyle(fontSize = fontSize, fontFamily = fontFamily)
         val annotatedString = AnnotatedString(text, spanStyle)
@@ -2655,7 +3370,42 @@
             text = annotatedString,
             style = TextStyle(),
             density = density,
+            maxLines = maxLines,
             fontFamilyResolver = fontFamilyResolver
-        ).layout(Constraints(), LayoutDirection.Ltr)
+        ).layout(constraints, LayoutDirection.Ltr)
+    }
+
+    @OptIn(InternalFoundationTextApi::class)
+    private fun constrainedTextLayout(
+        text: String = "",
+        fontSize: TextUnit = 20.sp,
+        density: Density = defaultDensity,
+        maxLines: Int = Int.MAX_VALUE,
+        overflow: TextOverflow = TextOverflow.Clip,
+        softWrap: Boolean = true,
+        widthInCharacters: Int = 20,
+        maxHeightInLines: Int = Int.MAX_VALUE
+    ): TextLayoutResult {
+        val spanStyle = SpanStyle(fontSize = fontSize, fontFamily = fontFamily)
+        val annotatedString = AnnotatedString(text, spanStyle)
+        val width = with(density) { fontSize.roundToPx() } * widthInCharacters
+        val constraints = Constraints(
+            minWidth = width,
+            maxWidth = width,
+            maxHeight = if (maxHeightInLines == Int.MAX_VALUE) {
+                Int.MAX_VALUE
+            } else {
+                with(density) { fontSize.roundToPx() } * maxHeightInLines
+            }
+        )
+        return TextDelegate(
+            text = annotatedString,
+            style = TextStyle(),
+            density = density,
+            fontFamilyResolver = fontFamilyResolver,
+            maxLines = maxLines,
+            overflow = overflow,
+            softWrap = softWrap
+        ).layout(constraints, LayoutDirection.Ltr)
     }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest.kt
index ad4fccb..00dba57 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest.kt
@@ -17,13 +17,16 @@
 package androidx.compose.foundation.text.selection
 
 import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.Handle
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextStyle
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import androidx.test.filters.RequiresDevice
 import androidx.test.filters.SdkSuppress
+import org.junit.Test
 import org.junit.runner.RunWith
 
 @MediumTest
@@ -36,10 +39,23 @@
         text: String,
         modifier: Modifier,
         style: TextStyle,
-        onTextLayout: (TextLayoutResult) -> Unit
+        onTextLayout: (TextLayoutResult) -> Unit,
+        maxLines: Int
     ) {
         SelectionContainer(modifier) {
-            BasicText(text, style = style, onTextLayout = onTextLayout)
+            BasicText(text, style = style, onTextLayout = onTextLayout, maxLines = maxLines)
         }
     }
+
+    @RequiresDevice // b/264702195
+    @Test
+    fun magnifier_goesToLastLine_whenSelectionEndDraggedBelowTextBounds_whenTextOverflowed() {
+        checkMagnifierAsHandleGoesOutOfBoundsUsingMaxLines(Handle.SelectionEnd)
+    }
+
+    @RequiresDevice // b/264702195
+    @Test
+    fun magnifier_hidden_whenSelectionStartDraggedBelowTextBounds_whenTextOverflowed() {
+        checkMagnifierAsHandleGoesOutOfBoundsUsingMaxLines(Handle.SelectionStart)
+    }
 }
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 0e16498..76317a7 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
@@ -16,10 +16,9 @@
 
 package androidx.compose.foundation.text.selection
 
-import android.view.View
-import android.view.ViewGroup
-import androidx.activity.ComponentActivity
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.foundation.text.Handle
 import androidx.compose.foundation.text.TEST_FONT_FAMILY
@@ -27,11 +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,30 +42,39 @@
 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
 import androidx.compose.ui.test.click
 import androidx.compose.ui.test.getUnclippedBoundsInRoot
 import androidx.compose.ui.test.hasAnyChild
 import androidx.compose.ui.test.hasTestTag
 import androidx.compose.ui.test.isRoot
 import androidx.compose.ui.test.junit4.ComposeTestRule
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.junit4.createComposeRule
 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
+import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.ResolvedTextDirection
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.unit.width
-import androidx.test.espresso.matcher.BoundedMatcher
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
@@ -71,24 +82,20 @@
 import com.nhaarman.mockitokotlin2.mock
 import com.nhaarman.mockitokotlin2.times
 import com.nhaarman.mockitokotlin2.verify
-import org.hamcrest.Description
+import java.util.concurrent.CountDownLatch
+import kotlin.math.max
+import kotlin.math.sign
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import kotlin.math.abs
-import kotlin.math.max
-import kotlin.math.roundToInt
-import org.junit.Ignore
 
-@Suppress("DEPRECATION")
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class SelectionContainerTest {
     @get:Rule
-    val rule = createAndroidComposeRule<ComponentActivity>()
-
-    private lateinit var view: View
+    val rule = createComposeRule().also {
+        it.mainClock.autoAdvance = false
+    }
 
     private val textContent = "Text Demo Text"
     private val fontFamily = TEST_FONT_FAMILY
@@ -96,6 +103,9 @@
     private val fontSize = 20.sp
     private val log = PointerInputChangeLog()
 
+    private val tag1 = "tag1"
+    private val tag2 = "tag2"
+
     private val hapticFeedback = mock<HapticFeedback>()
 
     @Test
@@ -190,7 +200,7 @@
         }
     }
 
-    @Ignore("b/230622412")
+    //    @Ignore("b/230622412")
     @Test
     fun long_press_select_a_word_rtl_layout() {
         with(rule.density) {
@@ -235,7 +245,280 @@
         }
     }
 
-    private fun createSelectionContainer(isRtl: Boolean = false) {
+    @Test
+    fun selectionContinues_toBelowText() = with(rule.density) {
+        createSelectionContainer {
+            Column {
+                BasicText(
+                    AnnotatedString(textContent),
+                    Modifier
+                        .fillMaxWidth()
+                        .testTag(tag1),
+                    style = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
+                )
+                BasicText(
+                    AnnotatedString(textContent),
+                    Modifier
+                        .fillMaxWidth()
+                        .testTag(tag2),
+                    style = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
+                )
+            }
+        }
+
+        startSelection(tag1)
+        dragHandleTo(Handle.SelectionEnd, offset = characterBox(tag2, 3).bottomRight)
+
+        assertAnchorInfo(selection.value?.start, offset = 0, selectableId = 1)
+        assertAnchorInfo(selection.value?.end, offset = 4, selectableId = 2)
+    }
+
+    @Test
+    fun selectionContinues_toAboveText() = with(rule.density) {
+        createSelectionContainer {
+            Column {
+                BasicText(
+                    AnnotatedString(textContent),
+                    Modifier
+                        .fillMaxWidth()
+                        .testTag(tag1),
+                    style = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
+                )
+                BasicText(
+                    AnnotatedString(textContent),
+                    Modifier
+                        .fillMaxWidth()
+                        .testTag(tag2),
+                    style = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
+                )
+            }
+        }
+
+        startSelection(tag2, offset = 6) // second word should be selected
+        dragHandleTo(Handle.SelectionStart, offset = characterBox(tag1, 5).bottomLeft)
+
+        assertAnchorInfo(selection.value?.start, offset = 5, selectableId = 1)
+        assertAnchorInfo(selection.value?.end, offset = 9, selectableId = 2)
+    }
+
+    @Test
+    fun selectionContinues_toNextText_skipsDisableSelection() = with(rule.density) {
+        createSelectionContainer {
+            Column {
+                BasicText(
+                    AnnotatedString(textContent),
+                    Modifier
+                        .fillMaxWidth()
+                        .testTag(tag1),
+                    style = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
+                )
+                DisableSelection {
+                    BasicText(
+                        AnnotatedString(textContent),
+                        Modifier
+                            .fillMaxWidth()
+                            .testTag(tag2),
+                        style = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
+                    )
+                }
+            }
+        }
+
+        startSelection(tag1)
+        dragHandleTo(Handle.SelectionEnd, offset = characterBox(tag2, 3).bottomRight)
+
+        assertAnchorInfo(selection.value?.start, offset = 0, selectableId = 1)
+        assertAnchorInfo(selection.value?.end, offset = textContent.length, selectableId = 1)
+    }
+
+    @Test
+    fun selectionHandle_remainsInComposition_whenTextIsOverflowed_clipped_softwrapDisabled() {
+        createSelectionContainer {
+            Column {
+                BasicText(
+                    AnnotatedString("$textContent ".repeat(100)),
+                    Modifier
+                        .fillMaxWidth()
+                        .testTag(tag1),
+                    style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
+                    softWrap = false
+                )
+                DisableSelection {
+                    BasicText(
+                        textContent,
+                        Modifier.fillMaxWidth(),
+                        style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
+                        softWrap = false
+                    )
+                }
+                BasicText(
+                    AnnotatedString("$textContent ".repeat(100)),
+                    Modifier
+                        .fillMaxWidth()
+                        .testTag(tag2),
+                    style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
+                    softWrap = false
+                )
+            }
+        }
+
+        startSelection(tag1)
+        dragHandleTo(Handle.SelectionEnd, offset = characterBox(tag2, 3).bottomRight)
+
+        assertAnchorInfo(selection.value?.start, offset = 0, selectableId = 1)
+        assertAnchorInfo(selection.value?.end, offset = 4, selectableId = 2)
+    }
+
+    @Test
+    fun allTextIsSelected_whenTextIsOverflowed_clipped_maxLines1() = with(rule.density) {
+        val longText = "$textContent ".repeat(100)
+        createSelectionContainer {
+            Column {
+                BasicText(
+                    AnnotatedString(longText),
+                    Modifier.fillMaxWidth().testTag(tag1),
+                    style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
+                    maxLines = 1
+                )
+            }
+        }
+
+        startSelection(tag1)
+        dragHandleTo(
+            handle = Handle.SelectionEnd,
+            offset = characterBox(tag1, 4).bottomRight + Offset(x = 0f, y = fontSize.toPx())
+        )
+
+        assertAnchorInfo(selection.value?.start, offset = 0, selectableId = 1)
+        assertAnchorInfo(selection.value?.end, offset = longText.length, selectableId = 1)
+    }
+
+    @Test
+    fun allTextIsSelected_whenTextIsOverflowed_ellipsized_maxLines1() = with(rule.density) {
+        val longText = "$textContent ".repeat(100)
+        createSelectionContainer {
+            Column {
+                BasicText(
+                    AnnotatedString(longText),
+                    Modifier.fillMaxWidth().testTag(tag1),
+                    style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
+                    maxLines = 1,
+                    overflow = TextOverflow.Ellipsis
+                )
+            }
+        }
+
+        startSelection(tag1)
+        dragHandleTo(
+            handle = Handle.SelectionEnd,
+            offset = characterBox(tag1, 4).bottomRight + Offset(x = 0f, y = fontSize.toPx())
+        )
+
+        assertAnchorInfo(selection.value?.start, offset = 0, selectableId = 1)
+        assertAnchorInfo(selection.value?.end, offset = longText.length, selectableId = 1)
+    }
+
+    @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
+    ) {
+        val textLayoutResult = rule.onNodeWithTag(tag).fetchTextLayoutResult()
+        val boundingBox = textLayoutResult.getBoundingBox(offset)
+        rule.onNodeWithTag(tag).performTouchInput {
+            longClick(boundingBox.center)
+        }
+    }
+
+    private fun characterBox(
+        tag: String,
+        offset: Int
+    ): Rect {
+        val nodePosition = rule.onNodeWithTag(tag).fetchSemanticsNode().positionInRoot
+        val textLayoutResult = rule.onNodeWithTag(tag).fetchTextLayoutResult()
+        return textLayoutResult.getBoundingBox(offset).translate(nodePosition)
+    }
+
+    private fun dragHandleTo(
+        handle: Handle,
+        offset: Offset
+    ) {
+        val position = rule.onNode(isSelectionHandle(handle))
+            .fetchSemanticsNode()
+            .config[SelectionHandleInfoKey]
+            .position
+        // selection handles are anchored at the bottom of a line.
+
+        rule.onNode(isSelectionHandle(handle)).performTouchInput {
+            val delta = offset - position
+            down(center)
+            movePastSlopBy(delta)
+            up()
+        }
+    }
+
+    /**
+     * Moves the first pointer by [delta] past the touch slop threshold on each axis.
+     * If [delta] is 0 on either axis it will stay 0.
+     */
+    private fun TouchInjectionScope.movePastSlopBy(delta: Offset) {
+        val slop = Offset(
+            x = viewConfiguration.touchSlop * delta.x.sign,
+            y = viewConfiguration.touchSlop * delta.y.sign
+        )
+        moveBy(delta + slop)
+    }
+
+    private fun assertAnchorInfo(
+        anchorInfo: Selection.AnchorInfo?,
+        resolvedTextDirection: ResolvedTextDirection = ResolvedTextDirection.Ltr,
+        offset: Int = 0,
+        selectableId: Long = 0
+    ) {
+        assertThat(anchorInfo).isEqualTo(
+            Selection.AnchorInfo(
+                resolvedTextDirection,
+                offset,
+                selectableId
+            )
+        )
+    }
+
+    private fun createSelectionContainer(
+        isRtl: Boolean = false,
+        content: (@Composable () -> Unit)? = null
+    ) {
         val measureLatch = CountDownLatch(1)
 
         val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
@@ -244,7 +527,11 @@
                 LocalHapticFeedback provides hapticFeedback,
                 LocalLayoutDirection provides layoutDirection
             ) {
-                TestParent(Modifier.testTag("selectionContainer").gestureSpy(log)) {
+                TestParent(
+                    Modifier
+                        .testTag("selectionContainer")
+                        .gestureSpy(log)
+                ) {
                     SelectionContainer(
                         modifier = Modifier.onGloballyPositioned {
                             measureLatch.countDown()
@@ -254,7 +541,7 @@
                             selection.value = it
                         }
                     ) {
-                        BasicText(
+                        content?.invoke() ?: BasicText(
                             AnnotatedString(textContent),
                             Modifier.fillMaxSize(),
                             style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
@@ -268,42 +555,6 @@
                 }
             }
         }
-        rule.activityRule.scenario.onActivity {
-            view = it.findViewById<ViewGroup>(android.R.id.content)
-        }
-    }
-
-    private fun matchesPosition(expectedX: Dp, expectedY: Dp): BoundedMatcher<View, View> {
-        return object : BoundedMatcher<View, View>(View::class.java) {
-            // (-1, -1) no position found
-            var positionFound = IntOffset(-1, -1)
-
-            override fun matchesSafely(item: View?): Boolean {
-                with(rule.density) {
-                    val position = IntArray(2)
-                    item?.getLocationOnScreen(position)
-                    positionFound = IntOffset(position[0], position[1])
-
-                    val expectedPositionXInt = expectedX.value.roundToInt()
-                    val expectedPositionYInt = expectedY.value.roundToInt()
-                    val positionFoundXInt = positionFound.x.toDp().value.roundToInt()
-                    val positionFoundYInt = positionFound.y.toDp().value.roundToInt()
-                    return abs(expectedPositionXInt - positionFoundXInt) < 5 &&
-                        abs(expectedPositionYInt - positionFoundYInt) < 5
-                }
-            }
-
-            override fun describeTo(description: Description?) {
-                with(rule.density) {
-                    description?.appendText(
-                        "with expected position: " +
-                            "${expectedX.value}, ${expectedY.value} " +
-                            "but position found:" +
-                            "${positionFound.x.toDp().value}, ${positionFound.y.toDp().value}"
-                    )
-                }
-            }
-        }
     }
 }
 
@@ -353,6 +604,19 @@
     }
 }
 
+/**
+ * Returns the text layout result caught by [SemanticsActions.GetTextLayoutResult]
+ * under this node. Throws an AssertionError if the node has not defined GetTextLayoutResult
+ * semantics action.
+ */
+fun SemanticsNodeInteraction.fetchTextLayoutResult(): TextLayoutResult {
+    val textLayoutResults = mutableListOf<TextLayoutResult>()
+    performSemanticsAction(SemanticsActions.GetTextLayoutResult) {
+        it(textLayoutResults)
+    }
+    return textLayoutResults.first()
+}
+
 @Composable
 fun TestParent(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
     Layout(modifier = modifier, content = content) { measurables, constraints ->
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldMagnifierTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldMagnifierTest.kt
index 75614836..0cc0ff8 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldMagnifierTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldMagnifierTest.kt
@@ -56,16 +56,18 @@
         text: String,
         modifier: Modifier,
         style: TextStyle,
-        onTextLayout: (TextLayoutResult) -> Unit
+        onTextLayout: (TextLayoutResult) -> Unit,
+        maxLines: Int
     ) {
         BasicTextField(
             text,
-                onValueChange = {},
-                modifier = modifier,
-                textStyle = style,
-                onTextLayout = onTextLayout
-            )
-        }
+            onValueChange = {},
+            modifier = modifier,
+            textStyle = style,
+            onTextLayout = onTextLayout,
+            maxLines = Int.MAX_VALUE
+        )
+    }
 
     @Test
     fun magnifier_appears_whileStartCursorTouched() {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldVisualTransformationMagnifierTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldVisualTransformationMagnifierTest.kt
index 431142c..0b9362c 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldVisualTransformationMagnifierTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldVisualTransformationMagnifierTest.kt
@@ -43,6 +43,7 @@
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
 import kotlin.math.sign
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -96,6 +97,7 @@
         assertThat(getMagnifierCenterOffset()).isNotEqualTo(Offset.Zero)
     }
 
+    @Ignore("b/266233836")
     @RequiresDevice // b/264701475
     @Test
     fun checkMagnifierFollowsHandleHorizontally() {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
index 319f135..c3a20e6e 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
@@ -30,6 +30,7 @@
 import androidx.compose.testutils.assertPixels
 import androidx.compose.testutils.assertShape
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.MotionDurationScale
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Brush
@@ -38,6 +39,7 @@
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.graphics.toPixelMap
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.hasSetTextAction
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -64,8 +66,13 @@
 @LargeTest
 class TextFieldCursorTest {
 
+    private val motionDurationScale = object : MotionDurationScale {
+        override var scaleFactor: Float by mutableStateOf(1f)
+    }
+
+    @OptIn(ExperimentalTestApi::class)
     @get:Rule
-    val rule = createComposeRule().also {
+    val rule = createComposeRule(effectContext = motionDurationScale).also {
         it.mainClock.autoAdvance = false
     }
 
@@ -86,8 +93,11 @@
     private val bgModifier = Modifier.background(textFieldBgColor)
     private val focusModifier = Modifier.onFocusChanged { if (it.isFocused) isFocused = true }
     private val sizeModifier = Modifier.size(textFieldWidth, textFieldHeight)
+
     // default TextFieldModifier
-    private val textFieldModifier = sizeModifier.then(bgModifier).then(focusModifier)
+    private val textFieldModifier = sizeModifier
+        .then(bgModifier)
+        .then(focusModifier)
 
     // default onTextLayout to capture cursor boundaries.
     private val onTextLayout: (TextLayoutResult) -> Unit = { cursorRect = it.getCursorRect(0) }
@@ -205,6 +215,52 @@
             )
     }
 
+    @Suppress("UnnecessaryOptInAnnotation")
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    fun cursorBlinkingAnimation_whenSystemDisablesAnimations() = with(rule.density) {
+        motionDurationScale.scaleFactor = 0f
+
+        rule.setContent {
+            // The padding helps if the test is run accidentally in landscape. Landscape makes
+            // the cursor to be next to the navigation bar which affects the red color to be a bit
+            // different - possibly anti-aliasing.
+            Box(Modifier.padding(boxPadding)) {
+                BasicTextField(
+                    value = "",
+                    onValueChange = {},
+                    textStyle = textStyle,
+                    modifier = textFieldModifier,
+                    cursorBrush = SolidColor(cursorColor),
+                    onTextLayout = onTextLayout
+                )
+            }
+        }
+
+        focusAndWait()
+
+        // cursor visible first 500 ms
+        rule.mainClock.advanceTimeBy(100)
+        with(rule.density) {
+            rule.onNode(hasSetTextAction())
+                .captureToImage()
+                .assertCursor(2.dp, this, cursorRect)
+        }
+
+        // cursor invisible during next 500 ms
+        rule.mainClock.advanceTimeBy(700)
+        rule.onNode(hasSetTextAction())
+            .captureToImage()
+            .assertShape(
+                density = rule.density,
+                shape = RectangleShape,
+                shapeColor = Color.White,
+                backgroundColor = Color.White,
+                shapeOverlapPixelCount = 0.0f
+            )
+    }
+
     @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     fun cursorUnsetColor_noCursor() = with(rule.density) {
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/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldVisualTransformationSelectionBoundsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldVisualTransformationSelectionBoundsTest.kt
index c582ec6..aecaba0 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldVisualTransformationSelectionBoundsTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldVisualTransformationSelectionBoundsTest.kt
@@ -184,7 +184,7 @@
         assertValidMessage(error, sourceIndex = 0, toTransformed = false)
     }
 
-    @FlakyTest(bugId = 241572024)
+    @Ignore // b/241572024
     @Test
     fun selectionEnd_throws_onStart_whenInvalidOriginalToTransformed() {
         rule.runOnIdle {
@@ -213,6 +213,7 @@
         assertValidMessage(error, sourceIndex = 0, toTransformed = false)
     }
 
+    @Ignore // b/241572024
     @Test
     fun selectionStart_throws_onDrag_whenInvalidOriginalToTransformed() {
         rule.onNodeWithTag(testTag).performTouchInput { longClick() }
@@ -228,6 +229,7 @@
         assertValidMessage(error, sourceIndex = 0, toTransformed = true)
     }
 
+    @Ignore // b/241572024
     @Test
     fun selectionStart_throws_onDrag_whenInvalidTransformedToOriginal() {
         rule.onNodeWithTag(testTag).performTouchInput { longClick() }
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt
index b651632..0df1c34 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt
@@ -63,12 +63,10 @@
 internal actual fun rememberOverscrollEffect(): OverscrollEffect {
     val context = LocalContext.current
     val config = LocalOverscrollConfiguration.current
-    return remember(context, config) {
-        if (config != null) {
-            AndroidEdgeEffectOverscrollEffect(context, config)
-        } else {
-            NoOpOverscrollEffect
-        }
+    return if (config != null) {
+        remember(context, config) { AndroidEdgeEffectOverscrollEffect(context, config) }
+    } else {
+        NoOpOverscrollEffect
     }
 }
 
@@ -132,41 +130,44 @@
 
     private var scrollCycleInProgress: Boolean = false
 
-    override fun consumePreScroll(
-        scrollDelta: Offset,
-        source: NestedScrollSource
+    override fun applyToScroll(
+        delta: Offset,
+        source: NestedScrollSource,
+        performScroll: (Offset) -> Offset
     ): Offset {
+        // Early return
         if (containerSize.isEmpty()) {
-            return Offset.Zero
+            return performScroll(delta)
         }
+
         if (!scrollCycleInProgress) {
             stopOverscrollAnimation()
             scrollCycleInProgress = true
         }
         val pointer = pointerPosition ?: containerSize.center
         val consumedPixelsY = when {
-            scrollDelta.y == 0f -> 0f
+            delta.y == 0f -> 0f
             topEffect.distanceCompat != 0f -> {
-                pullTop(scrollDelta, pointer).also {
+                pullTop(delta, pointer).also {
                     if (topEffect.distanceCompat == 0f) topEffect.onRelease()
                 }
             }
             bottomEffect.distanceCompat != 0f -> {
-                pullBottom(scrollDelta, pointer).also {
+                pullBottom(delta, pointer).also {
                     if (bottomEffect.distanceCompat == 0f) bottomEffect.onRelease()
                 }
             }
             else -> 0f
         }
         val consumedPixelsX = when {
-            scrollDelta.x == 0f -> 0f
+            delta.x == 0f -> 0f
             leftEffect.distanceCompat != 0f -> {
-                pullLeft(scrollDelta, pointer).also {
+                pullLeft(delta, pointer).also {
                     if (leftEffect.distanceCompat == 0f) leftEffect.onRelease()
                 }
             }
             rightEffect.distanceCompat != 0f -> {
-                pullRight(scrollDelta, pointer).also {
+                pullRight(delta, pointer).also {
                     if (rightEffect.distanceCompat == 0f) rightEffect.onRelease()
                 }
             }
@@ -174,39 +175,49 @@
         }
         val consumedOffset = Offset(consumedPixelsX, consumedPixelsY)
         if (consumedOffset != Offset.Zero) invalidateOverscroll()
-        return consumedOffset
-    }
 
-    override fun consumePostScroll(
-        initialDragDelta: Offset,
-        overscrollDelta: Offset,
-        source: NestedScrollSource
-    ) {
-        if (containerSize.isEmpty()) {
-            return
-        }
+        val leftForDelta = delta - consumedOffset
+        val consumedByDelta = performScroll(leftForDelta)
+        val leftForOverscroll = leftForDelta - consumedByDelta
+
         var needsInvalidation = false
         if (source == NestedScrollSource.Drag) {
-            val pointer = pointerPosition ?: containerSize.center
-            if (overscrollDelta.x > 0) {
-                pullLeft(overscrollDelta, pointer)
-            } else if (overscrollDelta.x < 0) {
-                pullRight(overscrollDelta, pointer)
+            // Ignore small deltas (< 0.5) as this usually comes from floating point rounding issues
+            // and can cause scrolling to lock up (b/265363356)
+            val appliedHorizontalOverscroll = if (leftForOverscroll.x > 0.5f) {
+                pullLeft(leftForOverscroll, pointer)
+                true
+            } else if (leftForOverscroll.x < -0.5f) {
+                pullRight(leftForOverscroll, pointer)
+                true
+            } else {
+                false
             }
-            if (overscrollDelta.y > 0) {
-                pullTop(overscrollDelta, pointer)
-            } else if (overscrollDelta.y < 0) {
-                pullBottom(overscrollDelta, pointer)
+            val appliedVerticalOverscroll = if (leftForOverscroll.y > 0.5f) {
+                pullTop(leftForOverscroll, pointer)
+                true
+            } else if (leftForOverscroll.y < -0.5f) {
+                pullBottom(leftForOverscroll, pointer)
+                true
+            } else {
+                false
             }
-            needsInvalidation = overscrollDelta != Offset.Zero || needsInvalidation
+            needsInvalidation = appliedHorizontalOverscroll || appliedVerticalOverscroll
         }
-        needsInvalidation = releaseOppositeOverscroll(initialDragDelta) || needsInvalidation
+        needsInvalidation = releaseOppositeOverscroll(delta) || needsInvalidation
         if (needsInvalidation) invalidateOverscroll()
+
+        return consumedOffset + consumedByDelta
     }
 
-    override suspend fun consumePreFling(velocity: Velocity): Velocity {
+    override suspend fun applyToFling(
+        velocity: Velocity,
+        performFling: suspend (Velocity) -> Velocity
+    ) {
+        // Early return
         if (containerSize.isEmpty()) {
-            return Velocity.Zero
+            performFling(velocity)
+            return
         }
         val consumedX = if (velocity.x > 0f && leftEffect.distanceCompat != 0f) {
             leftEffect.onAbsorbCompat(velocity.x.roundToInt())
@@ -228,25 +239,23 @@
         }
         val consumed = Velocity(consumedX, consumedY)
         if (consumed != Velocity.Zero) invalidateOverscroll()
-        return consumed
-    }
 
-    override suspend fun consumePostFling(velocity: Velocity) {
-        if (containerSize.isEmpty()) {
-            return
-        }
+        val remainingVelocity = velocity - consumed
+        val consumedByVelocity = performFling(remainingVelocity)
+        val leftForOverscroll = remainingVelocity - consumedByVelocity
+
         scrollCycleInProgress = false
-        if (velocity.x > 0) {
-            leftEffect.onAbsorbCompat(velocity.x.roundToInt())
-        } else if (velocity.x < 0) {
-            rightEffect.onAbsorbCompat(-velocity.x.roundToInt())
+        if (leftForOverscroll.x > 0) {
+            leftEffect.onAbsorbCompat(leftForOverscroll.x.roundToInt())
+        } else if (leftForOverscroll.x < 0) {
+            rightEffect.onAbsorbCompat(-leftForOverscroll.x.roundToInt())
         }
-        if (velocity.y > 0) {
-            topEffect.onAbsorbCompat(velocity.y.roundToInt())
-        } else if (velocity.y < 0) {
-            bottomEffect.onAbsorbCompat(-velocity.y.roundToInt())
+        if (leftForOverscroll.y > 0) {
+            topEffect.onAbsorbCompat(leftForOverscroll.y.roundToInt())
+        } else if (leftForOverscroll.y < 0) {
+            bottomEffect.onAbsorbCompat(-leftForOverscroll.y.roundToInt())
         }
-        if (velocity != Velocity.Zero) invalidateOverscroll()
+        if (leftForOverscroll != Velocity.Zero) invalidateOverscroll()
         animateToRelease()
     }
 
@@ -464,57 +473,65 @@
     private fun pullTop(scroll: Offset, displacement: Offset): Float {
         val displacementX: Float = displacement.x / containerSize.width
         val pullY = scroll.y / containerSize.height
-        return topEffect.onPullDistanceCompat(pullY, displacementX) * containerSize.height
+        val consumed = topEffect.onPullDistanceCompat(pullY, displacementX) * containerSize.height
+        // If overscroll is showing, assume we have consumed all the provided scroll, and return
+        // that amount directly to avoid floating point rounding issues (b/265363356)
+        return if (topEffect.distanceCompat != 0f) {
+            scroll.y
+        } else {
+            consumed
+        }
     }
 
     private fun pullBottom(scroll: Offset, displacement: Offset): Float {
         val displacementX: Float = displacement.x / containerSize.width
         val pullY = scroll.y / containerSize.height
-        return -bottomEffect.onPullDistanceCompat(
+        val consumed = -bottomEffect.onPullDistanceCompat(
             -pullY,
             1 - displacementX
         ) * containerSize.height
+        // If overscroll is showing, assume we have consumed all the provided scroll, and return
+        // that amount directly to avoid floating point rounding issues (b/265363356)
+        return if (bottomEffect.distanceCompat != 0f) {
+            scroll.y
+        } else {
+            consumed
+        }
     }
 
     private fun pullLeft(scroll: Offset, displacement: Offset): Float {
         val displacementY: Float = displacement.y / containerSize.height
         val pullX = scroll.x / containerSize.width
-        return leftEffect.onPullDistanceCompat(pullX, 1 - displacementY) * containerSize.width
+        val consumed = leftEffect.onPullDistanceCompat(
+            pullX,
+            1 - displacementY
+        ) * containerSize.width
+        // If overscroll is showing, assume we have consumed all the provided scroll, and return
+        // that amount directly to avoid floating point rounding issues (b/265363356)
+        return if (leftEffect.distanceCompat != 0f) {
+            scroll.x
+        } else {
+            consumed
+        }
     }
 
     private fun pullRight(scroll: Offset, displacement: Offset): Float {
         val displacementY: Float = displacement.y / containerSize.height
         val pullX = scroll.x / containerSize.width
-        return -rightEffect.onPullDistanceCompat(-pullX, displacementY) * containerSize.width
+        val consumed = -rightEffect.onPullDistanceCompat(
+            -pullX,
+            displacementY
+        ) * containerSize.width
+        // If overscroll is showing, assume we have consumed all the provided scroll, and return
+        // that amount directly to avoid floating point rounding issues (b/265363356)
+        return if (rightEffect.distanceCompat != 0f) {
+            scroll.x
+        } else {
+            consumed
+        }
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
-private val NoOpOverscrollEffect = object : OverscrollEffect {
-
-    override fun consumePreScroll(
-        scrollDelta: Offset,
-        source: NestedScrollSource
-    ): Offset = Offset.Zero
-
-    override fun consumePostScroll(
-        initialDragDelta: Offset,
-        overscrollDelta: Offset,
-        source: NestedScrollSource
-    ) {
-    }
-
-    override suspend fun consumePreFling(velocity: Velocity): Velocity = Velocity.Zero
-
-    override suspend fun consumePostFling(velocity: Velocity) {}
-
-    override val isInProgress: Boolean
-        get() = false
-
-    override val effectModifier: Modifier
-        get() = Modifier
-}
-
 /**
  * There is an unwanted behavior in the stretch overscroll effect we have to workaround:
  * When the effect is started it is getting the current RenderNode bounds and clips the content
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/SystemGestureExclusion.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/SystemGestureExclusion.kt
index df702a8..a62fdb5 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/SystemGestureExclusion.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/SystemGestureExclusion.kt
@@ -26,7 +26,6 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.graphics.toAndroidRect
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.OnGloballyPositionedModifier
 import androidx.compose.ui.layout.boundsInRoot
@@ -97,7 +96,13 @@
 
     override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
         val newRect = if (exclusion == null) {
-            coordinates.boundsInRoot().toAndroidRect()
+            val boundsInRoot = coordinates.boundsInRoot()
+            android.graphics.Rect(
+                boundsInRoot.left.roundToInt(),
+                boundsInRoot.top.roundToInt(),
+                boundsInRoot.right.roundToInt(),
+                boundsInRoot.bottom.roundToInt()
+            )
         } else {
             calcBounds(coordinates, exclusion.invoke(coordinates))
         }
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt
index be9c1fa..48c54d1 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt
@@ -268,7 +268,8 @@
     private class PrefetchRequest(
         val index: Int,
         val constraints: Constraints
-    ) : LazyLayoutPrefetchState.PrefetchHandle {
+    ) : @Suppress("SEALED_INHERITOR_IN_DIFFERENT_MODULE")
+    LazyLayoutPrefetchState.PrefetchHandle {
 
         var precomposeHandle: PrecomposedSlotHandle? = null
         var canceled = false
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/BasicMarquee.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt
index d1b621f..048508f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt
@@ -26,6 +26,7 @@
 import androidx.compose.animation.core.infiniteRepeatable
 import androidx.compose.animation.core.repeatable
 import androidx.compose.animation.core.tween
+import androidx.compose.foundation.FixedMotionDurationScale.scaleFactor
 import androidx.compose.foundation.MarqueeAnimationMode.Companion.Immediately
 import androidx.compose.foundation.MarqueeAnimationMode.Companion.WhileFocused
 import androidx.compose.runtime.LaunchedEffect
@@ -37,6 +38,7 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.MotionDurationScale
 import androidx.compose.ui.composed
 import androidx.compose.ui.draw.DrawModifier
 import androidx.compose.ui.focus.FocusState
@@ -62,6 +64,7 @@
 import kotlin.math.roundToInt
 import kotlin.math.sign
 import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.withContext
 
 // From https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/TextView.java;l=736;drc=6d97d6d7215fef247d1a90e05545cac3676f9212
 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@@ -256,29 +259,35 @@
             return
         }
 
-        snapshotFlow {
-            // Don't animate if content fits. (Because coroutines, the int will get boxed anyway.)
-            if (contentWidth <= containerWidth) return@snapshotFlow null
-            if (animationMode == WhileFocused && !hasFocus) return@snapshotFlow null
-            (contentWidth + spacingPx).toFloat()
-        }.collectLatest { contentWithSpacingWidth ->
-            // Don't animate when the content fits.
-            if (contentWithSpacingWidth == null) return@collectLatest
+        // Marquee animations should not be affected by motion accessibility settings.
+        // Wrap the entire flow instead of just the animation calls so kotlin doesn't have to create
+        // an extra CoroutineContext every time the flow emits.
+        withContext(FixedMotionDurationScale) {
+            snapshotFlow {
+                // Don't animate if content fits. (Because coroutines, the int will get boxed
+                // anyway.)
+                if (contentWidth <= containerWidth) return@snapshotFlow null
+                if (animationMode == WhileFocused && !hasFocus) return@snapshotFlow null
+                (contentWidth + spacingPx).toFloat()
+            }.collectLatest { contentWithSpacingWidth ->
+                // Don't animate when the content fits.
+                if (contentWithSpacingWidth == null) return@collectLatest
 
-            val spec = createMarqueeAnimationSpec(
-                iterations,
-                contentWithSpacingWidth,
-                initialDelayMillis,
-                delayMillis,
-                velocity,
-                density
-            )
+                val spec = createMarqueeAnimationSpec(
+                    iterations,
+                    contentWithSpacingWidth,
+                    initialDelayMillis,
+                    delayMillis,
+                    velocity,
+                    density
+                )
 
-            offset.snapTo(0f)
-            try {
-                offset.animateTo(contentWithSpacingWidth, spec)
-            } finally {
                 offset.snapTo(0f)
+                try {
+                    offset.animateTo(contentWithSpacingWidth, spec)
+                } finally {
+                    offset.snapTo(0f)
+                }
             }
         }
     }
@@ -399,4 +408,10 @@
             (fraction * width).roundToInt()
         }
     }
+}
+
+/** A [MotionDurationScale] that always reports a [scaleFactor] of 1. */
+private object FixedMotionDurationScale : MotionDurationScale {
+    override val scaleFactor: Float
+        get() = 1f
 }
\ No newline at end of file
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/Overscroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Overscroll.kt
index b56d000..927a72e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Overscroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Overscroll.kt
@@ -24,107 +24,140 @@
 import androidx.compose.ui.unit.Velocity
 
 /**
- * Controller to control the overscroll logic for a particular scrollable container.
+ * An OverscrollEffect represents a visual effect that displays when the edges of a scrolling
+ * container have been reached with a scroll or fling. For the default platform effect that should
+ * be used in most cases, see
+ * [androidx.compose.foundation.gestures.ScrollableDefaults.overscrollEffect].
  *
- * This entity defines the "how" of the overscroll effect. If the default platform effect is needed,
- * consider using [ScrollableDefaults.overscrollEffect]. In order for overscroll to work,
- * the controller is supposed to be passed to [scrollable] modifier to receive the data of
- * overscroll, and then applied where appropriate using [overscroll] modifier.
+ * OverscrollEffect conceptually 'decorates' scroll / fling events: consuming some of the delta or
+ * velocity before and/or after the event is consumed by the scrolling container. [applyToScroll]
+ * applies overscroll to a scroll event, and [applyToFling] applies overscroll to a fling.
+ *
+ * Higher level components such as [androidx.compose.foundation.lazy.LazyColumn] will automatically
+ * configure an OverscrollEffect for you. To use a custom OverscrollEffect you first need to
+ * provide it with scroll and/or fling events - usually by providing it to a
+ * [androidx.compose.foundation.gestures.scrollable]. Then you can draw the effect on top of the
+ * scrolling content using [Modifier.overscroll].
  *
  * @sample androidx.compose.foundation.samples.OverscrollSample
  */
 @ExperimentalFoundationApi
 @Stable
 interface OverscrollEffect {
-
     /**
-     * Consume any overscroll before the scroll happens if needed.
+     * Applies overscroll to [performScroll]. [performScroll] should represent a drag / scroll, and
+     * returns the amount of delta consumed, so in simple cases the amount of overscroll to show
+     * should be equal to `delta - performScroll(delta)`. The OverscrollEffect can optionally
+     * consume some delta before calling [performScroll], such as to release any existing tension.
+     * The implementation *must* call [performScroll] exactly once. This function should return the
+     * sum of all the delta that was consumed during this operation - both by the overscroll and
+     * [performScroll].
      *
-     * This is usually relevant when the overscroll expresses the effect that is needed to be
-     * negated first before actually scrolling. The example might be a spring-based overscroll,
-     * where you want to release the tension of a spring before starting to scroll to provide a
-     * good user-experience.
+     * For example, assume we want to apply overscroll to a custom component that isn't using
+     * [androidx.compose.foundation.gestures.scrollable]. Here is a simple example of a component
+     * using [androidx.compose.foundation.gestures.draggable] instead:
      *
-     * @param scrollDelta the original delta to scroll
-     * @param source source of the scroll event
+     * @sample androidx.compose.foundation.samples.OverscrollWithDraggable_Before
      *
-     * @return the amount of scroll consumed that won't be available for scrollable container
-     * anymore
+     * To apply overscroll, we need to decorate the existing logic with applyToScroll, and
+     * return the amount of delta we have consumed when updating the drag position. Note that we
+     * also need to call applyToFling - this is used as an end signal for overscroll so that effects
+     * can correctly reset after any animations, when the gesture has stopped.
+     *
+     * @sample androidx.compose.foundation.samples.OverscrollWithDraggable_After
+     *
+     * @param delta total scroll delta available
+     * @param source the source of the delta
+     * @param performScroll the scroll action that the overscroll is applied to. The [Offset]
+     * parameter represents how much delta is available, and the return value is how much delta was
+     * consumed. Any delta that was not consumed should be used to show the overscroll effect.
+     * @return the delta consumed from [delta] by the operation of this function - including that
+     * consumed by [performScroll].
      */
-    fun consumePreScroll(
-        scrollDelta: Offset,
-        source: NestedScrollSource
+    fun applyToScroll(
+        delta: Offset,
+        source: NestedScrollSource,
+        performScroll: (Offset) -> Offset
     ): Offset
 
     /**
-     * Process scroll delta that is available after the scroll iteration is over.
+     * Applies overscroll to [performFling]. [performFling] should represent a fling (the release
+     * of a drag or scroll), and returns the amount of [Velocity] consumed, so in simple cases the
+     * amount of overscroll to show should be equal to `velocity - performFling(velocity)`. The
+     * OverscrollEffect can optionally consume some [Velocity] before calling [performFling], such
+     * as to release any existing tension. The implementation *must* call [performFling] exactly
+     * once.
      *
-     * This is the main method to show an overscroll, as [overscrollDelta] will be a
-     * non-[zero][Offset.Zero] only if the scroll is happening at the bound of a scrollable
-     * container.
+     * For example, assume we want to apply overscroll to a custom component that isn't using
+     * [androidx.compose.foundation.gestures.scrollable]. Here is a simple example of a component
+     * using [androidx.compose.foundation.gestures.draggable] instead:
      *
-     * @param initialDragDelta initial drag delta before any consumption was made
-     * @param overscrollDelta the amount of overscroll left after the scroll process
-     * @param source source of the scroll event
+     * @sample androidx.compose.foundation.samples.OverscrollWithDraggable_Before
+     *
+     * To apply overscroll, we decorate the existing logic with applyToScroll, and return the amount
+     * of delta we have consumed when updating the drag position. We then call applyToFling using
+     * the velocity provided by onDragStopped.
+     *
+     * @sample androidx.compose.foundation.samples.OverscrollWithDraggable_After
+     *
+     * @param velocity total [Velocity] available
+     * @param performFling the [Velocity] consuming lambda that the overscroll is applied to. The
+     * [Velocity] parameter represents how much [Velocity] is available, and the return value is how
+     * much [Velocity] was consumed. Any [Velocity] that was not consumed should be used to show the
+     * overscroll effect.
      */
-    fun consumePostScroll(
-        initialDragDelta: Offset,
-        overscrollDelta: Offset,
-        source: NestedScrollSource
-    )
+    suspend fun applyToFling(velocity: Velocity, performFling: suspend (Velocity) -> Velocity)
 
     /**
-     * Consume any velocity for the over scroll effect before the fling happens.
+     * Whether this OverscrollEffect is currently displaying overscroll.
      *
-     * relevant when the overscroll expresses the effect that is needed to be
-     * negated (or increased) first before actually scrolling. The example might be a spring-based
-     * overscroll, where you want to release the tension of a spring before starting to scroll to
-     * provide a good user-experience.
-     *
-     * @param velocity velocity available to a scrolling container before flinging
-     *
-     * @return the amount of velocity that overscroll effect consumed that won't be available for
-     * fling operation
-     */
-    suspend fun consumePreFling(velocity: Velocity): Velocity
-
-    /**
-     * Feed and process velocity overscroll to show an effect.
-     *
-     * @param velocity the amount of velocity that is left for overscroll after the fling happened.
-     */
-    suspend fun consumePostFling(velocity: Velocity)
-
-    /**
-     * Whether over scroll within this controller is currently on progress or not, e.g. if the
-     * overscroll effect is playing animation or shown/interactable in any other way.
-     *
-     * @return true if there is over scroll happening at the time of the call, false otherwise
+     * @return true if this OverscrollEffect is currently displaying overscroll
      */
     val isInProgress: Boolean
 
     /**
-     * A modifier that will apply the overscroll effect as desired
+     * A [Modifier] that will draw this OverscrollEffect
      */
     val effectModifier: Modifier
 }
 
+/**
+ * Renders overscroll from the provided [overscrollEffect].
+ *
+ * This modifier is a convenience method to call [OverscrollEffect.effectModifier], which
+ * renders the actual effect. Note that this modifier is only responsible for the visual part of
+ * overscroll - on its own it will not handle input events. In addition to using this modifier you
+ * also need to propagate events to the [overscrollEffect], most commonly by using a
+ * [androidx.compose.foundation.gestures.scrollable].
+ *
+ * @sample androidx.compose.foundation.samples.OverscrollSample
+ *
+ * @param overscrollEffect the [OverscrollEffect] to render
+ */
+@ExperimentalFoundationApi
+fun Modifier.overscroll(overscrollEffect: OverscrollEffect): Modifier =
+    this.then(overscrollEffect.effectModifier)
+
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
 internal expect fun rememberOverscrollEffect(): OverscrollEffect
 
-/**
- * Modifier to apply the overscroll as specified by [OverscrollEffect]
- *
- * This modifier is a convenience method to call [OverscrollEffect.effectModifier], which
- * performs the actual overscroll logic. Note that this modifier is the representation of the
- * overscroll on the UI, to make overscroll events to be propagated to the [OverscrollEffect],
- * you have to pass it to [scrollable].
- *
- * @sample androidx.compose.foundation.samples.OverscrollSample
- *
- * @param overscrollEffect controller that defines the behavior and the overscroll state.
- */
-@ExperimentalFoundationApi
-fun Modifier.overscroll(overscrollEffect: OverscrollEffect): Modifier =
-    this.then(overscrollEffect.effectModifier)
\ No newline at end of file
+@OptIn(ExperimentalFoundationApi::class)
+internal object NoOpOverscrollEffect : OverscrollEffect {
+    override fun applyToScroll(
+        delta: Offset,
+        source: NestedScrollSource,
+        performScroll: (Offset) -> Offset
+    ): Offset = performScroll(delta)
+
+    override suspend fun applyToFling(
+        velocity: Velocity,
+        performFling: suspend (Velocity) -> Velocity
+    ) { performFling(velocity) }
+
+    override val isInProgress: Boolean
+        get() = false
+
+    override val effectModifier: Modifier
+        get() = Modifier
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
index 76621b48..c88b73f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
@@ -109,6 +109,11 @@
         }
 
     /**
+     * Size of the viewport on the scrollable axis, or 0 if still unknown.
+     */
+    internal var viewportSize: Int by mutableStateOf(0, structuralEqualityPolicy())
+
+    /**
      * [InteractionSource] that will be used to dispatch drag events when this
      * list is being dragged. If you want to know whether the fling (or smooth scroll) is in
      * progress, use [isScrollInProgress].
@@ -348,6 +353,7 @@
         // measurements inside onRemeasured are able to scroll to the new max based on the newly-
         // measured size.
         scrollerState.maxValue = side
+        scrollerState.viewportSize = if (isVertical) height else width
         return layout(width, height) {
             val scroll = scrollerState.value.coerceIn(0, side)
             val absScroll = if (isReversed) scroll - side else -scroll
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index 4057fb4..54ce08b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -355,61 +355,37 @@
      */
     fun ScrollScope.dispatchScroll(availableDelta: Offset, source: NestedScrollSource): Offset {
         val scrollDelta = availableDelta.singleAxisOffset()
-        val overscrollPreConsumed = overscrollPreConsumeDelta(scrollDelta, source)
 
-        val afterPreOverscroll = scrollDelta - overscrollPreConsumed
-        val nestedScrollDispatcher = nestedScrollDispatcher.value
-        val preConsumedByParent = nestedScrollDispatcher
-            .dispatchPreScroll(afterPreOverscroll, source)
+        val performScroll: (Offset) -> Offset = { delta ->
+            val nestedScrollDispatcher = nestedScrollDispatcher.value
+            val preConsumedByParent = nestedScrollDispatcher
+                .dispatchPreScroll(delta, source)
 
-        val scrollAvailable = afterPreOverscroll - preConsumedByParent
-        // Consume on a single axis
-        val axisConsumed =
-            scrollBy(scrollAvailable.reverseIfNeeded().toFloat()).toOffset().reverseIfNeeded()
+            val scrollAvailable = delta - preConsumedByParent
+            // Consume on a single axis
+            val axisConsumed =
+                scrollBy(scrollAvailable.reverseIfNeeded().toFloat()).toOffset().reverseIfNeeded()
 
-        val leftForParent = scrollAvailable - axisConsumed
-        val parentConsumed = nestedScrollDispatcher.dispatchPostScroll(
-            axisConsumed,
-            leftForParent,
-            source
-        )
-        overscrollPostConsumeDelta(
-            scrollAvailable,
-            leftForParent - parentConsumed,
-            source
-        )
+            val leftForParent = scrollAvailable - axisConsumed
+            val parentConsumed = nestedScrollDispatcher.dispatchPostScroll(
+                axisConsumed,
+                leftForParent,
+                source
+            )
 
-        return overscrollPreConsumed + preConsumedByParent + axisConsumed + parentConsumed
+            preConsumedByParent + axisConsumed + parentConsumed
+        }
+
+        return if (overscrollEffect != null && shouldDispatchOverscroll) {
+            overscrollEffect.applyToScroll(scrollDelta, source, performScroll)
+        } else {
+            performScroll(scrollDelta)
+        }
     }
 
     private val shouldDispatchOverscroll
         get() = scrollableState.canScrollForward || scrollableState.canScrollBackward
 
-    fun overscrollPreConsumeDelta(
-        scrollDelta: Offset,
-        source: NestedScrollSource
-    ): Offset {
-        return if (overscrollEffect != null && shouldDispatchOverscroll) {
-            overscrollEffect.consumePreScroll(scrollDelta, source)
-        } else {
-            Offset.Zero
-        }
-    }
-
-    private fun overscrollPostConsumeDelta(
-        consumedByChain: Offset,
-        availableForOverscroll: Offset,
-        source: NestedScrollSource
-    ) {
-        if (overscrollEffect != null && shouldDispatchOverscroll) {
-            overscrollEffect.consumePostScroll(
-                consumedByChain,
-                availableForOverscroll,
-                source
-            )
-        }
-    }
-
     fun performRawScroll(scroll: Offset): Offset {
         return if (scrollableState.isScrollInProgress) {
             Offset.Zero
@@ -424,25 +400,25 @@
         registerNestedFling(true)
 
         val availableVelocity = initialVelocity.singleAxisVelocity()
-        val preOverscrollConsumed =
-            if (overscrollEffect != null && shouldDispatchOverscroll) {
-                overscrollEffect.consumePreFling(availableVelocity)
-            } else {
-                Velocity.Zero
-            }
-        val velocity = (availableVelocity - preOverscrollConsumed)
-        val preConsumedByParent = nestedScrollDispatcher
-            .value.dispatchPreFling(velocity)
-        val available = velocity - preConsumedByParent
-        val velocityLeft = doFlingAnimation(available)
-        val consumedPost =
-            nestedScrollDispatcher.value.dispatchPostFling(
-                (available - velocityLeft),
-                velocityLeft
-            )
-        val totalLeft = velocityLeft - consumedPost
+
+        val performFling: suspend (Velocity) -> Velocity = { velocity ->
+            val preConsumedByParent = nestedScrollDispatcher
+                .value.dispatchPreFling(velocity)
+            val available = velocity - preConsumedByParent
+            val velocityLeft = doFlingAnimation(available)
+            val consumedPost =
+                nestedScrollDispatcher.value.dispatchPostFling(
+                    (available - velocityLeft),
+                    velocityLeft
+                )
+            val totalLeft = velocityLeft - consumedPost
+            velocity - totalLeft
+        }
+
         if (overscrollEffect != null && shouldDispatchOverscroll) {
-            overscrollEffect.consumePostFling(totalLeft)
+            overscrollEffect.applyToFling(availableVelocity, performFling)
+        } else {
+            performFling(availableVelocity)
         }
 
         // Self stopped flinging, reset
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt
index 397ee29..e34a548 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt
@@ -17,6 +17,10 @@
 package androidx.compose.foundation.gestures
 
 import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.gestures.TransformEvent.TransformDelta
+import androidx.compose.foundation.gestures.TransformEvent.TransformStarted
+import androidx.compose.foundation.gestures.TransformEvent.TransformStopped
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.State
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
@@ -24,22 +28,18 @@
 import androidx.compose.ui.composed
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.pointer.AwaitPointerEventScope
-import androidx.compose.ui.input.pointer.PointerEvent
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerId
 import androidx.compose.ui.input.pointer.PointerInputScope
-import androidx.compose.ui.input.pointer.changedToDown
-import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed
-import androidx.compose.ui.input.pointer.changedToUp
-import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.input.pointer.positionChanged
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.util.fastAny
 import androidx.compose.ui.util.fastForEach
-import kotlinx.coroutines.CancellationException
 import kotlin.math.PI
 import kotlin.math.abs
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.isActive
 
 /**
  * Enable transformation gestures of the modified UI element.
@@ -64,20 +64,40 @@
     enabled: Boolean = true
 ) = composed(
     factory = {
-        val updatedState = rememberUpdatedState(state)
         val updatePanZoomLock = rememberUpdatedState(lockRotationOnZoomPan)
+        val channel = remember { Channel<TransformEvent>(capacity = Channel.UNLIMITED) }
+        if (enabled) {
+            LaunchedEffect(state) {
+                while (isActive) {
+                    var event = channel.receive()
+                    if (event !is TransformStarted) continue
+                    try {
+                        state.transform(MutatePriority.UserInput) {
+                            while (event !is TransformStopped) {
+                                (event as? TransformDelta)?.let {
+                                    transformBy(it.zoomChange, it.panChange, it.rotationChange)
+                                }
+                                event = channel.receive()
+                            }
+                        }
+                    } catch (_: CancellationException) {
+                        // ignore the cancellation and start over again.
+                    }
+                }
+            }
+        }
         val block: suspend PointerInputScope.() -> Unit = remember {
             {
-                /**
-                 * This cannot be converted to awaitEachGesture() because
-                 * [TransformableState.transform] is a suspend function. Unfortunately, this means
-                 * that events can be lost in the middle of a gesture.
-                 *
-                 * TODO(b/251826790) Convert to awaitEachGesture()
-                 */
-                @Suppress("DEPRECATION")
-                forEachGesture {
-                    detectZoom(updatePanZoomLock, updatedState)
+                coroutineScope {
+                    awaitEachGesture {
+                        try {
+                            detectZoom(updatePanZoomLock, channel)
+                        } catch (exception: CancellationException) {
+                            if (!isActive) throw exception
+                        } finally {
+                            channel.trySend(TransformStopped)
+                        }
+                    }
                 }
             }
         }
@@ -91,11 +111,19 @@
     }
 )
 
-// b/242023503 for a planned fix for the MultipleAwaitPointerEventScopes lint violation.
-@Suppress("MultipleAwaitPointerEventScopes")
-private suspend fun PointerInputScope.detectZoom(
+private sealed class TransformEvent {
+    object TransformStarted : TransformEvent()
+    object TransformStopped : TransformEvent()
+    class TransformDelta(
+        val zoomChange: Float,
+        val panChange: Offset,
+        val rotationChange: Float
+    ) : TransformEvent()
+}
+
+private suspend fun AwaitPointerEventScope.detectZoom(
     panZoomLock: State<Boolean>,
-    state: State<TransformableState>
+    channel: Channel<TransformEvent>
 ) {
     var rotation = 0f
     var zoom = 1f
@@ -103,86 +131,49 @@
     var pastTouchSlop = false
     val touchSlop = viewConfiguration.touchSlop
     var lockedToPanZoom = false
-    awaitPointerEventScope {
-        awaitTwoDowns(requireUnconsumed = false)
-    }
-    try {
-        state.value.transform(MutatePriority.UserInput) {
-            awaitPointerEventScope {
-                do {
-                    val event = awaitPointerEvent()
-                    val canceled = event.changes.fastAny { it.isConsumed }
-                    if (!canceled) {
-                        val zoomChange = event.calculateZoom()
-                        val rotationChange = event.calculateRotation()
-                        val panChange = event.calculatePan()
-
-                        if (!pastTouchSlop) {
-                            zoom *= zoomChange
-                            rotation += rotationChange
-                            pan += panChange
-
-                            val centroidSize = event.calculateCentroidSize(useCurrent = false)
-                            val zoomMotion = abs(1 - zoom) * centroidSize
-                            val rotationMotion = abs(rotation * PI.toFloat() * centroidSize / 180f)
-                            val panMotion = pan.getDistance()
-
-                            if (zoomMotion > touchSlop ||
-                                rotationMotion > touchSlop ||
-                                panMotion > touchSlop
-                            ) {
-                                pastTouchSlop = true
-                                lockedToPanZoom = panZoomLock.value && rotationMotion < touchSlop
-                            }
-                        }
-
-                        if (pastTouchSlop) {
-                            val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange
-                            if (effectiveRotation != 0f ||
-                                zoomChange != 1f ||
-                                panChange != Offset.Zero
-                            ) {
-                                transformBy(zoomChange, panChange, effectiveRotation)
-                            }
-                            event.changes.fastForEach {
-                                if (it.positionChanged()) {
-                                    it.consume()
-                                }
-                            }
-                        }
-                    }
-                } while (!canceled && event.changes.fastAny { it.pressed })
-            }
-        }
-    } catch (c: CancellationException) {
-        // cancelled by higher priority, start listening over
-    }
-}
-
-/**
- * Reads events until the first down is received. If [requireUnconsumed] is `true` and the first
- * down is consumed in the [PointerEventPass.Main] pass, that gesture is ignored.
- */
-private suspend fun AwaitPointerEventScope.awaitTwoDowns(requireUnconsumed: Boolean = true) {
-    var event: PointerEvent
-    var firstDown: PointerId? = null
+    awaitFirstDown(requireUnconsumed = false)
     do {
-        event = awaitPointerEvent()
-        var downPointers = if (firstDown != null) 1 else 0
-        event.changes.fastForEach {
-            val isDown =
-                if (requireUnconsumed) it.changedToDown() else it.changedToDownIgnoreConsumed()
-            val isUp =
-                if (requireUnconsumed) it.changedToUp() else it.changedToUpIgnoreConsumed()
-            if (isUp && firstDown == it.id) {
-                firstDown = null
-                downPointers -= 1
+        val event = awaitPointerEvent()
+        val canceled = event.changes.fastAny { it.isConsumed }
+        if (!canceled) {
+            val zoomChange = event.calculateZoom()
+            val rotationChange = event.calculateRotation()
+            val panChange = event.calculatePan()
+
+            if (!pastTouchSlop) {
+                zoom *= zoomChange
+                rotation += rotationChange
+                pan += panChange
+
+                val centroidSize = event.calculateCentroidSize(useCurrent = false)
+                val zoomMotion = abs(1 - zoom) * centroidSize
+                val rotationMotion = abs(rotation * PI.toFloat() * centroidSize / 180f)
+                val panMotion = pan.getDistance()
+
+                if (zoomMotion > touchSlop ||
+                    rotationMotion > touchSlop ||
+                    panMotion > touchSlop
+                ) {
+                    pastTouchSlop = true
+                    lockedToPanZoom = panZoomLock.value && rotationMotion < touchSlop
+                    channel.trySend(TransformStarted)
+                }
             }
-            if (isDown) {
-                firstDown = it.id
-                downPointers += 1
+
+            if (pastTouchSlop) {
+                val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange
+                if (effectiveRotation != 0f ||
+                    zoomChange != 1f ||
+                    panChange != Offset.Zero
+                ) {
+                    channel.trySend(TransformDelta(zoomChange, panChange, effectiveRotation))
+                }
+                event.changes.fastForEach {
+                    if (it.positionChanged()) {
+                        it.consume()
+                    }
+                }
             }
         }
-        val satisfied = downPointers > 1
-    } while (!satisfied)
-}
\ No newline at end of file
+    } while (!canceled && event.changes.fastAny { it.pressed })
+}
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 b6e53ad..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
@@ -115,9 +115,7 @@
         initialVelocity: Float,
         onSettlingDistanceUpdated: (Float) -> Unit
     ): Float {
-        val (remainingOffset, remainingState) = withContext(motionScaleDuration) {
-            fling(initialVelocity, onSettlingDistanceUpdated)
-        }
+        val (remainingOffset, remainingState) = fling(initialVelocity, onSettlingDistanceUpdated)
 
         debugLog { "Post Settling Offset=$remainingOffset" }
         // No remaining offset means we've used everything, no need to propagate velocity. Otherwise
@@ -355,6 +353,8 @@
         density.calculateSnappingOffsetBounds()
     }
 
+    debugLog { "Proposed Bounds: Lower=$lowerBound Upper=$upperBound" }
+
     val finalDistance = when (sign(velocity)) {
         0f -> {
             if (abs(upperBound) <= abs(lowerBound)) {
@@ -412,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/LazyListItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
index ae6229e..0691a11 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
 import androidx.compose.foundation.lazy.layout.IntervalList
 import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyLayoutPinnableItem
 import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.derivedStateOf
@@ -79,7 +80,7 @@
         intervals = intervals,
         nearestItemsRange = nearestItemsRange,
         itemContent = { interval, index ->
-            LazyListPinnableContainerProvider(state, index) {
+            LazyLayoutPinnableItem(index, state.pinnedItems) {
                 interval.value.item.invoke(itemScope, index - interval.startIndex)
             }
         }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfo.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfo.kt
index 730dab9..be71365 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfo.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfo.kt
@@ -82,4 +82,9 @@
      * For example it is a bottom content padding for LazyColumn with reverseLayout set to false.
      */
     val afterContentPadding: Int get() = 0
+
+    /**
+     * The spacing between items in the direction of scrolling.
+     */
+    val mainAxisItemSpacing: Int get() = 0
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
index 104832d7..d580e73 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -16,9 +16,11 @@
 
 package androidx.compose.foundation.lazy
 
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.fastFilter
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.unit.Constraints
@@ -36,6 +38,7 @@
  * Measures and calculates the positions for the requested items. The result is produced
  * as a [LazyListMeasureResult] which contains all the calculations.
  */
+@OptIn(ExperimentalFoundationApi::class)
 internal fun measureLazyList(
     itemsCount: Int,
     itemProvider: LazyMeasuredItemProvider,
@@ -56,7 +59,7 @@
     placementAnimator: LazyListItemPlacementAnimator,
     beyondBoundsInfo: LazyListBeyondBoundsInfo,
     beyondBoundsItemCount: Int,
-    pinnedItems: List<LazyListPinnedItem>,
+    pinnedItems: LazyLayoutPinnedItemList,
     layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
 ): LazyListMeasureResult {
     require(beforeContentPadding >= 0)
@@ -75,7 +78,8 @@
             totalItemsCount = 0,
             reverseLayout = reverseLayout,
             orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
-            afterContentPadding = afterContentPadding
+            afterContentPadding = afterContentPadding,
+            mainAxisItemSpacing = spaceBetweenItems
         )
     } else {
         var currentFirstItemIndex = firstVisibleItemIndex
@@ -326,18 +330,20 @@
             totalItemsCount = itemsCount,
             reverseLayout = reverseLayout,
             orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
-            afterContentPadding = afterContentPadding
+            afterContentPadding = afterContentPadding,
+            mainAxisItemSpacing = spaceBetweenItems
         )
     }
 }
 
+@OptIn(ExperimentalFoundationApi::class)
 private fun createItemsAfterList(
     beyondBoundsInfo: LazyListBeyondBoundsInfo,
     visibleItems: MutableList<LazyMeasuredItem>,
     itemProvider: LazyMeasuredItemProvider,
     itemsCount: Int,
     beyondBoundsItemCount: Int,
-    pinnedItems: List<LazyListPinnedItem>
+    pinnedItems: LazyLayoutPinnedItemList
 ): List<LazyMeasuredItem> {
     fun LazyListBeyondBoundsInfo.endIndex() = min(end, itemsCount - 1)
 
@@ -362,22 +368,23 @@
         addItem(i)
     }
 
-    pinnedItems.fastForEach {
-        if (it.index > end && it.index < itemsCount) {
-            addItem(it.index)
+    pinnedItems.fastForEach { item ->
+        if (item.index > end && item.index < itemsCount) {
+            addItem(item.index)
         }
     }
 
     return list ?: emptyList()
 }
 
+@OptIn(ExperimentalFoundationApi::class)
 private fun createItemsBeforeList(
     beyondBoundsInfo: LazyListBeyondBoundsInfo,
     currentFirstItemIndex: DataIndex,
     itemProvider: LazyMeasuredItemProvider,
     itemsCount: Int,
     beyondBoundsItemCount: Int,
-    pinnedItems: List<LazyListPinnedItem>
+    pinnedItems: LazyLayoutPinnedItemList
 ): List<LazyMeasuredItem> {
     fun LazyListBeyondBoundsInfo.startIndex() = min(start, itemsCount - 1)
 
@@ -402,9 +409,9 @@
         addItem(i)
     }
 
-    pinnedItems.fastForEach {
-        if (it.index < start) {
-            addItem(it.index)
+    pinnedItems.fastForEach { item ->
+        if (item.index < start) {
+            addItem(item.index)
         }
     }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt
index 00805368..7faf587 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt
@@ -49,7 +49,9 @@
     /** see [LazyListLayoutInfo.orientation] */
     override val orientation: Orientation,
     /** see [LazyListLayoutInfo.afterContentPadding] */
-    override val afterContentPadding: Int
+    override val afterContentPadding: Int,
+    /** see [LazyListLayoutInfo.mainAxisItemSpacing] */
+    override val mainAxisItemSpacing: Int
 ) : LazyListLayoutInfo, MeasureResult by measureResult {
     override val viewportSize: IntSize
         get() = IntSize(width, height)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPinnableContainerProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPinnableContainerProvider.kt
deleted file mode 100644
index 6d8ffb8..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPinnableContainerProvider.kt
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.lazy
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.layout.LocalPinnableContainer
-import androidx.compose.ui.layout.PinnableContainer
-
-internal interface LazyListPinnedItem {
-    val index: Int
-}
-
-@Composable
-internal fun LazyListPinnableContainerProvider(
-    state: LazyListState,
-    index: Int,
-    content: @Composable () -> Unit
-) {
-    val pinnableItem = remember(state) { LazyListPinnableItem(state) }
-    pinnableItem.index = index
-    pinnableItem.parentPinnableContainer = LocalPinnableContainer.current
-    DisposableEffect(pinnableItem) { onDispose { pinnableItem.onDisposed() } }
-    CompositionLocalProvider(
-        LocalPinnableContainer provides pinnableItem, content = content
-    )
-}
-
-private class LazyListPinnableItem(
-    private val state: LazyListState,
-) : PinnableContainer, PinnableContainer.PinnedHandle, LazyListPinnedItem {
-    /**
-     * Current index associated with this item.
-     */
-    override var index by mutableStateOf(-1)
-
-    /**
-     * 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].
-     */
-    private var pinsCount by mutableStateOf(0)
-
-    /**
-     * Handle associated with the current [parentPinnableContainer].
-     */
-    private var parentHandle by mutableStateOf<PinnableContainer.PinnedHandle?>(null)
-
-    /**
-     * Current parent [PinnableContainer].
-     * Note that we should correctly re-pin if we pinned the previous container.
-     */
-    private var _parentPinnableContainer by mutableStateOf<PinnableContainer?>(null)
-    var parentPinnableContainer: PinnableContainer? get() = _parentPinnableContainer
-        set(value) {
-            Snapshot.withoutReadObservation {
-                val previous = _parentPinnableContainer
-                if (value !== previous) {
-                    _parentPinnableContainer = value
-                    if (pinsCount > 0) {
-                        parentHandle?.unpin()
-                        parentHandle = value?.pin()
-                    }
-                }
-            }
-        }
-
-    override fun pin(): PinnableContainer.PinnedHandle {
-        if (pinsCount == 0) {
-            state.pinnedItems.add(this)
-            parentHandle = parentPinnableContainer?.pin()
-        }
-        pinsCount++
-        return this
-    }
-
-    override fun unpin() {
-        check(pinsCount > 0) { "Unpin should only be called once" }
-        pinsCount--
-        if (pinsCount == 0) {
-            state.pinnedItems.remove(this)
-            parentHandle?.unpin()
-            parentHandle = null
-        }
-    }
-
-    fun onDisposed() {
-        repeat(pinsCount) {
-            unpin()
-        }
-    }
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
index 906b051..c0756a3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
@@ -24,11 +24,11 @@
 import androidx.compose.foundation.interaction.InteractionSource
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
 import androidx.compose.foundation.lazy.layout.animateScrollToItem
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.saveable.Saver
 import androidx.compose.runtime.saveable.listSaver
@@ -222,9 +222,9 @@
     internal var premeasureConstraints by mutableStateOf(Constraints())
 
     /**
-     * List of extra items to compose during the measure pass.
+     * Stores currently pinned items which are always composed.
      */
-    internal val pinnedItems = mutableStateListOf<LazyListPinnedItem>()
+    internal val pinnedItems = LazyLayoutPinnedItemList()
 
     /**
      * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
@@ -432,6 +432,7 @@
     override val reverseLayout = false
     override val beforeContentPadding = 0
     override val afterContentPadding = 0
+    override val mainAxisItemSpacing = 0
 }
 
 internal class AwaitFirstLayoutModifier : OnGloballyPositionedModifier {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index e643447..7ecc15a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -261,7 +261,7 @@
             this,
             spaceBetweenLines
         ) { index, key, crossAxisSize, mainAxisSpacing, placeables ->
-            LazyMeasuredItem(
+            LazyGridMeasuredItem(
                 index = index,
                 key = key,
                 isVertical = isVertical,
@@ -285,7 +285,7 @@
             measuredItemProvider,
             spanLayoutProvider
         ) { index, items, spans, mainAxisSpacing ->
-            LazyMeasuredLine(
+            LazyGridMeasuredLine(
                 index = index,
                 items = items,
                 spans = spans,
@@ -345,6 +345,7 @@
             density = this,
             placementAnimator = placementAnimator,
             spanLayoutProvider = spanLayoutProvider,
+            pinnedItems = state.pinnedItems,
             layout = { width, height, placement ->
                 layout(
                     containerConstraints.constrainWidth(width + totalHorizontalPadding),
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/grid/LazyGridItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
index f7a2499..1e5f46c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
@@ -58,8 +58,8 @@
     private val movingAwayKeys = LinkedHashSet<Any>()
     private val movingInFromStartBound = mutableListOf<LazyGridPositionedItem>()
     private val movingInFromEndBound = mutableListOf<LazyGridPositionedItem>()
-    private val movingAwayToStartBound = mutableListOf<LazyMeasuredItem>()
-    private val movingAwayToEndBound = mutableListOf<LazyMeasuredItem>()
+    private val movingAwayToStartBound = mutableListOf<LazyGridMeasuredItem>()
+    private val movingAwayToEndBound = mutableListOf<LazyGridMeasuredItem>()
 
     /**
      * Should be called after the measuring so we can detect position changes and start animations.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
index 57d3aed..4f148e7 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
 import androidx.compose.foundation.lazy.layout.IntervalList
 import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyLayoutPinnableItem
 import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
@@ -56,7 +57,8 @@
             LazyGridItemProviderImpl(
                 gridScope.intervals,
                 gridScope.hasCustomSpans,
-                nearestItemsRangeState.value
+                state,
+                nearestItemsRangeState.value,
             )
         }
 
@@ -80,12 +82,15 @@
 private class LazyGridItemProviderImpl(
     private val intervals: IntervalList<LazyGridIntervalContent>,
     override val hasCustomSpans: Boolean,
+    state: LazyGridState,
     nearestItemsRange: IntRange
 ) : LazyGridItemProvider, LazyLayoutItemProvider by LazyLayoutItemProvider(
     intervals = intervals,
     nearestItemsRange = nearestItemsRange,
     itemContent = { interval, index ->
-        interval.value.item.invoke(LazyGridItemScopeImpl, index - interval.startIndex)
+        LazyLayoutPinnableItem(index, state.pinnedItems) {
+            interval.value.item.invoke(LazyGridItemScopeImpl, index - interval.startIndex)
+        }
     }
 ) {
     override val spanLayoutProvider: LazyGridSpanLayoutProvider =
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfo.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfo.kt
index 62a9bfa..a09de86 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfo.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfo.kt
@@ -80,4 +80,9 @@
      * For example it is a bottom content padding for LazyVerticalGrid with reverseLayout set to false.
      */
     val afterContentPadding: Int
+
+    /**
+     * The spacing between lines in the direction of scrolling.
+     */
+    val mainAxisItemSpacing: Int
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
index 1a3f592..c08c7d0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
@@ -17,8 +17,10 @@
 package androidx.compose.foundation.lazy.grid
 
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.fastFilter
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.unit.Constraints
@@ -37,6 +39,7 @@
  * Measures and calculates the positions for the currently visible items. The result is produced
  * as a [LazyGridMeasureResult] which contains all the calculations.
  */
+@OptIn(ExperimentalFoundationApi::class)
 internal fun measureLazyGrid(
     itemsCount: Int,
     measuredLineProvider: LazyMeasuredLineProvider,
@@ -56,6 +59,7 @@
     density: Density,
     placementAnimator: LazyGridItemPlacementAnimator,
     spanLayoutProvider: LazyGridSpanLayoutProvider,
+    pinnedItems: LazyLayoutPinnedItemList,
     layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
 ): LazyGridMeasureResult {
     require(beforeContentPadding >= 0)
@@ -74,7 +78,8 @@
             totalItemsCount = 0,
             reverseLayout = reverseLayout,
             orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
-            afterContentPadding = afterContentPadding
+            afterContentPadding = afterContentPadding,
+            mainAxisItemSpacing = spaceBetweenLines
         )
     } else {
         var currentFirstLineIndex = firstVisibleLineIndex
@@ -94,7 +99,7 @@
         }
 
         // this will contain all the MeasuredItems representing the visible lines
-        val visibleLines = mutableListOf<LazyMeasuredLine>()
+        val visibleLines = mutableListOf<LazyGridMeasuredLine>()
 
         // define min and max offsets
         val minOffset = -beforeContentPadding + if (spaceBetweenLines < 0) spaceBetweenLines else 0
@@ -146,7 +151,6 @@
         ) {
             val measuredLine = measuredLineProvider.getAndMeasure(index)
             if (measuredLine.isEmpty()) {
-                --index
                 break
             }
 
@@ -202,6 +206,22 @@
         val visibleLinesScrollOffset = -currentFirstLineScrollOffset
         var firstLine = visibleLines.first()
 
+        val firstItemIndex = firstLine.items.firstOrNull()?.index?.value ?: 0
+        val lastItemIndex = visibleLines.lastOrNull()?.items?.lastOrNull()?.index?.value ?: 0
+        val extraItemsBefore = calculateExtraItems(
+            pinnedItems,
+            measuredItemProvider,
+            itemConstraints = { measuredLineProvider.itemConstraints(it) },
+            filter = { it in 0 until firstItemIndex }
+        )
+
+        val extraItemsAfter = calculateExtraItems(
+            pinnedItems,
+            measuredItemProvider,
+            itemConstraints = { measuredLineProvider.itemConstraints(it) },
+            filter = { it in (lastItemIndex + 1) until itemsCount }
+        )
+
         // even if we compose lines to fill before content padding we should ignore lines fully
         // located there for the state's scroll position calculation (first line + first offset)
         if (beforeContentPadding > 0 || spaceBetweenLines < 0) {
@@ -230,6 +250,8 @@
 
         val positionedItems = calculateItemsOffsets(
             lines = visibleLines,
+            itemsBefore = extraItemsBefore,
+            itemsAfter = extraItemsAfter,
             layoutWidth = layoutWidth,
             layoutHeight = layoutHeight,
             finalMainAxisOffset = currentMainAxisOffset,
@@ -254,28 +276,61 @@
         return LazyGridMeasureResult(
             firstVisibleLine = firstLine,
             firstVisibleLineScrollOffset = currentFirstLineScrollOffset,
-            canScrollForward = index.value < itemsCount || currentMainAxisOffset > maxOffset,
+            canScrollForward =
+                lastItemIndex != itemsCount - 1 || currentMainAxisOffset > maxOffset,
             consumedScroll = consumedScroll,
             measureResult = layout(layoutWidth, layoutHeight) {
                 positionedItems.fastForEach { it.place(this) }
             },
             viewportStartOffset = -beforeContentPadding,
             viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
-            visibleItemsInfo = positionedItems,
+            visibleItemsInfo = if (extraItemsBefore.isEmpty() && extraItemsAfter.isEmpty()) {
+                positionedItems
+            } else {
+                positionedItems.fastFilter {
+                    it.index in firstItemIndex..lastItemIndex
+                }
+            },
             totalItemsCount = itemsCount,
             reverseLayout = reverseLayout,
             orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
-            afterContentPadding = afterContentPadding
+            afterContentPadding = afterContentPadding,
+            mainAxisItemSpacing = spaceBetweenLines
         )
     }
 }
 
+@ExperimentalFoundationApi
+private inline fun calculateExtraItems(
+    pinnedItems: LazyLayoutPinnedItemList,
+    itemProvider: LazyMeasuredItemProvider,
+    itemConstraints: (ItemIndex) -> Constraints,
+    filter: (Int) -> Boolean
+): List<LazyGridMeasuredItem> {
+    var items: MutableList<LazyGridMeasuredItem>? = null
+
+    pinnedItems.fastForEach { item ->
+        if (filter(item.index)) {
+            val itemIndex = ItemIndex(item.index)
+            val constraints = itemConstraints(itemIndex)
+            val measuredItem = itemProvider.getAndMeasure(itemIndex, constraints = constraints)
+            if (items == null) {
+                items = mutableListOf()
+            }
+            items?.add(measuredItem)
+        }
+    }
+
+    return items ?: emptyList()
+}
+
 /**
- * Calculates [LazyMeasuredLine]s offsets.
+ * Calculates [LazyGridMeasuredLine]s offsets.
  */
-@OptIn(ExperimentalFoundationApi::class)
 private fun calculateItemsOffsets(
-    lines: List<LazyMeasuredLine>,
+    lines: List<LazyGridMeasuredLine>,
+    itemsBefore: List<LazyGridMeasuredItem>,
+    itemsAfter: List<LazyGridMeasuredItem>,
     layoutWidth: Int,
     layoutHeight: Int,
     finalMainAxisOffset: Int,
@@ -296,6 +351,7 @@
     val positionedItems = ArrayList<LazyGridPositionedItem>(lines.fastSumBy { it.items.size })
 
     if (hasSpareSpace) {
+        require(itemsBefore.isEmpty() && itemsAfter.isEmpty())
         val linesCount = lines.size
         fun Int.reverseAware() =
             if (!reverseLayout) this else linesCount - this - 1
@@ -334,10 +390,36 @@
         }
     } else {
         var currentMainAxis = firstLineScrollOffset
+
+        itemsBefore.fastForEach {
+            currentMainAxis -= it.mainAxisSizeWithSpacings
+            positionedItems.add(it.positionExtraItem(currentMainAxis, layoutWidth, layoutHeight))
+        }
+
+        currentMainAxis = firstLineScrollOffset
         lines.fastForEach {
             positionedItems.addAll(it.position(currentMainAxis, layoutWidth, layoutHeight))
             currentMainAxis += it.mainAxisSizeWithSpacings
         }
+
+        itemsAfter.fastForEach {
+            positionedItems.add(it.positionExtraItem(currentMainAxis, layoutWidth, layoutHeight))
+            currentMainAxis += it.mainAxisSizeWithSpacings
+        }
     }
     return positionedItems
 }
+
+private fun LazyGridMeasuredItem.positionExtraItem(
+    mainAxisOffset: Int,
+    layoutWidth: Int,
+    layoutHeight: Int
+): LazyGridPositionedItem =
+    position(
+        mainAxisOffset = mainAxisOffset,
+        crossAxisOffset = 0,
+        layoutWidth = layoutWidth,
+        layoutHeight = layoutHeight,
+        row = 0,
+        column = 0
+    )
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt
index 8218fac..00dfd71 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt
@@ -28,7 +28,7 @@
 internal class LazyGridMeasureResult(
     // properties defining the scroll position:
     /** The new first visible line of items.*/
-    val firstVisibleLine: LazyMeasuredLine?,
+    val firstVisibleLine: LazyGridMeasuredLine?,
     /** The new value for [LazyGridState.firstVisibleItemScrollOffset].*/
     val firstVisibleLineScrollOffset: Int,
     /** True if there is some space available to continue scrolling in the forward direction.*/
@@ -51,7 +51,9 @@
     /** see [LazyGridLayoutInfo.orientation] */
     override val orientation: Orientation,
     /** see [LazyGridLayoutInfo.afterContentPadding] */
-    override val afterContentPadding: Int
+    override val afterContentPadding: Int,
+    /** see [LazyGridLayoutInfo.mainAxisItemSpacing] */
+    override val mainAxisItemSpacing: Int
 ) : LazyGridLayoutInfo, MeasureResult by measureResult {
     override val viewportSize: IntSize
         get() = IntSize(width, height)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt
new file mode 100644
index 0000000..eff4278
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.grid
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastForEach
+
+/**
+ * Represents one measured item of the lazy grid. It can in fact consist of multiple placeables
+ * if the user emit multiple layout nodes in the item callback.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyGridMeasuredItem(
+    val index: ItemIndex,
+    val key: Any,
+    private val isVertical: Boolean,
+    /**
+     * Cross axis size is the same for all [placeables]. Take it as parameter for the case when
+     * [placeables] is empty.
+     */
+    val crossAxisSize: Int,
+    val mainAxisSpacing: Int,
+    private val reverseLayout: Boolean,
+    private val layoutDirection: LayoutDirection,
+    private val beforeContentPadding: Int,
+    private val afterContentPadding: Int,
+    val placeables: List<Placeable>,
+    private val placementAnimator: LazyGridItemPlacementAnimator,
+    /**
+     * The offset which shouldn't affect any calculations but needs to be applied for the final
+     * value passed into the place() call.
+     */
+    private val visualOffset: IntOffset
+) {
+    /**
+     * Main axis size of the item - the max main axis size of the placeables.
+     */
+    val mainAxisSize: Int
+
+    /**
+     * The max main axis size of the placeables plus mainAxisSpacing.
+     */
+    val mainAxisSizeWithSpacings: Int
+
+    init {
+        var maxMainAxis = 0
+        placeables.fastForEach {
+            maxMainAxis = maxOf(maxMainAxis, if (isVertical) it.height else it.width)
+        }
+        mainAxisSize = maxMainAxis
+        mainAxisSizeWithSpacings = (maxMainAxis + mainAxisSpacing).coerceAtLeast(0)
+    }
+
+    /**
+     * Calculates positions for the inner placeables at [mainAxisOffset], [crossAxisOffset].
+     * [layoutWidth] and [layoutHeight] should be provided to not place placeables which are ended
+     * up outside of the viewport (for example one item consist of 2 placeables, and the first one
+     * is not going to be visible, so we don't place it as an optimization, but place the second
+     * one). If [reverseOrder] is true the inner placeables would be placed in the inverted order.
+     */
+    fun position(
+        mainAxisOffset: Int,
+        crossAxisOffset: Int,
+        layoutWidth: Int,
+        layoutHeight: Int,
+        row: Int,
+        column: Int
+    ): LazyGridPositionedItem {
+        val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
+        val crossAxisLayoutSize = if (isVertical) layoutWidth else layoutHeight
+        @Suppress("NAME_SHADOWING")
+        val crossAxisOffset = if (isVertical && layoutDirection == LayoutDirection.Rtl) {
+            crossAxisLayoutSize - crossAxisOffset - crossAxisSize
+        } else {
+            crossAxisOffset
+        }
+        return LazyGridPositionedItem(
+            offset = if (isVertical) {
+                IntOffset(crossAxisOffset, mainAxisOffset)
+            } else {
+                IntOffset(mainAxisOffset, crossAxisOffset)
+            },
+            index = index.value,
+            key = key,
+            row = row,
+            column = column,
+            size = if (isVertical) {
+                IntSize(crossAxisSize, mainAxisSize)
+            } else {
+                IntSize(mainAxisSize, crossAxisSize)
+            },
+            minMainAxisOffset = -beforeContentPadding,
+            maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding,
+            isVertical = isVertical,
+            placeables = placeables,
+            placementAnimator = placementAnimator,
+            visualOffset = visualOffset,
+            mainAxisLayoutSize = mainAxisLayoutSize,
+            reverseLayout = reverseLayout
+        )
+    }
+}
+
+internal class LazyGridPositionedItem(
+    override val offset: IntOffset,
+    override val index: Int,
+    override val key: Any,
+    override val row: Int,
+    override val column: Int,
+    override val size: IntSize,
+    private val minMainAxisOffset: Int,
+    private val maxMainAxisOffset: Int,
+    private val isVertical: Boolean,
+    private val placeables: List<Placeable>,
+    private val placementAnimator: LazyGridItemPlacementAnimator,
+    private val visualOffset: IntOffset,
+    private val mainAxisLayoutSize: Int,
+    private val reverseLayout: Boolean
+) : LazyGridItemInfo {
+    val placeablesCount: Int get() = placeables.size
+
+    fun getMainAxisSize(index: Int) = placeables[index].mainAxisSize
+
+    fun getMainAxisSize() = if (isVertical) size.height else size.width
+
+    fun getCrossAxisSize() = if (isVertical) size.width else size.height
+
+    fun getCrossAxisOffset() = if (isVertical) offset.x else offset.y
+
+    @Suppress("UNCHECKED_CAST")
+    fun getAnimationSpec(index: Int) =
+        placeables[index].parentData as? FiniteAnimationSpec<IntOffset>?
+
+    val hasAnimations = run {
+        repeat(placeablesCount) { index ->
+            if (getAnimationSpec(index) != null) {
+                return@run true
+            }
+        }
+        false
+    }
+
+    fun place(
+        scope: Placeable.PlacementScope,
+    ) = with(scope) {
+        repeat(placeablesCount) { index ->
+            val placeable = placeables[index]
+            val minOffset = minMainAxisOffset - placeable.mainAxisSize
+            val maxOffset = maxMainAxisOffset
+            val offset = if (getAnimationSpec(index) != null) {
+                placementAnimator.getAnimatedOffset(
+                    key, index, minOffset, maxOffset, offset
+                )
+            } else {
+                offset
+            }
+
+            val reverseLayoutAwareOffset = if (reverseLayout) {
+                offset.copy { mainAxisOffset ->
+                    mainAxisLayoutSize - mainAxisOffset - placeable.mainAxisSize
+                }
+            } else {
+                offset
+            }
+            if (isVertical) {
+                placeable.placeWithLayer(reverseLayoutAwareOffset + visualOffset)
+            } else {
+                placeable.placeRelativeWithLayer(reverseLayoutAwareOffset + visualOffset)
+            }
+        }
+    }
+
+    private val Placeable.mainAxisSize get() = if (isVertical) height else width
+    private inline fun IntOffset.copy(mainAxisMap: (Int) -> Int): IntOffset =
+        IntOffset(if (isVertical) x else mainAxisMap(x), if (isVertical) mainAxisMap(y) else y)
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLine.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLine.kt
new file mode 100644
index 0000000..efc095e
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLine.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.ui.unit.LayoutDirection
+
+/**
+ * Represents one measured line of the lazy list. Each item on the line can in fact consist of
+ * multiple placeables if the user emit multiple layout nodes in the item callback.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyGridMeasuredLine constructor(
+    val index: LineIndex,
+    val items: Array<LazyGridMeasuredItem>,
+    private val spans: List<GridItemSpan>,
+    private val isVertical: Boolean,
+    private val slotsPerLine: Int,
+    private val layoutDirection: LayoutDirection,
+    /**
+     * Spacing to be added after [mainAxisSize], in the main axis direction.
+     */
+    private val mainAxisSpacing: Int,
+    private val crossAxisSpacing: Int
+) {
+    /**
+     * Main axis size of the line - the max main axis size of the items on the line.
+     */
+    val mainAxisSize: Int
+
+    /**
+     * Sum of [mainAxisSpacing] and the max of the main axis sizes of the placeables on the line.
+     */
+    val mainAxisSizeWithSpacings: Int
+
+    init {
+        var maxMainAxis = 0
+        items.forEach { item ->
+            maxMainAxis = maxOf(maxMainAxis, item.mainAxisSize)
+        }
+        mainAxisSize = maxMainAxis
+        mainAxisSizeWithSpacings = (maxMainAxis + mainAxisSpacing).coerceAtLeast(0)
+    }
+
+    /**
+     * Whether this line contains any items.
+     */
+    fun isEmpty() = items.isEmpty()
+
+    /**
+     * Calculates positions for the [items] at [offset] main axis position.
+     * If [reverseOrder] is true the [items] would be placed in the inverted order.
+     */
+    fun position(
+        offset: Int,
+        layoutWidth: Int,
+        layoutHeight: Int
+    ): List<LazyGridPositionedItem> {
+        var usedCrossAxis = 0
+        var usedSpan = 0
+        return items.mapIndexed { itemIndex, item ->
+            val span = spans[itemIndex].currentLineSpan
+            val startSlot = if (layoutDirection == LayoutDirection.Rtl) {
+                slotsPerLine - usedSpan - span
+            } else {
+                usedSpan
+            }
+
+            item.position(
+                mainAxisOffset = offset,
+                crossAxisOffset = usedCrossAxis,
+                layoutWidth = layoutWidth,
+                layoutHeight = layoutHeight,
+                row = if (isVertical) index.value else startSlot,
+                column = if (isVertical) startSlot else index.value
+            ).also {
+                usedCrossAxis += item.crossAxisSize + crossAxisSpacing
+                usedSpan += span
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
index f7d777b..e4d0e7b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
@@ -210,7 +210,7 @@
         return LineIndex(currentLine)
     }
 
-    private fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemProvider) {
+    fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemProvider) {
         with(LazyGridItemSpanScopeImpl) {
             maxCurrentLineSpan = maxSpan
             maxLineSpan = slotsPerLine
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
index a19e3d2..25fd5f5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
@@ -25,6 +25,7 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.lazy.AwaitFirstLayoutModifier
 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
 import androidx.compose.foundation.lazy.layout.animateScrollToItem
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
@@ -81,6 +82,7 @@
     firstVisibleItemIndex: Int = 0,
     firstVisibleItemScrollOffset: Int = 0
 ) : ScrollableState {
+
     /**
      * The holder class for the current scroll position.
      */
@@ -226,6 +228,11 @@
     private val animateScrollScope = LazyGridAnimateScrollScope(this)
 
     /**
+     * Stores currently pinned items which are always composed.
+     */
+    internal val pinnedItems = LazyLayoutPinnedItemList()
+
+    /**
      * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
      * pixels.
      *
@@ -448,4 +455,5 @@
     override val reverseLayout = false
     override val beforeContentPadding: Int = 0
     override val afterContentPadding: Int = 0
+    override val mainAxisItemSpacing = 0
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt
deleted file mode 100644
index dcf4450..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.lazy.grid
-
-import androidx.compose.animation.core.FiniteAnimationSpec
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.util.fastForEach
-
-/**
- * Represents one measured item of the lazy grid. It can in fact consist of multiple placeables
- * if the user emit multiple layout nodes in the item callback.
- */
-@OptIn(ExperimentalFoundationApi::class)
-internal class LazyMeasuredItem(
-    val index: ItemIndex,
-    val key: Any,
-    private val isVertical: Boolean,
-    /**
-     * Cross axis size is the same for all [placeables]. Take it as parameter for the case when
-     * [placeables] is empty.
-     */
-    val crossAxisSize: Int,
-    val mainAxisSpacing: Int,
-    private val reverseLayout: Boolean,
-    private val layoutDirection: LayoutDirection,
-    private val beforeContentPadding: Int,
-    private val afterContentPadding: Int,
-    val placeables: List<Placeable>,
-    private val placementAnimator: LazyGridItemPlacementAnimator,
-    /**
-     * The offset which shouldn't affect any calculations but needs to be applied for the final
-     * value passed into the place() call.
-     */
-    private val visualOffset: IntOffset
-) {
-    /**
-     * Main axis size of the item - the max main axis size of the placeables.
-     */
-    val mainAxisSize: Int
-
-    /**
-     * The max main axis size of the placeables plus mainAxisSpacing.
-     */
-    val mainAxisSizeWithSpacings: Int
-
-    init {
-        var maxMainAxis = 0
-        placeables.fastForEach {
-            maxMainAxis = maxOf(maxMainAxis, if (isVertical) it.height else it.width)
-        }
-        mainAxisSize = maxMainAxis
-        mainAxisSizeWithSpacings = (maxMainAxis + mainAxisSpacing).coerceAtLeast(0)
-    }
-
-    /**
-     * Calculates positions for the inner placeables at [mainAxisOffset], [crossAxisOffset].
-     * [layoutWidth] and [layoutHeight] should be provided to not place placeables which are ended
-     * up outside of the viewport (for example one item consist of 2 placeables, and the first one
-     * is not going to be visible, so we don't place it as an optimization, but place the second
-     * one). If [reverseOrder] is true the inner placeables would be placed in the inverted order.
-     */
-    fun position(
-        mainAxisOffset: Int,
-        crossAxisOffset: Int,
-        layoutWidth: Int,
-        layoutHeight: Int,
-        row: Int,
-        column: Int
-    ): LazyGridPositionedItem {
-        val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
-        val crossAxisLayoutSize = if (isVertical) layoutWidth else layoutHeight
-        @Suppress("NAME_SHADOWING")
-        val crossAxisOffset = if (isVertical && layoutDirection == LayoutDirection.Rtl) {
-            crossAxisLayoutSize - crossAxisOffset - crossAxisSize
-        } else {
-            crossAxisOffset
-        }
-        return LazyGridPositionedItem(
-            offset = if (isVertical) {
-                IntOffset(crossAxisOffset, mainAxisOffset)
-            } else {
-                IntOffset(mainAxisOffset, crossAxisOffset)
-            },
-            index = index.value,
-            key = key,
-            row = row,
-            column = column,
-            size = if (isVertical) {
-                IntSize(crossAxisSize, mainAxisSize)
-            } else {
-                IntSize(mainAxisSize, crossAxisSize)
-            },
-            minMainAxisOffset = -beforeContentPadding,
-            maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding,
-            isVertical = isVertical,
-            placeables = placeables,
-            placementAnimator = placementAnimator,
-            visualOffset = visualOffset,
-            mainAxisLayoutSize = mainAxisLayoutSize,
-            reverseLayout = reverseLayout
-        )
-    }
-}
-
-internal class LazyGridPositionedItem(
-    override val offset: IntOffset,
-    override val index: Int,
-    override val key: Any,
-    override val row: Int,
-    override val column: Int,
-    override val size: IntSize,
-    private val minMainAxisOffset: Int,
-    private val maxMainAxisOffset: Int,
-    private val isVertical: Boolean,
-    private val placeables: List<Placeable>,
-    private val placementAnimator: LazyGridItemPlacementAnimator,
-    private val visualOffset: IntOffset,
-    private val mainAxisLayoutSize: Int,
-    private val reverseLayout: Boolean
-) : LazyGridItemInfo {
-    val placeablesCount: Int get() = placeables.size
-
-    fun getMainAxisSize(index: Int) = placeables[index].mainAxisSize
-
-    fun getMainAxisSize() = if (isVertical) size.height else size.width
-
-    fun getCrossAxisSize() = if (isVertical) size.width else size.height
-
-    fun getCrossAxisOffset() = if (isVertical) offset.x else offset.y
-
-    @Suppress("UNCHECKED_CAST")
-    fun getAnimationSpec(index: Int) =
-        placeables[index].parentData as? FiniteAnimationSpec<IntOffset>?
-
-    val hasAnimations = run {
-        repeat(placeablesCount) { index ->
-            if (getAnimationSpec(index) != null) {
-                return@run true
-            }
-        }
-        false
-    }
-
-    fun place(
-        scope: Placeable.PlacementScope,
-    ) = with(scope) {
-        repeat(placeablesCount) { index ->
-            val placeable = placeables[index]
-            val minOffset = minMainAxisOffset - placeable.mainAxisSize
-            val maxOffset = maxMainAxisOffset
-            val offset = if (getAnimationSpec(index) != null) {
-                placementAnimator.getAnimatedOffset(
-                    key, index, minOffset, maxOffset, offset
-                )
-            } else {
-                offset
-            }
-
-            val reverseLayoutAwareOffset = if (reverseLayout) {
-                offset.copy { mainAxisOffset ->
-                    mainAxisLayoutSize - mainAxisOffset - placeable.mainAxisSize
-                }
-            } else {
-                offset
-            }
-            if (isVertical) {
-                placeable.placeWithLayer(reverseLayoutAwareOffset + visualOffset)
-            } else {
-                placeable.placeRelativeWithLayer(reverseLayoutAwareOffset + visualOffset)
-            }
-        }
-    }
-
-    private val Placeable.mainAxisSize get() = if (isVertical) height else width
-    private inline fun IntOffset.copy(mainAxisMap: (Int) -> Int): IntOffset =
-        IntOffset(if (isVertical) x else mainAxisMap(x), if (isVertical) mainAxisMap(y) else y)
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt
index 68d6504..651aa4f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt
@@ -33,13 +33,13 @@
 ) {
     /**
      * Used to subcompose individual items of lazy grids. Composed placeables will be measured
-     * with the provided [constraints] and wrapped into [LazyMeasuredItem].
+     * with the provided [constraints] and wrapped into [LazyGridMeasuredItem].
      */
     fun getAndMeasure(
         index: ItemIndex,
         mainAxisSpacing: Int = defaultMainAxisSpacing,
         constraints: Constraints
-    ): LazyMeasuredItem {
+    ): LazyGridMeasuredItem {
         val key = itemProvider.getKey(index.value)
         val placeables = measureScope.measure(index.value, constraints)
         val crossAxisSize = if (constraints.hasFixedWidth) {
@@ -72,5 +72,5 @@
         crossAxisSize: Int,
         mainAxisSpacing: Int,
         placeables: List<Placeable>
-    ): LazyMeasuredItem
+    ): LazyGridMeasuredItem
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt
deleted file mode 100644
index 2c9279f..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.lazy.grid
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.ui.unit.LayoutDirection
-
-/**
- * Represents one measured line of the lazy list. Each item on the line can in fact consist of
- * multiple placeables if the user emit multiple layout nodes in the item callback.
- */
-@OptIn(ExperimentalFoundationApi::class)
-internal class LazyMeasuredLine constructor(
-    val index: LineIndex,
-    val items: Array<LazyMeasuredItem>,
-    private val spans: List<GridItemSpan>,
-    private val isVertical: Boolean,
-    private val slotsPerLine: Int,
-    private val layoutDirection: LayoutDirection,
-    /**
-     * Spacing to be added after [mainAxisSize], in the main axis direction.
-     */
-    private val mainAxisSpacing: Int,
-    private val crossAxisSpacing: Int
-) {
-    /**
-     * Main axis size of the line - the max main axis size of the items on the line.
-     */
-    val mainAxisSize: Int
-
-    /**
-     * Sum of [mainAxisSpacing] and the max of the main axis sizes of the placeables on the line.
-     */
-    val mainAxisSizeWithSpacings: Int
-
-    init {
-        var maxMainAxis = 0
-        items.forEach { item ->
-            maxMainAxis = maxOf(maxMainAxis, item.mainAxisSize)
-        }
-        mainAxisSize = maxMainAxis
-        mainAxisSizeWithSpacings = (maxMainAxis + mainAxisSpacing).coerceAtLeast(0)
-    }
-
-    /**
-     * Whether this line contains any items.
-     */
-    fun isEmpty() = items.isEmpty()
-
-    /**
-     * Calculates positions for the [items] at [offset] main axis position.
-     * If [reverseOrder] is true the [items] would be placed in the inverted order.
-     */
-    fun position(
-        offset: Int,
-        layoutWidth: Int,
-        layoutHeight: Int
-    ): List<LazyGridPositionedItem> {
-        var usedCrossAxis = 0
-        var usedSpan = 0
-        return items.mapIndexed { itemIndex, item ->
-            val span = spans[itemIndex].currentLineSpan
-            val startSlot = if (layoutDirection == LayoutDirection.Rtl) {
-                slotsPerLine - usedSpan - span
-            } else {
-                usedSpan
-            }
-
-            item.position(
-                mainAxisOffset = offset,
-                crossAxisOffset = usedCrossAxis,
-                layoutWidth = layoutWidth,
-                layoutHeight = layoutHeight,
-                row = if (isVertical) index.value else startSlot,
-                column = if (isVertical) startSlot else index.value
-            ).also {
-                usedCrossAxis += item.crossAxisSize + crossAxisSpacing
-                usedSpan += span
-            }
-        }
-    }
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt
index 588ae51..0b7296e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt
@@ -46,11 +46,19 @@
         }
     }
 
+    fun itemConstraints(itemIndex: ItemIndex): Constraints {
+        val span = spanLayoutProvider.spanOf(
+            itemIndex.value,
+            spanLayoutProvider.slotsPerLine
+        )
+        return childConstraints(0, span)
+    }
+
     /**
      * Used to subcompose items on lines of lazy grids. Composed placeables will be measured
-     * with the correct constraints and wrapped into [LazyMeasuredLine].
+     * with the correct constraints and wrapped into [LazyGridMeasuredLine].
      */
-    fun getAndMeasure(lineIndex: LineIndex): LazyMeasuredLine {
+    fun getAndMeasure(lineIndex: LineIndex): LazyGridMeasuredLine {
         val lineConfiguration = spanLayoutProvider.getLineConfiguration(lineIndex.value)
         val lineItemsCount = lineConfiguration.spans.size
 
@@ -93,8 +101,8 @@
 internal fun interface MeasuredLineFactory {
     fun createLine(
         index: LineIndex,
-        items: Array<LazyMeasuredItem>,
+        items: Array<LazyGridMeasuredItem>,
         spans: List<GridItemSpan>,
         mainAxisSpacing: Int
-    ): LazyMeasuredLine
+    ): LazyGridMeasuredLine
 }
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/LazyLayoutPinnableItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPinnableItem.kt
new file mode 100644
index 0000000..37bd32f
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPinnableItem.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.layout
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.ui.layout.LocalPinnableContainer
+import androidx.compose.ui.layout.PinnableContainer
+
+/**
+ * Wrapper supporting [PinnableContainer] in lazy layout items. Each pinned item
+ * is considered important to keep alive even if it would be discarded otherwise.
+ *
+ * @param index current index of the item inside the lazy layout
+ * @param pinnedItemList container to keep currently pinned items
+ * @param content inner content of this item
+ */
+@ExperimentalFoundationApi
+@Composable
+fun LazyLayoutPinnableItem(
+    index: Int,
+    pinnedItemList: LazyLayoutPinnedItemList,
+    content: @Composable () -> Unit
+) {
+    val pinnableItem = remember(pinnedItemList) { LazyLayoutPinnableItem(pinnedItemList) }
+    pinnableItem.index = index
+    pinnableItem.parentPinnableContainer = LocalPinnableContainer.current
+    DisposableEffect(pinnableItem) { onDispose { pinnableItem.onDisposed() } }
+    CompositionLocalProvider(
+        LocalPinnableContainer provides pinnableItem, content = content
+    )
+}
+
+/**
+ * Read-only list of pinned items in a lazy layout.
+ * The items are modified internally by the [PinnableContainer] consumers, for example if something
+ * inside item content is focused.
+ */
+@ExperimentalFoundationApi
+class LazyLayoutPinnedItemList private constructor(
+    private val items: MutableList<PinnedItem>
+) : List<LazyLayoutPinnedItemList.PinnedItem> by items {
+    constructor() : this(SnapshotStateList())
+
+    internal fun pin(item: PinnedItem) {
+        items.add(item)
+    }
+
+    internal fun release(item: PinnedItem) {
+        items.remove(item)
+    }
+
+    /**
+     * Item pinned in a lazy layout. Pinned item should be always measured and laid out,
+     * even if the item is beyond the boundaries of the layout.
+     */
+    @ExperimentalFoundationApi
+    sealed interface PinnedItem {
+        /**
+         * Index of the pinned item.
+         * Note: it is possible for index to change during lifetime of the object.
+         */
+        val index: Int
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private class LazyLayoutPinnableItem(
+    private val pinnedItemList: LazyLayoutPinnedItemList,
+) : PinnableContainer, PinnableContainer.PinnedHandle, LazyLayoutPinnedItemList.PinnedItem {
+    /**
+     * Current index associated with this item.
+     */
+    override var index by mutableStateOf(-1)
+
+    /**
+     * 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 [release].
+     */
+    private var pinsCount by mutableStateOf(0)
+
+    /**
+     * Handle associated with the current [parentPinnableContainer].
+     */
+    private var parentHandle by mutableStateOf<PinnableContainer.PinnedHandle?>(null)
+
+    /**
+     * Current parent [PinnableContainer].
+     * Note that we should correctly re-pin if we pinned the previous container.
+     */
+    private var _parentPinnableContainer by mutableStateOf<PinnableContainer?>(null)
+    var parentPinnableContainer: PinnableContainer? get() = _parentPinnableContainer
+        set(value) {
+            Snapshot.withoutReadObservation {
+                val previous = _parentPinnableContainer
+                if (value !== previous) {
+                    _parentPinnableContainer = value
+                    if (pinsCount > 0) {
+                        parentHandle?.release()
+                        parentHandle = value?.pin()
+                    }
+                }
+            }
+        }
+
+    override fun pin(): PinnableContainer.PinnedHandle {
+        if (pinsCount == 0) {
+            pinnedItemList.pin(this)
+            parentHandle = parentPinnableContainer?.pin()
+        }
+        pinsCount++
+        return this
+    }
+
+    override fun release() {
+        check(pinsCount > 0) { "Release should only be called once" }
+        pinsCount--
+        if (pinsCount == 0) {
+            pinnedItemList.release(this)
+            parentHandle?.release()
+            parentHandle = null
+        }
+    }
+
+    fun onDisposed() {
+        repeat(pinsCount) {
+            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/lazy/staggeredgrid/LazyStaggeredGridDsl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridDsl.kt
index 28f690f..1a506df 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridDsl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridDsl.kt
@@ -214,7 +214,7 @@
     /**
      * Add a single item to the staggered grid.
      *
-     * @param key a factory of stable and unique keys representing the item. The key
+     * @param key a stable and unique key representing the item. The key
      *  MUST be saveable via Bundle on Android. If set to null (by default), the position of the
      *  item will be used as a key instead.
      *  Using the same key for multiple items in the staggered grid is not allowed.
@@ -222,15 +222,18 @@
      *  When you specify the key the scroll position will be maintained based on the key, which
      *  means if you add/remove items before the current visible item the item with the given key
      *  will be kept as the first visible one.
-     * @param contentType a factory of content types representing the item. Content for item of
+     * @param contentType a content type representing the item. Content for item of
      *  the same type can be reused more efficiently. null is a valid type as well and items
      *  of such type will be considered compatible.
+     * @param span a custom span for this item. Spans configure how many lanes defined by
+     *  [StaggeredGridCells] the item will occupy. By default each item will take one lane.
      * @param content composable content displayed by current item
      */
     @ExperimentalFoundationApi
     fun item(
         key: Any? = null,
         contentType: Any? = null,
+        span: StaggeredGridItemSpan? = null,
         content: @Composable LazyStaggeredGridItemScope.() -> Unit
     )
 
@@ -249,12 +252,15 @@
      * @param contentType a factory of content types representing the item. Content for item of
      *  the same type can be reused more efficiently. null is a valid type as well and items
      *  of such type will be considered compatible.
+     *  @param span a factory of custom spans for this item. Spans configure how many lanes defined
+     *  by [StaggeredGridCells] the item will occupy. By default each item will take one lane.
      * @param itemContent composable content displayed by item on provided position
      */
     fun items(
         count: Int,
         key: ((index: Int) -> Any)? = null,
         contentType: (index: Int) -> Any? = { null },
+        span: ((index: Int) -> StaggeredGridItemSpan)? = null,
         itemContent: @Composable LazyStaggeredGridItemScope.(index: Int) -> Unit
     )
 }
@@ -274,14 +280,17 @@
  * @param contentType a factory of content types representing the item. Content for item of
  *  the same type can be reused more efficiently. null is a valid type as well and items
  *  of such type will be considered compatible.
+ * @param span a factory of custom spans for this item. Spans configure how many lanes defined
+ *  by [StaggeredGridCells] the item will occupy. By default each item will take one lane.
  * @param itemContent composable content displayed by the provided item
  */
 @ExperimentalFoundationApi
-fun <T> LazyStaggeredGridScope.items(
+inline fun <T> LazyStaggeredGridScope.items(
     items: List<T>,
-    key: ((item: T) -> Any)? = null,
-    contentType: (item: T) -> Any? = { null },
-    itemContent: @Composable LazyStaggeredGridItemScope.(item: T) -> Unit
+    noinline key: ((item: T) -> Any)? = null,
+    crossinline contentType: (item: T) -> Any? = { null },
+    noinline span: ((item: T) -> StaggeredGridItemSpan)? = null,
+    crossinline itemContent: @Composable LazyStaggeredGridItemScope.(item: T) -> Unit
 ) {
     items(
         count = items.size,
@@ -289,6 +298,9 @@
             { index -> key(items[index]) }
         },
         contentType = { index -> contentType(items[index]) },
+        span = span?.let {
+            { index -> span(items[index]) }
+        },
         itemContent = { index -> itemContent(items[index]) }
     )
 }
@@ -308,14 +320,17 @@
  * @param contentType a factory of content types representing the item. Content for item of
  *  the same type can be reused more efficiently. null is a valid type as well and items
  *  of such type will be considered compatible.
+ * @param span a factory of custom spans for this item. Spans configure how many lanes defined
+ *  by [StaggeredGridCells] the item will occupy. By default each item will take one lane.
  * @param itemContent composable content displayed given item and index
  */
 @ExperimentalFoundationApi
-fun <T> LazyStaggeredGridScope.itemsIndexed(
+inline fun <T> LazyStaggeredGridScope.itemsIndexed(
     items: List<T>,
-    key: ((index: Int, item: T) -> Any)? = null,
-    contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
-    itemContent: @Composable LazyStaggeredGridItemScope.(index: Int, item: T) -> Unit
+    noinline key: ((index: Int, item: T) -> Any)? = null,
+    crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
+    noinline span: ((index: Int, item: T) -> StaggeredGridItemSpan)? = null,
+    crossinline itemContent: @Composable LazyStaggeredGridItemScope.(index: Int, item: T) -> Unit
 ) {
     items(
         count = items.size,
@@ -323,6 +338,9 @@
             { index -> key(index, items[index]) }
         },
         contentType = { index -> contentType(index, items[index]) },
+        span = span?.let {
+            { index -> span(index, items[index]) }
+        },
         itemContent = { index -> itemContent(index, items[index]) }
     )
 }
@@ -342,14 +360,17 @@
  * @param contentType a factory of content types representing the item. Content for item of
  *  the same type can be reused more efficiently. null is a valid type as well and items
  *  of such type will be considered compatible.
+ * @param span a factory of custom spans for this item. Spans configure how many lanes defined
+ *  by [StaggeredGridCells] the item will occupy. By default each item will take one lane.
  * @param itemContent composable content displayed by the provided item
  */
 @ExperimentalFoundationApi
-fun <T> LazyStaggeredGridScope.items(
+inline fun <T> LazyStaggeredGridScope.items(
     items: Array<T>,
-    key: ((item: T) -> Any)? = null,
-    contentType: (item: T) -> Any? = { null },
-    itemContent: @Composable LazyStaggeredGridItemScope.(item: T) -> Unit
+    noinline key: ((item: T) -> Any)? = null,
+    crossinline contentType: (item: T) -> Any? = { null },
+    noinline span: ((item: T) -> StaggeredGridItemSpan)? = null,
+    crossinline itemContent: @Composable LazyStaggeredGridItemScope.(item: T) -> Unit
 ) {
     items(
         count = items.size,
@@ -357,6 +378,9 @@
             { index -> key(items[index]) }
         },
         contentType = { index -> contentType(items[index]) },
+        span = span?.let {
+            { index -> span(items[index]) }
+        },
         itemContent = { index -> itemContent(items[index]) }
     )
 }
@@ -376,14 +400,17 @@
  * @param contentType a factory of content types representing the item. Content for item of
  *  the same type can be reused more efficiently. null is a valid type as well and items
  *  of such type will be considered compatible.
+ * @param span a factory of custom spans for this item. Spans configure how many lanes defined
+ *  by [StaggeredGridCells] the item will occupy. By default each item will take one lane.
  * @param itemContent composable content displayed given item and index
  */
 @ExperimentalFoundationApi
-fun <T> LazyStaggeredGridScope.itemsIndexed(
+inline fun <T> LazyStaggeredGridScope.itemsIndexed(
     items: Array<T>,
-    key: ((index: Int, item: T) -> Any)? = null,
-    contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
-    itemContent: @Composable LazyStaggeredGridItemScope.(index: Int, item: T) -> Unit
+    noinline key: ((index: Int, item: T) -> Any)? = null,
+    crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
+    noinline span: ((index: Int, item: T) -> StaggeredGridItemSpan)? = null,
+    crossinline itemContent: @Composable LazyStaggeredGridItemScope.(index: Int, item: T) -> Unit
 ) {
     items(
         count = items.size,
@@ -391,6 +418,9 @@
             { index -> key(index, items[index]) }
         },
         contentType = { index -> contentType(index, items[index]) },
+        span = span?.let {
+            { index -> span(index, items[index]) }
+        },
         itemContent = { index -> itemContent(index, items[index]) }
     )
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt
index 5d35898..87cf120 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt
@@ -19,18 +19,24 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
 import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyLayoutPinnableItem
 import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
 
+@OptIn(ExperimentalFoundationApi::class)
+internal interface LazyStaggeredGridItemProvider : LazyLayoutItemProvider {
+    val spanProvider: LazyStaggeredGridSpanProvider
+}
+
 @Composable
 @ExperimentalFoundationApi
 internal fun rememberStaggeredGridItemProvider(
     state: LazyStaggeredGridState,
     content: LazyStaggeredGridScope.() -> Unit,
-): LazyLayoutItemProvider {
+): LazyStaggeredGridItemProvider {
     val latestContent = rememberUpdatedState(content)
     val nearestItemsRangeState = rememberLazyNearestItemsRangeState(
         firstVisibleItemIndex = { state.firstVisibleItemIndex },
@@ -40,16 +46,26 @@
     return remember(state) {
         val itemProviderState = derivedStateOf {
             val scope = LazyStaggeredGridScopeImpl().apply(latestContent.value)
-            LazyLayoutItemProvider(
+            object : LazyLayoutItemProvider by LazyLayoutItemProvider(
                 scope.intervals,
                 nearestItemsRangeState.value,
-            ) { interval, index ->
-                interval.value.item.invoke(
-                    LazyStaggeredGridItemScopeImpl,
-                    index - interval.startIndex
-                )
+                itemContent = { interval, index ->
+                    LazyLayoutPinnableItem(index, state.pinnedItems) {
+                        interval.value.item.invoke(
+                            LazyStaggeredGridItemScopeImpl,
+                            index - interval.startIndex
+                        )
+                    }
+                }
+            ), LazyStaggeredGridItemProvider {
+                override val spanProvider = LazyStaggeredGridSpanProvider(scope.intervals)
             }
         }
-        object : LazyLayoutItemProvider by DelegatingLazyLayoutItemProvider(itemProviderState) { }
+
+        object : LazyLayoutItemProvider by DelegatingLazyLayoutItemProvider(itemProviderState),
+            LazyStaggeredGridItemProvider {
+
+            override val spanProvider get() = itemProviderState.value.spanProvider
+        }
     }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLaneInfo.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLaneInfo.kt
new file mode 100644
index 0000000..eee96e9
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridLaneInfo.kt
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.staggeredgrid
+
+/**
+ * Utility class to remember grid lane assignments in a sliding window relative to requested
+ * item position (usually reflected by scroll position).
+ * Remembers the maximum range of remembered items is reflected by [MaxCapacity], if index is beyond
+ * the bounds, [anchor] moves to reflect new position.
+ */
+internal class LazyStaggeredGridLaneInfo {
+    private var anchor = 0
+    private var lanes = IntArray(16)
+    private val spannedItems = ArrayDeque<SpannedItem>()
+
+    private class SpannedItem(val index: Int, var gaps: IntArray)
+
+    /**
+     * Sets given lane for given item index.
+     */
+    fun setLane(itemIndex: Int, lane: Int) {
+        require(itemIndex >= 0) { "Negative lanes are not supported" }
+        ensureValidIndex(itemIndex)
+        lanes[itemIndex - anchor] = lane + 1
+    }
+
+    /**
+     * Get lane for given item index.
+     * @return lane previously recorded for given item or [Unset] if it doesn't exist.
+     */
+    fun getLane(itemIndex: Int): Int {
+        if (itemIndex < lowerBound() || itemIndex >= upperBound()) {
+            return Unset
+        }
+        return lanes[itemIndex - anchor] - 1
+    }
+
+    /**
+     * Checks whether item can be in the target lane
+     * @param itemIndex item to check lane for
+     * @param targetLane lane it should belong to
+     */
+    fun assignedToLane(itemIndex: Int, targetLane: Int): Boolean {
+        val lane = getLane(itemIndex)
+        return lane == targetLane || lane == Unset || lane == FullSpan
+    }
+
+    /**
+     * @return upper bound of currently valid item range
+     */
+    /* @VisibleForTests */
+    fun upperBound(): Int = anchor + lanes.size
+
+    /**
+     * @return lower bound of currently valid item range
+     */
+    /* @VisibleForTests */
+    fun lowerBound(): Int = anchor
+
+    /**
+     * Delete remembered lane assignments.
+     */
+    fun reset() {
+        lanes.fill(0)
+        spannedItems.clear()
+    }
+
+    /**
+     * Find the previous item relative to [itemIndex] set to target lane
+     * @return found item index or -1 if it doesn't exist.
+     */
+    fun findPreviousItemIndex(itemIndex: Int, targetLane: Int): Int {
+        for (i in (itemIndex - 1) downTo 0) {
+            if (assignedToLane(i, targetLane)) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Find the next item relative to [itemIndex] set to target lane
+     * @return found item index or [upperBound] if it doesn't exist.
+     */
+    fun findNextItemIndex(itemIndex: Int, targetLane: Int): Int {
+        for (i in itemIndex + 1 until upperBound()) {
+            if (assignedToLane(i, targetLane)) {
+                return i
+            }
+        }
+        return upperBound()
+    }
+
+    fun ensureValidIndex(requestedIndex: Int) {
+        val requestedCapacity = requestedIndex - anchor
+
+        if (requestedCapacity in 0 until MaxCapacity) {
+            // simplest path - just grow array to given capacity
+            ensureCapacity(requestedCapacity + 1)
+        } else {
+            // requested index is beyond current span bounds
+            // rebase anchor so that requested index is in the middle of span array
+            val oldAnchor = anchor
+            anchor = maxOf(requestedIndex - (lanes.size / 2), 0)
+            var delta = anchor - oldAnchor
+
+            if (delta >= 0) {
+                // copy previous span data if delta is smaller than span size
+                if (delta < lanes.size) {
+                    lanes.copyInto(
+                        lanes,
+                        destinationOffset = 0,
+                        startIndex = delta,
+                        endIndex = lanes.size
+                    )
+                }
+                // fill the rest of the spans with default values
+                lanes.fill(0, maxOf(0, lanes.size - delta), lanes.size)
+            } else {
+                delta = -delta
+                // check if we can grow spans to match delta
+                if (lanes.size + delta < MaxCapacity) {
+                    // grow spans and leave space in the start
+                    ensureCapacity(lanes.size + delta + 1, delta)
+                } else {
+                    // otherwise, just move data that fits
+                    if (delta < lanes.size) {
+                        lanes.copyInto(
+                            lanes,
+                            destinationOffset = delta,
+                            startIndex = 0,
+                            endIndex = lanes.size - delta
+                        )
+                    }
+                    // fill the rest of the spans with default values
+                    lanes.fill(0, 0, minOf(lanes.size, delta))
+                }
+            }
+        }
+
+        // ensure full item spans beyond saved index are forgotten to save memory
+
+        while (spannedItems.isNotEmpty() && spannedItems.first().index < lowerBound()) {
+            spannedItems.removeFirst()
+        }
+
+        while (spannedItems.isNotEmpty() && spannedItems.last().index > upperBound()) {
+            spannedItems.removeLast()
+        }
+    }
+
+    fun setGaps(itemIndex: Int, gaps: IntArray?) {
+        val foundIndex = spannedItems.binarySearchBy(itemIndex) { it.index }
+        if (foundIndex < 0) {
+            if (gaps == null) {
+                return
+            }
+            // not found, insert new element
+            val insertionIndex = -(foundIndex + 1)
+            spannedItems.add(insertionIndex, SpannedItem(itemIndex, gaps))
+        } else {
+            if (gaps == null) {
+                // found, but gaps are reset, remove item
+                spannedItems.removeAt(foundIndex)
+            } else {
+                // found, update gaps
+                spannedItems[foundIndex].gaps = gaps
+            }
+        }
+    }
+
+    fun getGaps(itemIndex: Int): IntArray? {
+        val foundIndex = spannedItems.binarySearchBy(itemIndex) { it.index }
+        return spannedItems.getOrNull(foundIndex)?.gaps
+    }
+
+    private fun ensureCapacity(capacity: Int, newOffset: Int = 0) {
+        require(capacity <= MaxCapacity) {
+            "Requested item capacity $capacity is larger than max supported: $MaxCapacity!"
+        }
+        if (lanes.size < capacity) {
+            var newSize = lanes.size
+            while (newSize < capacity) newSize *= 2
+            lanes = lanes.copyInto(IntArray(newSize), destinationOffset = newOffset)
+        }
+    }
+
+    companion object {
+        private const val MaxCapacity = 131_072 // Closest to 100_000, 2 ^ 17
+        internal const val Unset = -1
+        internal const val FullSpan = -2
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
index 138d229..1c38c19 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
@@ -21,7 +21,8 @@
 import androidx.compose.foundation.fastMaxOfOrNull
 import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
 import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
-import androidx.compose.runtime.collection.MutableVector
+import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridLaneInfo.Companion.FullSpan
+import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridLaneInfo.Companion.Unset
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.unit.Constraints
@@ -30,15 +31,24 @@
 import androidx.compose.ui.unit.constrainHeight
 import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.packInts
+import androidx.compose.ui.util.unpackInt1
+import androidx.compose.ui.util.unpackInt2
 import kotlin.math.abs
-import kotlin.math.min
 import kotlin.math.roundToInt
 import kotlin.math.sign
 
+private const val DebugLoggingEnabled = false
+private inline fun debugLog(message: () -> String) {
+    if (DebugLoggingEnabled) {
+        println(message())
+    }
+}
+
 @ExperimentalFoundationApi
 internal fun LazyLayoutMeasureScope.measureStaggeredGrid(
     state: LazyStaggeredGridState,
-    itemProvider: LazyLayoutItemProvider,
+    itemProvider: LazyStaggeredGridItemProvider,
     resolvedSlotSums: IntArray,
     constraints: Constraints,
     isVertical: Boolean,
@@ -77,23 +87,23 @@
             } else {
                 // Grid got resized (or we are in a initial state)
                 // Adjust indices accordingly
-                context.spans.reset()
+                context.laneInfo.reset()
                 IntArray(resolvedSlotSums.size).apply {
                     // Try to adjust indices in case grid got resized
                     for (lane in indices) {
                         this[lane] = if (
-                            lane < firstVisibleIndices.size && firstVisibleIndices[lane] != -1
+                            lane < firstVisibleIndices.size && firstVisibleIndices[lane] != Unset
                         ) {
                             firstVisibleIndices[lane]
                         } else {
                             if (lane == 0) {
                                 0
                             } else {
-                                context.findNextItemIndex(this[lane - 1], lane)
+                                maxInRange(SpanRange(0, lane)) + 1
                             }
                         }
                         // Ensure spans are updated to be in correct range
-                        context.spans.setSpan(this[lane], lane)
+                        context.laneInfo.setLane(this[lane], lane)
                     }
                 }
             }
@@ -127,7 +137,7 @@
 @OptIn(ExperimentalFoundationApi::class)
 private class LazyStaggeredGridMeasureContext(
     val state: LazyStaggeredGridState,
-    val itemProvider: LazyLayoutItemProvider,
+    val itemProvider: LazyStaggeredGridItemProvider,
     val resolvedSlotSums: IntArray,
     val constraints: Constraints,
     val isVertical: Boolean,
@@ -143,20 +153,40 @@
         isVertical,
         itemProvider,
         measureScope,
-        resolvedSlotSums
-    ) { index, lane, key, placeables ->
-        val isLastInLane = spans.findNextItemIndex(index, lane) >= itemProvider.itemCount
+        resolvedSlotSums,
+        crossAxisSpacing
+    ) { index, lane, span, key, placeables ->
         LazyStaggeredGridMeasuredItem(
             index,
             key,
             placeables,
             isVertical,
             contentOffset,
-            if (isLastInLane) 0 else mainAxisSpacing
+            mainAxisSpacing,
+            lane,
+            span
         )
     }
 
-    val spans = state.spans
+    val laneInfo = state.laneInfo
+
+    val laneCount = resolvedSlotSums.size
+
+    fun LazyStaggeredGridItemProvider.isFullSpan(itemIndex: Int): Boolean =
+        spanProvider.isFullSpan(itemIndex)
+
+    fun LazyStaggeredGridItemProvider.getSpanRange(itemIndex: Int, lane: Int): SpanRange {
+        val isFullSpan = spanProvider.isFullSpan(itemIndex)
+        val span = if (isFullSpan) laneCount else 1
+        val targetLane = if (isFullSpan) 0 else lane
+        return SpanRange(targetLane, span)
+    }
+
+    inline val SpanRange.isFullSpan: Boolean
+        get() = size != 1
+
+    inline val SpanRange.laneInfo: Int
+        get() = if (isFullSpan) FullSpan else start
 }
 
 @ExperimentalFoundationApi
@@ -169,7 +199,7 @@
     with(measureScope) {
         val itemCount = itemProvider.itemCount
 
-        if (itemCount <= 0 || resolvedSlotSums.isEmpty()) {
+        if (itemCount <= 0 || laneCount == 0) {
             return LazyStaggeredGridMeasureResult(
                 firstVisibleItemIndices = initialItemIndices,
                 firstVisibleItemScrollOffsets = initialItemOffsets,
@@ -185,6 +215,7 @@
                 viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
                 beforeContentPadding = beforeContentPadding,
                 afterContentPadding = afterContentPadding,
+                mainAxisItemSpacing = mainAxisSpacing
             )
         }
 
@@ -202,8 +233,8 @@
         firstItemOffsets.offsetBy(-scrollDelta)
 
         // this will contain all the MeasuredItems representing the visible items
-        val measuredItems = Array(resolvedSlotSums.size) {
-            ArrayDeque<LazyStaggeredGridMeasuredItem>()
+        val measuredItems = Array(laneCount) {
+            ArrayDeque<LazyStaggeredGridMeasuredItem>(16)
         }
 
         // include the start padding so we compose items in the padding area. before starting
@@ -215,7 +246,7 @@
                 val itemIndex = firstItemIndices[lane]
                 val itemOffset = firstItemOffsets[lane]
 
-                if (itemOffset < -mainAxisSpacing && itemIndex > 0) {
+                if (itemOffset < maxOf(-mainAxisSpacing, 0) && itemIndex > 0) {
                     return true
                 }
             }
@@ -225,56 +256,90 @@
 
         var laneToCheckForGaps = -1
 
-        // we had scrolled backward or we compose items in the start padding area, which means
-        // items before current firstItemScrollOffset should be visible. compose them and update
-        // firstItemScrollOffset
-        while (hasSpaceBeforeFirst()) {
-            val laneIndex = firstItemOffsets.indexOfMinValue()
-            val previousItemIndex = findPreviousItemIndex(
-                item = firstItemIndices[laneIndex],
-                lane = laneIndex
-            )
+        debugLog { "=========== MEASURE START ==========" }
+        debugLog {
+            "| Filling up from indices: ${firstItemIndices.toList()}, " +
+                "offsets: ${firstItemOffsets.toList()}"
+        }
 
+        // we had scrolled backward or we compose items in the start padding area, which means
+        // items before current firstItemOffset should be visible. compose them and update
+        // firstItemOffsets
+        while (hasSpaceBeforeFirst()) {
+            // staggered grid always keeps item index increasing top to bottom
+            // the first item that should contain something before it must have the largest index
+            // among the rest
+            val laneIndex = firstItemIndices.indexOfMaxValue()
+            val itemIndex = firstItemIndices[laneIndex]
+
+            // other lanes might have smaller offsets than the one chosen above, which indicates
+            // incorrect measurement (e.g. item was deleted or it changed size)
+            // correct this by offsetting affected lane back to match currently chosen offset
+            for (i in firstItemOffsets.indices) {
+                if (
+                    firstItemIndices[i] != firstItemIndices[laneIndex] &&
+                        firstItemOffsets[i] < firstItemOffsets[laneIndex]
+                ) {
+                    // If offset of the lane is smaller than currently chosen lane,
+                    // offset the lane to be where current value of the chosen index is.
+                    firstItemOffsets[i] = firstItemOffsets[laneIndex]
+                }
+            }
+
+            val previousItemIndex = findPreviousItemIndex(itemIndex, laneIndex)
             if (previousItemIndex < 0) {
                 laneToCheckForGaps = laneIndex
                 break
             }
 
-            if (spans.getSpan(previousItemIndex) == LazyStaggeredGridSpans.Unset) {
-                spans.setSpan(previousItemIndex, laneIndex)
-            }
-
+            val spanRange = itemProvider.getSpanRange(previousItemIndex, laneIndex)
+            laneInfo.setLane(previousItemIndex, spanRange.laneInfo)
             val measuredItem = measuredItemProvider.getAndMeasure(
-                previousItemIndex,
-                laneIndex
+                index = previousItemIndex,
+                span = spanRange
             )
-            measuredItems[laneIndex].addFirst(measuredItem)
 
-            firstItemIndices[laneIndex] = previousItemIndex
-            firstItemOffsets[laneIndex] += measuredItem.sizeWithSpacings
+            val offset = firstItemOffsets.maxInRange(spanRange)
+            val gaps = if (spanRange.isFullSpan) laneInfo.getGaps(previousItemIndex) else null
+            spanRange.forEach { lane ->
+                firstItemIndices[lane] = previousItemIndex
+                val gap = if (gaps == null) 0 else gaps[lane]
+                firstItemOffsets[lane] = offset + measuredItem.sizeWithSpacings + gap
+            }
+        }
+        debugLog {
+            @Suppress("ListIterator")
+            "| up filled, measured items are ${measuredItems.map { it.map { it.index } }}"
         }
 
         fun misalignedStart(referenceLane: Int): Boolean {
             // If we scrolled past the first item in the lane, we have a point of reference
             // to re-align items.
-            val laneRange = firstItemIndices.indices
 
             // Case 1: Each lane has laid out all items, but offsets do no match
-            val misalignedOffsets = laneRange.any { lane ->
-                findPreviousItemIndex(firstItemIndices[lane], lane) == -1 &&
-                    firstItemOffsets[lane] != firstItemOffsets[referenceLane]
+            for (lane in firstItemIndices.indices) {
+                val misalignedOffsets =
+                    findPreviousItemIndex(firstItemIndices[lane], lane) == Unset &&
+                        firstItemOffsets[lane] != firstItemOffsets[referenceLane]
+
+                if (misalignedOffsets) {
+                    return true
+                }
             }
             // Case 2: Some lanes are still missing items, and there's no space left to place them
-            val moreItemsInOtherLanes = laneRange.any { lane ->
-                findPreviousItemIndex(firstItemIndices[lane], lane) != -1 &&
-                    firstItemOffsets[lane] >= firstItemOffsets[referenceLane]
+            for (lane in firstItemIndices.indices) {
+                val moreItemsInOtherLanes =
+                    findPreviousItemIndex(firstItemIndices[lane], lane) != Unset &&
+                        firstItemOffsets[lane] >= firstItemOffsets[referenceLane]
+
+                if (moreItemsInOtherLanes) {
+                    return true
+                }
             }
             // Case 3: the first item is in the wrong lane (it should always be in
             // the first one)
-            val firstItemInWrongLane = spans.getSpan(0) != 0
-            // If items are not aligned, reset all measurement data we gathered before and
-            // proceed with initial measure
-            return misalignedOffsets || moreItemsInOtherLanes || firstItemInWrongLane
+            val firstItemLane = laneInfo.getLane(0)
+            return firstItemLane != 0 && firstItemLane != Unset && firstItemLane != FullSpan
         }
 
         // define min offset (currently includes beforeContentPadding)
@@ -285,6 +350,9 @@
         if (firstItemOffsets[0] < minOffset) {
             scrollDelta += firstItemOffsets[0]
             firstItemOffsets.offsetBy(minOffset - firstItemOffsets[0])
+            debugLog {
+                "| correcting scroll delta from ${firstItemOffsets[0]} to $minOffset"
+            }
         }
 
         // neutralize previously added start padding as we stopped filling the before content padding
@@ -297,7 +365,7 @@
         if (laneToCheckForGaps != -1) {
             val lane = laneToCheckForGaps
             if (misalignedStart(lane) && canRestartMeasure) {
-                spans.reset()
+                laneInfo.reset()
                 return measure(
                     initialScrollDelta = scrollDelta,
                     initialItemIndices = IntArray(firstItemIndices.size) { -1 },
@@ -309,29 +377,73 @@
             }
         }
 
-        val currentItemIndices = initialItemIndices.copyOf().apply {
-            // ensure indices match item count, in case it decreased
-            ensureIndicesInRange(this, itemCount)
-        }
-        val currentItemOffsets = IntArray(initialItemOffsets.size) {
-            -(initialItemOffsets[it] - scrollDelta)
+        // start measuring down from first item indices/offsets decided above to ensure correct
+        // arrangement.
+        // this means we are calling measure second time on items previously measured in this
+        // function, but LazyLayout caches them, so no overhead.
+        val currentItemIndices = firstItemIndices.copyOf()
+        val currentItemOffsets = IntArray(firstItemOffsets.size) {
+            -firstItemOffsets[it]
         }
 
         val maxOffset = (mainAxisAvailableSize + afterContentPadding).coerceAtLeast(0)
 
-        // compose first visible items we received from state
-        currentItemIndices.forEachIndexed { laneIndex, itemIndex ->
-            if (itemIndex < 0) return@forEachIndexed
+        debugLog {
+            "| filling from current: indices: ${currentItemIndices.toList()}, " +
+                "offsets: ${currentItemOffsets.toList()}"
+        }
 
-            val measuredItem = measuredItemProvider.getAndMeasure(itemIndex, laneIndex)
-            currentItemOffsets[laneIndex] += measuredItem.sizeWithSpacings
-            measuredItems[laneIndex].addLast(measuredItem)
+        // current item should be pointing to the index of previously measured item below,
+        // as lane assignments must be decided based on size and offset of all previous items
+        // this loop makes sure to measure items that were initially passed to the current item
+        // indices with correct item order
+        var initialItemsMeasured = 0
+        var initialLaneToMeasure = currentItemIndices.indexOfMinValue()
+        while (initialLaneToMeasure != -1 && initialItemsMeasured < laneCount) {
+            val itemIndex = currentItemIndices[initialLaneToMeasure]
+            val laneIndex = initialLaneToMeasure
 
-            spans.setSpan(itemIndex, laneIndex)
+            initialLaneToMeasure = currentItemIndices.indexOfMinValue(minBound = itemIndex)
+            initialItemsMeasured++
+
+            if (itemIndex < 0) continue
+
+            val spanRange = itemProvider.getSpanRange(itemIndex, laneIndex)
+            val measuredItem = measuredItemProvider.getAndMeasure(
+                itemIndex,
+                spanRange
+            )
+
+            laneInfo.setLane(itemIndex, spanRange.laneInfo)
+            val offset = currentItemOffsets.maxInRange(spanRange) + measuredItem.sizeWithSpacings
+            spanRange.forEach { lane ->
+                currentItemOffsets[lane] = offset
+                currentItemIndices[lane] = itemIndex
+                measuredItems[lane].addLast(measuredItem)
+            }
+
+            if (currentItemOffsets[spanRange.start] <= minOffset + mainAxisSpacing) {
+                measuredItem.isVisible = false
+            }
+
+            if (spanRange.isFullSpan) {
+                // full span items overwrite other slots if we measure it here, so skip measuring
+                // the rest of the slots
+                initialItemsMeasured = laneCount
+            }
+        }
+
+        debugLog {
+            @Suppress("ListIterator")
+            "| current filled, measured items are ${measuredItems.map { it.map { it.index } }}"
+        }
+        debugLog {
+            "| filling down from indices: ${currentItemIndices.toList()}, " +
+                "offsets: ${currentItemOffsets.toList()}"
         }
 
         // then composing visible items forward until we fill the whole viewport.
-        // we want to have at least one item in visibleItems even if in fact all the items are
+        // we want to have at least one item in measuredItems even if in fact all the items are
         // offscreen, this can happen if the content padding is larger than the available size.
         while (
             currentItemOffsets.any {
@@ -340,74 +452,72 @@
             } || measuredItems.all { it.isEmpty() }
         ) {
             val currentLaneIndex = currentItemOffsets.indexOfMinValue()
-            val nextItemIndex =
-                findNextItemIndex(currentItemIndices[currentLaneIndex], currentLaneIndex)
+            val previousItemIndex = currentItemIndices.max()
+            val itemIndex = previousItemIndex + 1
 
-            if (nextItemIndex >= itemCount) {
-                // if any items changed its size, the spans may not behave correctly
-                // there are no more items in this lane, but there could be more in others
-                // recheck if we can add more items and reset spans accordingly
-                var missedItemIndex = Int.MAX_VALUE
-                currentItemIndices.forEachIndexed { laneIndex, i ->
-                    if (laneIndex == currentLaneIndex) return@forEachIndexed
-                    var itemIndex = findNextItemIndex(i, laneIndex)
-                    while (itemIndex < itemCount) {
-                        missedItemIndex = minOf(itemIndex, missedItemIndex)
-                        spans.setSpan(itemIndex, LazyStaggeredGridSpans.Unset)
-                        itemIndex = findNextItemIndex(itemIndex, laneIndex)
-                    }
-                }
-                // there's at least one missed item which may fit current lane
-                if (missedItemIndex != Int.MAX_VALUE && canRestartMeasure) {
-                    // reset current lane to the missed item index and restart measure
-                    initialItemIndices[currentLaneIndex] =
-                        min(initialItemIndices[currentLaneIndex], missedItemIndex)
-                    return measure(
-                        initialScrollDelta = initialScrollDelta,
-                        initialItemIndices = initialItemIndices,
-                        initialItemOffsets = initialItemOffsets,
-                        canRestartMeasure = false
-                    )
-                } else {
-                    break
-                }
+            if (itemIndex >= itemCount) {
+                break
             }
 
-            if (firstItemIndices[currentLaneIndex] == -1) {
-                firstItemIndices[currentLaneIndex] = nextItemIndex
-            }
-            spans.setSpan(nextItemIndex, currentLaneIndex)
+            val spanRange = itemProvider.getSpanRange(itemIndex, currentLaneIndex)
 
-            val measuredItem =
-                measuredItemProvider.getAndMeasure(nextItemIndex, currentLaneIndex)
-            currentItemOffsets[currentLaneIndex] += measuredItem.sizeWithSpacings
-            measuredItems[currentLaneIndex].addLast(measuredItem)
-            currentItemIndices[currentLaneIndex] = nextItemIndex
+            laneInfo.setLane(itemIndex, spanRange.laneInfo)
+            val measuredItem = measuredItemProvider.getAndMeasure(itemIndex, spanRange)
+
+            val offset = currentItemOffsets.maxInRange(spanRange)
+            val gaps = if (spanRange.isFullSpan) {
+                laneInfo.getGaps(itemIndex) ?: IntArray(laneCount)
+            } else {
+                null
+            }
+            spanRange.forEach { lane ->
+                if (gaps != null) {
+                    gaps[lane] = offset - currentItemOffsets[lane]
+                }
+                currentItemIndices[lane] = itemIndex
+                currentItemOffsets[lane] = offset + measuredItem.sizeWithSpacings
+                measuredItems[lane].addLast(measuredItem)
+            }
+            laneInfo.setGaps(itemIndex, gaps)
+
+            if (currentItemOffsets[spanRange.start] <= minOffset + mainAxisSpacing) {
+                // We scrolled past measuredItem, and it is not visible anymore. We measured it
+                // for correct positioning of other items, but there's no need to place it.
+                // Mark it as not visible and filter below.
+                measuredItem.isVisible = false
+            }
+        }
+
+        debugLog {
+            @Suppress("ListIterator")
+            "| down filled, measured items are ${measuredItems.map { it.map { it.index } }}"
         }
 
         // some measured items are offscreen, remove them from the list and adjust indices/offsets
         for (laneIndex in measuredItems.indices) {
             val laneItems = measuredItems[laneIndex]
-            var offset = currentItemOffsets[laneIndex]
-            var inBoundsIndex = 0
-            for (i in laneItems.lastIndex downTo 0) {
-                val item = laneItems[i]
-                offset -= item.sizeWithSpacings
-                inBoundsIndex = i
-                if (offset <= minOffset + mainAxisSpacing) {
-                    break
-                }
+
+            while (laneItems.size > 1 && !laneItems.first().isVisible) {
+                val item = laneItems.removeFirst()
+                val gaps = if (item.span != 1) laneInfo.getGaps(item.index) else null
+                firstItemOffsets[laneIndex] -=
+                    item.sizeWithSpacings + if (gaps == null) 0 else gaps[laneIndex]
             }
 
-            // the rest of the items are offscreen, update firstIndex/Offset for lane and remove
-            // items from measured list
-            for (i in 0 until inBoundsIndex) {
-                val item = laneItems.removeFirst()
-                firstItemOffsets[laneIndex] -= item.sizeWithSpacings
-            }
-            if (laneItems.isNotEmpty()) {
-                firstItemIndices[laneIndex] = laneItems.first().index
-            }
+            firstItemIndices[laneIndex] = laneItems.firstOrNull()?.index ?: Unset
+        }
+
+        if (currentItemIndices.any { it == itemCount - 1 }) {
+            currentItemOffsets.offsetBy(-mainAxisSpacing)
+        }
+
+        debugLog {
+            @Suppress("ListIterator")
+            "| removed invisible items: ${measuredItems.map { it.map { it.index } }}"
+        }
+        debugLog {
+            "| filling back up from indices: ${firstItemIndices.toList()}, " +
+                "offsets: ${firstItemOffsets.toList()}"
         }
 
         // we didn't fill the whole viewport with items starting from firstVisibleItemIndex.
@@ -433,7 +543,7 @@
 
                 if (previousIndex < 0) {
                     if (misalignedStart(laneIndex) && canRestartMeasure) {
-                        spans.reset()
+                        laneInfo.reset()
                         return measure(
                             initialScrollDelta = scrollDelta,
                             initialItemIndices = IntArray(firstItemIndices.size) { -1 },
@@ -446,15 +556,21 @@
                     break
                 }
 
-                spans.setSpan(previousIndex, laneIndex)
-
+                val spanRange = itemProvider.getSpanRange(previousIndex, laneIndex)
+                laneInfo.setLane(previousIndex, spanRange.laneInfo)
                 val measuredItem = measuredItemProvider.getAndMeasure(
-                    previousIndex,
-                    laneIndex
+                    index = previousIndex,
+                    spanRange
                 )
-                measuredItems[laneIndex].addFirst(measuredItem)
-                firstItemOffsets[laneIndex] += measuredItem.sizeWithSpacings
-                firstItemIndices[laneIndex] = previousIndex
+
+                val offset = firstItemOffsets.maxInRange(spanRange)
+                val gaps = if (spanRange.isFullSpan) laneInfo.getGaps(previousIndex) else null
+                spanRange.forEach { lane ->
+                    measuredItems[lane].addFirst(measuredItem)
+                    firstItemIndices[lane] = previousIndex
+                    val gap = if (gaps == null) 0 else gaps[lane]
+                    firstItemOffsets[lane] = offset + measuredItem.sizeWithSpacings + gap
+                }
             }
             scrollDelta += toScrollBack
 
@@ -467,6 +583,14 @@
             }
         }
 
+        debugLog {
+            @Suppress("ListIterator")
+            "| measured: ${measuredItems.map { it.map { it.index } }}"
+        }
+        debugLog {
+            "| first indices: ${firstItemIndices.toList()}, offsets: ${firstItemOffsets.toList()}"
+        }
+
         // report the amount of pixels we consumed. scrollDelta can be smaller than
         // scrollToBeConsumed if there were not enough items to fill the offered space or it
         // can be larger if items were resized, or if, for example, we were previously
@@ -487,12 +611,15 @@
             for (laneIndex in measuredItems.indices) {
                 val laneItems = measuredItems[laneIndex]
                 for (i in laneItems.indices) {
-                    val size = laneItems[i].sizeWithSpacings
+                    val item = laneItems[i]
+                    val gaps = laneInfo.getGaps(item.index)
+                    val size = item.sizeWithSpacings + if (gaps == null) 0 else gaps[laneIndex]
                     if (
                         i != laneItems.lastIndex &&
                         firstItemOffsets[laneIndex] != 0 &&
                         firstItemOffsets[laneIndex] >= size
                     ) {
+
                         firstItemOffsets[laneIndex] -= size
                         firstItemIndices[laneIndex] = laneItems[i + 1].index
                     } else {
@@ -502,8 +629,70 @@
             }
         }
 
+        debugLog {
+            "| final first indices: ${firstItemIndices.toList()}, " +
+                "offsets: ${firstItemOffsets.toList()}"
+        }
+
         // end measure
 
+        var extraItemOffset = itemScrollOffsets[0]
+        val extraItemsBefore = calculateExtraItems(
+            position = {
+                extraItemOffset -= it.sizeWithSpacings
+                it.position(0, extraItemOffset, 0)
+            }
+        ) { itemIndex ->
+            val lane = laneInfo.getLane(itemIndex)
+            when (lane) {
+                Unset, FullSpan -> {
+                    firstItemIndices.all { it > itemIndex }
+                }
+                else -> {
+                    firstItemIndices[lane] > itemIndex
+                }
+            }
+        }
+
+        val positionedItems = calculatePositionedItems(
+            measuredItems,
+            itemScrollOffsets
+        )
+
+        extraItemOffset = itemScrollOffsets[0]
+        val extraItemsAfter = calculateExtraItems(
+            position = {
+                val positionedItem = it.position(0, extraItemOffset, 0)
+                extraItemOffset += it.sizeWithSpacings
+                positionedItem
+            }
+        ) { itemIndex ->
+            if (itemIndex >= itemCount) {
+                return@calculateExtraItems false
+            }
+            val lane = laneInfo.getLane(itemIndex)
+            when (lane) {
+                Unset, FullSpan -> {
+                    currentItemIndices.all { it < itemIndex }
+                }
+                else -> {
+                    currentItemIndices[lane] < itemIndex
+                }
+            }
+        }
+
+        debugLog {
+            @Suppress("ListIterator")
+            "| positioned: ${positionedItems.map { "${it.index} at ${it.offset}" }.toList()}"
+        }
+        debugLog {
+            "========== MEASURE COMPLETED ==========="
+        }
+
+        // todo: reverse layout support
+
+        // End placement
+
         val layoutWidth = if (isVertical) {
             constraints.maxWidth
         } else {
@@ -514,35 +703,6 @@
         } else {
             constraints.maxHeight
         }
-
-        // Placement
-        val positionedItems = MutableVector<LazyStaggeredGridPositionedItem>(
-            capacity = measuredItems.sumOf { it.size }
-        )
-        while (measuredItems.any { it.isNotEmpty() }) {
-            // find the next item to position
-            val laneIndex = measuredItems.indexOfMinBy {
-                it.firstOrNull()?.index ?: Int.MAX_VALUE
-            }
-            val item = measuredItems[laneIndex].removeFirst()
-
-            // todo(b/182882362): arrangement support
-            val mainAxisOffset = itemScrollOffsets[laneIndex]
-            val crossAxisOffset =
-                if (laneIndex == 0) {
-                    0
-                } else {
-                    resolvedSlotSums[laneIndex - 1] + crossAxisSpacing * laneIndex
-                }
-
-            positionedItems += item.position(laneIndex, mainAxisOffset, crossAxisOffset)
-            itemScrollOffsets[laneIndex] += item.sizeWithSpacings
-        }
-
-        // todo: reverse layout support
-
-        // End placement
-
         // only scroll backward if the first item is not on screen or fully visible
         val canScrollBackward = !(firstItemIndices[0] == 0 && firstItemOffsets[0] <= 0)
         // only scroll forward if the last item is not on screen or fully visible
@@ -555,35 +715,130 @@
             firstVisibleItemScrollOffsets = firstItemOffsets,
             consumedScroll = consumedScroll,
             measureResult = layout(layoutWidth, layoutHeight) {
-                positionedItems.forEach { item ->
+                extraItemsBefore.fastForEach { item ->
+                    item.place(this)
+                }
+
+                positionedItems.fastForEach { item ->
+                    item.place(this)
+                }
+
+                extraItemsAfter.fastForEach { item ->
                     item.place(this)
                 }
             },
             canScrollForward = canScrollForward,
             canScrollBackward = canScrollBackward,
             isVertical = isVertical,
-            visibleItemsInfo = positionedItems.asMutableList(),
+            visibleItemsInfo = positionedItems,
             totalItemsCount = itemCount,
             viewportSize = IntSize(layoutWidth, layoutHeight),
             viewportStartOffset = minOffset,
             viewportEndOffset = maxOffset,
             beforeContentPadding = beforeContentPadding,
-            afterContentPadding = afterContentPadding
+            afterContentPadding = afterContentPadding,
+            mainAxisItemSpacing = mainAxisSpacing
         )
     }
 }
 
+private fun LazyStaggeredGridMeasureContext.calculatePositionedItems(
+    measuredItems: Array<ArrayDeque<LazyStaggeredGridMeasuredItem>>,
+    itemScrollOffsets: IntArray,
+): List<LazyStaggeredGridPositionedItem> {
+    val positionedItems = ArrayList<LazyStaggeredGridPositionedItem>(
+        measuredItems.sumOf { it.size }
+    )
+    while (measuredItems.any { it.isNotEmpty() }) {
+        // find the next item to position
+        val laneIndex = measuredItems.indexOfMinBy {
+            it.firstOrNull()?.index ?: Int.MAX_VALUE
+        }
+        val item = measuredItems[laneIndex].removeFirst()
+
+        if (item.lane != laneIndex) {
+            continue
+        }
+
+        // todo(b/182882362): arrangement support
+        val spanRange = SpanRange(item.lane, item.span)
+        val mainAxisOffset = itemScrollOffsets.maxInRange(spanRange)
+        val crossAxisOffset =
+            if (laneIndex == 0) {
+                0
+            } else {
+                resolvedSlotSums[laneIndex - 1] + crossAxisSpacing * laneIndex
+            }
+
+        if (item.placeables.isEmpty()) {
+            // nothing to place, ignore spacings
+            continue
+        }
+
+        positionedItems += item.position(laneIndex, mainAxisOffset, crossAxisOffset)
+        spanRange.forEach { lane ->
+            itemScrollOffsets[lane] = mainAxisOffset + item.sizeWithSpacings
+        }
+    }
+    return positionedItems
+}
+
+@ExperimentalFoundationApi
+private inline fun LazyStaggeredGridMeasureContext.calculateExtraItems(
+    position: (LazyStaggeredGridMeasuredItem) -> LazyStaggeredGridPositionedItem,
+    filter: (itemIndex: Int) -> Boolean
+): List<LazyStaggeredGridPositionedItem> {
+    var result: MutableList<LazyStaggeredGridPositionedItem>? = null
+
+    val pinnedItems = state.pinnedItems
+    pinnedItems.fastForEach { item ->
+        if (filter(item.index)) {
+            val spanRange = itemProvider.getSpanRange(item.index, 0)
+            if (result == null) {
+                result = mutableListOf()
+            }
+            val measuredItem = measuredItemProvider.getAndMeasure(item.index, spanRange)
+            result?.add(position(measuredItem))
+        }
+    }
+
+    return result ?: emptyList()
+}
+
+@JvmInline
+private value class SpanRange private constructor(val packedValue: Long) {
+    constructor(lane: Int, span: Int) : this(packInts(lane, lane + span))
+
+    inline val start get(): Int = unpackInt1(packedValue)
+    inline val end get(): Int = unpackInt2(packedValue)
+    inline val size get(): Int = end - start
+}
+
+private inline fun SpanRange.forEach(block: (Int) -> Unit) {
+    for (i in start until end) {
+        block(i)
+    }
+}
+
 private fun IntArray.offsetBy(delta: Int) {
     for (i in indices) {
         this[i] = this[i] + delta
     }
 }
 
-internal fun IntArray.indexOfMinValue(): Int {
+private fun IntArray.maxInRange(indexRange: SpanRange): Int {
+    var max = Int.MIN_VALUE
+    indexRange.forEach {
+        max = maxOf(max, this[it])
+    }
+    return max
+}
+
+internal fun IntArray.indexOfMinValue(minBound: Int = Int.MIN_VALUE): Int {
     var result = -1
     var min = Int.MAX_VALUE
     for (i in indices) {
-        if (min > this[i]) {
+        if (this[i] in (minBound + 1) until min) {
             min = this[i]
             result = i
         }
@@ -632,21 +887,20 @@
 ) {
     // reverse traverse to make sure last items are recorded to the latter lanes
     for (i in indices.indices.reversed()) {
-        while (indices[i] >= itemCount) {
+        while (indices[i] >= itemCount || !laneInfo.assignedToLane(indices[i], i)) {
             indices[i] = findPreviousItemIndex(indices[i], i)
         }
-        if (indices[i] != -1) {
+        if (indices[i] >= 0) {
             // reserve item for span
-            spans.setSpan(indices[i], i)
+            if (!itemProvider.isFullSpan(indices[i])) {
+                laneInfo.setLane(indices[i], i)
+            }
         }
     }
 }
 
 private fun LazyStaggeredGridMeasureContext.findPreviousItemIndex(item: Int, lane: Int): Int =
-    spans.findPreviousItemIndex(item, lane)
-
-private fun LazyStaggeredGridMeasureContext.findNextItemIndex(item: Int, lane: Int): Int =
-    spans.findNextItemIndex(item, lane)
+    laneInfo.findPreviousItemIndex(item, lane)
 
 @OptIn(ExperimentalFoundationApi::class)
 private class LazyStaggeredGridMeasureProvider(
@@ -654,11 +908,13 @@
     private val itemProvider: LazyLayoutItemProvider,
     private val measureScope: LazyLayoutMeasureScope,
     private val resolvedSlotSums: IntArray,
-    private val measuredItemFactory: MeasuredItemFactory
+    private val crossAxisSpacing: Int,
+    private val measuredItemFactory: MeasuredItemFactory,
 ) {
-    private fun childConstraints(slot: Int): Constraints {
+    private fun childConstraints(slot: Int, span: Int): Constraints {
         val previousSum = if (slot == 0) 0 else resolvedSlotSums[slot - 1]
-        val crossAxisSize = resolvedSlotSums[slot] - previousSum
+        val crossAxisSize =
+            resolvedSlotSums[slot + span - 1] - previousSum + crossAxisSpacing * (span - 1)
         return if (isVertical) {
             Constraints.fixedWidth(crossAxisSize)
         } else {
@@ -666,10 +922,10 @@
         }
     }
 
-    fun getAndMeasure(index: Int, lane: Int): LazyStaggeredGridMeasuredItem {
+    fun getAndMeasure(index: Int, span: SpanRange): LazyStaggeredGridMeasuredItem {
         val key = itemProvider.getKey(index)
-        val placeables = measureScope.measure(index, childConstraints(lane))
-        return measuredItemFactory.createItem(index, lane, key, placeables)
+        val placeables = measureScope.measure(index, childConstraints(span.start, span.size))
+        return measuredItemFactory.createItem(index, span.start, span.size, key, placeables)
     }
 }
 
@@ -678,6 +934,7 @@
     fun createItem(
         index: Int,
         lane: Int,
+        span: Int,
         key: Any,
         placeables: List<Placeable>
     ): LazyStaggeredGridMeasuredItem
@@ -689,8 +946,12 @@
     val placeables: List<Placeable>,
     val isVertical: Boolean,
     val contentOffset: IntOffset,
-    val spacing: Int
+    val spacing: Int,
+    val lane: Int,
+    val span: Int
 ) {
+    var isVisible = true
+
     val mainAxisSize: Int = placeables.fastFold(0) { size, placeable ->
         size + if (isVertical) placeable.height else placeable.width
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt
index a645632..29ec864 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt
@@ -23,7 +23,6 @@
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.calculateEndPadding
 import androidx.compose.foundation.layout.calculateStartPadding
-import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
 import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
@@ -39,7 +38,7 @@
 @ExperimentalFoundationApi
 internal fun rememberStaggeredGridMeasurePolicy(
     state: LazyStaggeredGridState,
-    itemProvider: LazyLayoutItemProvider,
+    itemProvider: LazyStaggeredGridItemProvider,
     contentPadding: PaddingValues,
     reverseLayout: Boolean,
     orientation: Orientation,
@@ -67,6 +66,7 @@
         // setup information for prefetch
         state.laneWidthsPrefixSum = resolvedSlotSums
         state.isVertical = isVertical
+        state.spanProvider = itemProvider.spanProvider
 
         val beforeContentPadding = contentPadding.beforePadding(
             orientation, reverseLayout, layoutDirection
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt
index 352a762..254168f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt
@@ -109,6 +109,11 @@
      * Content padding in pixels applied after the items in scroll direction.
      */
     val afterContentPadding: Int
+
+    /**
+     * The spacing between items in scroll direction.
+     */
+    val mainAxisItemSpacing: Int
 }
 
 @OptIn(ExperimentalFoundationApi::class)
@@ -142,7 +147,8 @@
     override val viewportStartOffset: Int,
     override val viewportEndOffset: Int,
     override val beforeContentPadding: Int,
-    override val afterContentPadding: Int
+    override val afterContentPadding: Int,
+    override val mainAxisItemSpacing: Int
 ) : LazyStaggeredGridLayoutInfo, MeasureResult by measureResult {
     override val orientation: Orientation =
         if (isVertical) Orientation.Vertical else Orientation.Horizontal
@@ -157,5 +163,6 @@
     override val viewportEndOffset: Int = 0
     override val beforeContentPadding: Int = 0
     override val afterContentPadding: Int = 0
+    override val mainAxisItemSpacing: Int = 0
     override val orientation: Orientation = Orientation.Vertical
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScope.kt
index 4d748da..364b075 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScope.kt
@@ -21,20 +21,21 @@
 import androidx.compose.foundation.lazy.layout.MutableIntervalList
 import androidx.compose.runtime.Composable
 
-@ExperimentalFoundationApi
+@OptIn(ExperimentalFoundationApi::class)
 internal class LazyStaggeredGridScopeImpl : LazyStaggeredGridScope {
     val intervals = MutableIntervalList<LazyStaggeredGridIntervalContent>()
 
-    @ExperimentalFoundationApi
     override fun item(
         key: Any?,
         contentType: Any?,
+        span: StaggeredGridItemSpan?,
         content: @Composable LazyStaggeredGridItemScope.() -> Unit
     ) {
         items(
             count = 1,
             key = key?.let { { key } },
             contentType = { contentType },
+            span = span?.let { { span } },
             itemContent = { content() }
         )
     }
@@ -43,6 +44,7 @@
         count: Int,
         key: ((index: Int) -> Any)?,
         contentType: (index: Int) -> Any?,
+        span: ((index: Int) -> StaggeredGridItemSpan)?,
         itemContent: @Composable LazyStaggeredGridItemScope.(index: Int) -> Unit
     ) {
         intervals.addInterval(
@@ -50,6 +52,7 @@
             LazyStaggeredGridIntervalContent(
                 key,
                 contentType,
+                span,
                 itemContent
             )
         )
@@ -63,5 +66,6 @@
 internal class LazyStaggeredGridIntervalContent(
     override val key: ((index: Int) -> Any)?,
     override val type: ((index: Int) -> Any?),
+    val span: ((index: Int) -> StaggeredGridItemSpan)?,
     val item: @Composable LazyStaggeredGridItemScope.(Int) -> Unit
 ) : LazyLayoutIntervalContent
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSpan.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSpan.kt
new file mode 100644
index 0000000..1379767a
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSpan.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.staggeredgrid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.IntervalList
+import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan.Companion.FullLine
+
+/**
+ * Span defines a number of lanes (columns in vertical grid/rows in horizontal grid) for
+ * staggered grid items.
+ * Two variations of span are supported:
+ *   - item taking a single lane ([SingleLane]);
+ *   - item all lanes in line ([FullLine]).
+ * By default, staggered grid uses [SingleLane] for all items.
+ */
+@ExperimentalFoundationApi
+class StaggeredGridItemSpan private constructor(internal val value: Int) {
+    companion object {
+        /**
+         * Force item to occupy whole line in cross axis.
+         */
+        val FullLine = StaggeredGridItemSpan(0)
+
+        /**
+         * Force item to use a single lane.
+         */
+        val SingleLane = StaggeredGridItemSpan(1)
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyStaggeredGridSpanProvider(
+    val intervals: IntervalList<LazyStaggeredGridIntervalContent>
+) {
+    fun isFullSpan(itemIndex: Int): Boolean {
+        if (itemIndex !in 0 until intervals.size) return false
+        intervals[itemIndex].run {
+            val span = value.span
+            val localIndex = itemIndex - startIndex
+
+            return span != null && span(localIndex) === FullLine
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSpans.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSpans.kt
deleted file mode 100644
index 5ec8708..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSpans.kt
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.lazy.staggeredgrid
-
-/**
- * Utility class to remember grid lane assignments (spans) in a sliding window relative to requested
- * item position (usually reflected by scroll position).
- * Remembers the maximum range of remembered items is reflected by [MaxCapacity], if index is beyond
- * the bounds, [anchor] moves to reflect new position.
- */
-internal class LazyStaggeredGridSpans {
-    private var anchor = 0
-    private var spans = IntArray(16)
-
-    /**
-     * Sets given span for given item index.
-     */
-    fun setSpan(item: Int, span: Int) {
-        require(item >= 0) { "Negative spans are not supported" }
-        ensureValidIndex(item)
-        spans[item - anchor] = span + 1
-    }
-
-    /**
-     * Get span for given item index.
-     * @return span previously recorded for given item or [Unset] if it doesn't exist.
-     */
-    fun getSpan(item: Int): Int {
-        if (item < lowerBound() || item >= upperBound()) {
-            return Unset
-        }
-        return spans[item - anchor] - 1
-    }
-
-    /**
-     * @return upper bound of currently valid span range
-     */
-    /* @VisibleForTests */
-    fun upperBound(): Int = anchor + spans.size
-
-    /**
-     * @return lower bound of currently valid span range
-     */
-    /* @VisibleForTests */
-    fun lowerBound(): Int = anchor
-
-    /**
-     * Delete remembered span assignments.
-     */
-    fun reset() {
-        spans.fill(0)
-    }
-
-    /**
-     * Find the previous item relative to [item] set to target span
-     * @return found item index or -1 if it doesn't exist.
-     */
-    fun findPreviousItemIndex(item: Int, target: Int): Int {
-        for (i in (item - 1) downTo 0) {
-            val span = getSpan(i)
-            if (span == target || span == Unset) {
-                return i
-            }
-        }
-        return -1
-    }
-
-    /**
-     * Find the next item relative to [item] set to target span
-     * @return found item index or [upperBound] if it doesn't exist.
-     */
-    fun findNextItemIndex(item: Int, target: Int): Int {
-        for (i in item + 1 until upperBound()) {
-            val span = getSpan(i)
-            if (span == target || span == Unset) {
-                return i
-            }
-        }
-        return upperBound()
-    }
-
-    fun ensureValidIndex(requestedIndex: Int) {
-        val requestedCapacity = requestedIndex - anchor
-
-        if (requestedCapacity in 0 until MaxCapacity) {
-            // simplest path - just grow array to given capacity
-            ensureCapacity(requestedCapacity + 1)
-        } else {
-            // requested index is beyond current span bounds
-            // rebase anchor so that requested index is in the middle of span array
-            val oldAnchor = anchor
-            anchor = maxOf(requestedIndex - (spans.size / 2), 0)
-            var delta = anchor - oldAnchor
-
-            if (delta >= 0) {
-                // copy previous span data if delta is smaller than span size
-                if (delta < spans.size) {
-                    spans.copyInto(
-                        spans,
-                        destinationOffset = 0,
-                        startIndex = delta,
-                        endIndex = spans.size
-                    )
-                }
-                // fill the rest of the spans with default values
-                spans.fill(0, maxOf(0, spans.size - delta), spans.size)
-            } else {
-                delta = -delta
-                // check if we can grow spans to match delta
-                if (spans.size + delta < MaxCapacity) {
-                    // grow spans and leave space in the start
-                    ensureCapacity(spans.size + delta + 1, delta)
-                } else {
-                    // otherwise, just move data that fits
-                    if (delta < spans.size) {
-                        spans.copyInto(
-                            spans,
-                            destinationOffset = delta,
-                            startIndex = 0,
-                            endIndex = spans.size - delta
-                        )
-                    }
-                    // fill the rest of the spans with default values
-                    spans.fill(0, 0, minOf(spans.size, delta))
-                }
-            }
-        }
-    }
-
-    private fun ensureCapacity(capacity: Int, newOffset: Int = 0) {
-        require(capacity <= MaxCapacity) {
-            "Requested span capacity $capacity is larger than max supported: $MaxCapacity!"
-        }
-        if (spans.size < capacity) {
-            var newSize = spans.size
-            while (newSize < capacity) newSize *= 2
-            spans = spans.copyInto(IntArray(newSize), destinationOffset = newOffset)
-        }
-    }
-
-    companion object {
-        private const val MaxCapacity = 131_072 // Closest to 100_000, 2 ^ 17
-        internal const val Unset = -1
-    }
-}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
index cd08ee3..9bada77 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
@@ -26,14 +26,18 @@
 import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle
+import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
 import androidx.compose.foundation.lazy.layout.animateScrollToItem
+import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridLaneInfo.Companion.Unset
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.saveable.listSaver
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
+import androidx.compose.runtime.structuralEqualityPolicy
 import androidx.compose.ui.layout.Remeasurement
 import androidx.compose.ui.layout.RemeasurementModifier
 import androidx.compose.ui.unit.Constraints
@@ -94,12 +98,13 @@
      * This property is observable and when use it in composable function it will be recomposed on
      * each scroll, potentially causing performance issues.
      */
-    val firstVisibleItemIndex: Int
-        get() = scrollPosition.indices.minOfOrNull {
+    val firstVisibleItemIndex: Int by derivedStateOf(structuralEqualityPolicy()) {
+        scrollPosition.indices.minOfOrNull {
             // index array can contain -1, indicating lane being empty (cell number > itemCount)
             // if any of the lanes are empty, we always on 0th item index
             if (it == -1) 0 else it
         } ?: 0
+    }
 
     /**
      * Current offset of the item with [firstVisibleItemIndex] relative to the container start.
@@ -107,10 +112,19 @@
      * This property is observable and when use it in composable function it will be recomposed on
      * each scroll, potentially causing performance issues.
      */
-    val firstVisibleItemScrollOffset: Int
-        get() = scrollPosition.offsets.run {
-            if (isEmpty()) 0 else this[scrollPosition.indices.indexOfMinValue()]
+    val firstVisibleItemScrollOffset: Int by derivedStateOf(structuralEqualityPolicy()) {
+        scrollPosition.offsets.let { offsets ->
+            val firstVisibleIndex = firstVisibleItemIndex
+            val indices = scrollPosition.indices
+            var minOffset = Int.MAX_VALUE
+            for (lane in offsets.indices) {
+                if (indices[lane] == firstVisibleIndex) {
+                    minOffset = minOf(minOffset, offsets[lane])
+                }
+            }
+            if (minOffset == Int.MAX_VALUE) 0 else minOffset
         }
+    }
 
     /** holder for current scroll position **/
     internal val scrollPosition = LazyStaggeredGridScrollPosition(
@@ -133,7 +147,7 @@
         mutableStateOf(EmptyLazyStaggeredGridLayoutInfo)
 
     /** storage for lane assignments for each item for consistent scrolling in both directions **/
-    internal val spans = LazyStaggeredGridSpans()
+    internal val laneInfo = LazyStaggeredGridLaneInfo()
 
     override var canScrollForward: Boolean by mutableStateOf(false)
         private set
@@ -170,9 +184,11 @@
     /* @VisibleForTesting */
     internal var measurePassCount = 0
 
-    /** states required for prefetching **/
+    /** transient information from measure required for prefetching **/
     internal var isVertical = false
     internal var laneWidthsPrefixSum: IntArray = IntArray(0)
+    internal var spanProvider: LazyStaggeredGridSpanProvider? = null
+    /** prefetch state **/
     private var prefetchBaseIndex: Int = -1
     private val currentItemPrefetchHandles = mutableMapOf<Int, PrefetchHandle>()
 
@@ -191,6 +207,11 @@
     internal val mutableInteractionSource = MutableInteractionSource()
 
     /**
+     * Stores currently pinned items which are always composed.
+     */
+    internal val pinnedItems = LazyLayoutPinnedItemList()
+
+    /**
      * Call this function to take control of scrolling and gain the ability to send scroll events
      * via [ScrollScope.scrollBy]. All actions that change the logical scroll position must be
      * performed within a [scroll] block (even if they don't call any other methods on this
@@ -329,15 +350,15 @@
 
                 // find the next item for each line and prefetch if it is valid
                 targetIndex = if (scrollingForward) {
-                    spans.findNextItemIndex(previousIndex, lane)
+                    laneInfo.findNextItemIndex(previousIndex, lane)
                 } else {
-                    spans.findPreviousItemIndex(previousIndex, lane)
+                    laneInfo.findPreviousItemIndex(previousIndex, lane)
                 }
                 if (
                     targetIndex !in (0 until info.totalItemsCount) ||
-                    previousIndex == targetIndex
+                        targetIndex in prefetchHandlesUsed
                 ) {
-                    return
+                    break
                 }
 
                 prefetchHandlesUsed += targetIndex
@@ -345,8 +366,12 @@
                     continue
                 }
 
-                val crossAxisSize = laneWidthsPrefixSum[lane] -
-                    if (lane == 0) 0 else laneWidthsPrefixSum[lane - 1]
+                val isFullSpan = spanProvider?.isFullSpan(targetIndex) == true
+                val slot = if (isFullSpan) 0 else lane
+                val span = if (isFullSpan) laneCount else 1
+
+                val crossAxisSize = laneWidthsPrefixSum[slot + span - 1] -
+                    if (slot == 0) 0 else laneWidthsPrefixSum[slot - 1]
 
                 val constraints = if (isVertical) {
                     Constraints.fixedWidth(crossAxisSize)
@@ -399,19 +424,22 @@
     }
 
     private fun fillNearestIndices(itemIndex: Int, laneCount: Int): IntArray {
-        // reposition spans if needed to ensure valid indices
-        spans.ensureValidIndex(itemIndex + laneCount)
-
-        val span = spans.getSpan(itemIndex)
-        val targetLaneIndex =
-            if (span == LazyStaggeredGridSpans.Unset) 0 else minOf(span, laneCount)
         val indices = IntArray(laneCount)
+        if (spanProvider?.isFullSpan(itemIndex) == true) {
+            indices.fill(itemIndex)
+            return indices
+        }
+
+        // reposition spans if needed to ensure valid indices
+        laneInfo.ensureValidIndex(itemIndex + laneCount)
+        val previousLane = laneInfo.getLane(itemIndex)
+        val targetLaneIndex = if (previousLane == Unset) 0 else minOf(previousLane, laneCount)
 
         // fill lanes before starting index
         var currentItemIndex = itemIndex
         for (lane in (targetLaneIndex - 1) downTo 0) {
-            indices[lane] = spans.findPreviousItemIndex(currentItemIndex, lane)
-            if (indices[lane] == -1) {
+            indices[lane] = laneInfo.findPreviousItemIndex(currentItemIndex, lane)
+            if (indices[lane] == Unset) {
                 indices.fill(-1, toIndex = lane)
                 break
             }
@@ -423,7 +451,7 @@
         // fill lanes after starting index
         currentItemIndex = itemIndex
         for (lane in (targetLaneIndex + 1) until laneCount) {
-            indices[lane] = spans.findNextItemIndex(currentItemIndex, lane)
+            indices[lane] = laneInfo.findNextItemIndex(currentItemIndex, lane)
             currentItemIndex = indices[lane]
         }
 
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 c9ff972..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
@@ -18,7 +18,7 @@
 
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.DecayAnimationSpec
-import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.LinearEasing
 import androidx.compose.animation.core.Spring
 import androidx.compose.animation.core.calculateTargetValue
 import androidx.compose.animation.core.spring
@@ -64,9 +64,14 @@
 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
+import kotlin.math.ceil
+import kotlin.math.floor
 import kotlin.math.roundToInt
+import kotlin.math.sign
 import kotlinx.coroutines.flow.drop
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.launch
@@ -458,7 +463,10 @@
     fun flingBehavior(
         state: PagerState,
         pagerSnapDistance: PagerSnapDistance = PagerSnapDistance.atMost(1),
-        lowVelocityAnimationSpec: AnimationSpec<Float> = tween(easing = LinearOutSlowInEasing),
+        lowVelocityAnimationSpec: AnimationSpec<Float> = tween(
+            easing = LinearEasing,
+            durationMillis = LowVelocityAnimationDefaultDuration
+        ),
         highVelocityAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
         snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
     ): SnapFlingBehavior {
@@ -593,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
                 }
@@ -616,18 +624,34 @@
         }
 
         override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
-            val effectivePageSize = pagerState.pageSize + pagerState.pageSpacing
-            val initialOffset = pagerState.currentPageOffsetFraction * effectivePageSize
-            val animationOffset =
+            val effectivePageSizePx = pagerState.pageSize + pagerState.pageSpacing
+            val animationOffsetPx =
                 decayAnimationSpec.calculateTargetValue(0f, initialVelocity)
+            val startPage = pagerState.firstVisiblePage?.let {
+                if (initialVelocity < 0) it.index + 1 else it.index
+            } ?: pagerState.currentPage
 
-            val startPage = pagerState.currentPage
-            val startPageOffset = startPage * effectivePageSize
+            val scrollOffset =
+                layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == startPage }?.offset ?: 0
 
-            val targetOffset =
-                (startPageOffset + initialOffset + animationOffset) / effectivePageSize
+            debugLog {
+                "Initial Offset=$scrollOffset " +
+                    "\nAnimation Offset=$animationOffsetPx " +
+                    "\nFling Start Page=$startPage " +
+                    "\nEffective Page Size=$effectivePageSizePx"
+            }
 
-            val targetPage = targetOffset.toInt()
+            val targetOffsetPx = startPage * effectivePageSizePx + animationOffsetPx
+
+            val targetPageValue = targetOffsetPx / effectivePageSizePx
+            val targetPage = if (initialVelocity > 0) {
+                ceil(targetPageValue)
+            } else {
+                floor(targetPageValue)
+            }.toInt().coerceIn(0, pagerState.pageCount)
+
+            debugLog { "Fling Target Page=$targetPage" }
+
             val correctedTargetPage = pagerSnapDistance.calculateTargetPage(
                 startPage,
                 targetPage,
@@ -636,9 +660,22 @@
                 pagerState.pageSpacing
             ).coerceIn(0, pagerState.pageCount)
 
-            val finalOffset = (correctedTargetPage - startPage) * effectivePageSize
+            debugLog { "Fling Corrected Target Page=$correctedTargetPage" }
 
-            return (finalOffset - initialOffset)
+            val proposedFlingOffset = (correctedTargetPage - startPage) * effectivePageSizePx
+
+            debugLog { "Proposed Fling Approach Offset=$proposedFlingOffset" }
+
+            val flingApproachOffsetPx =
+                (proposedFlingOffset.absoluteValue - scrollOffset.absoluteValue).coerceAtLeast(0)
+
+            return if (flingApproachOffsetPx == 0) {
+                flingApproachOffsetPx.toFloat()
+            } else {
+                flingApproachOffsetPx * initialVelocity.sign
+            }.also {
+                debugLog { "Fling Approach Offset=$it" }
+            }
         }
     }
 }
@@ -732,4 +769,13 @@
             pageRight { performForwardPaging() }
         }
     })
-}
\ No newline at end of file
+}
+
+private const val DEBUG = false
+private inline fun debugLog(generateMsg: () -> String) {
+    if (DEBUG) {
+        println("Pager: ${generateMsg()}")
+    }
+}
+
+private const val LowVelocityAnimationDefaultDuration = 500
\ No newline at end of file
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 9e3faa2..13a2201 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).roundToInt() / pageAvailableSpace
-            (currentPage + pageDisplacement).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"
@@ -298,6 +308,11 @@
             } else {
                 page + visiblePages.size.coerceAtMost(currentPosition)
             }
+
+            debugLog {
+                "animateScrollToPage with pre-jump to position=$preJumpPosition"
+            }
+
             // Pre-jump to 1 viewport away from destination item, if possible
             requireNotNull(lazyListState).scrollToItem(preJumpPosition)
             currentPosition = preJumpPosition
@@ -309,6 +324,8 @@
             distanceToSnapPosition + pageOffsetFraction * pageAvailableSpace
 
         val displacement = targetOffset - currentOffset + pageOffsetToSnappedPosition
+
+        debugLog { "animateScrollToPage $displacement pixels" }
         requireNotNull(lazyListState).animateScrollBy(displacement, animationSpec)
         animationTargetPage = -1
     }
@@ -388,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 {
@@ -417,6 +434,7 @@
     override val viewportStartOffset: Int = 0
     override val viewportEndOffset: Int = 0
     override val totalItemsCount: Int = 0
+    override val mainAxisItemSpacing: Int = 0
 }
 
 private val UnitDensity = object : Density {
@@ -427,4 +445,11 @@
 private val EmptyInteractionSources = object : InteractionSource {
     override val interactions: Flow<Interaction>
         get() = emptyFlow()
+}
+
+private const val DEBUG = false
+private inline fun debugLog(generateMsg: () -> String) {
+    if (DEBUG) {
+        println("PagerState: ${generateMsg()}")
+    }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/ClickableText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/ClickableText.kt
index a14f38b..05b8dad 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/ClickableText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/ClickableText.kt
@@ -15,17 +15,23 @@
  */
 package androidx.compose.foundation.text
 
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerEventPass
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.MultiParagraph
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
+import kotlinx.coroutines.launch
 
 /**
  * A continent version of [BasicText] component to be able to handle click event on the text.
@@ -92,4 +98,95 @@
             onTextLayout(it)
         }
     )
-}
\ No newline at end of file
+}
+
+/**
+ * A continent version of [BasicText] component to be able to handle click event on the text.
+ *
+ * This is a shorthand of [BasicText] with [pointerInput] to be able to handle click
+ * event easily.
+ *
+ * @sample androidx.compose.foundation.samples.ClickableText
+ *
+ * For other gestures, e.g. long press, dragging, follow sample code.
+ *
+ * @sample androidx.compose.foundation.samples.LongClickableText
+ *
+ * @see BasicText
+ * @see androidx.compose.ui.input.pointer.pointerInput
+ * @see androidx.compose.foundation.gestures.detectTapGestures
+ *
+ * @param text The text to be displayed.
+ * @param modifier Modifier to apply to this layout node.
+ * @param style Style configuration for the text such as color, font, line height etc.
+ * @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the
+ * text will be positioned as if there was unlimited horizontal space. If [softWrap] is false,
+ * [overflow] and [TextAlign] may have unexpected effects.
+ * @param overflow How visual overflow should be handled.
+ * @param maxLines An optional maximum number of lines for the text to span, wrapping if
+ * necessary. If the text exceeds the given number of lines, it will be truncated according to
+ * [overflow] and [softWrap]. If it is not null, then it must be greater than zero.
+ * @param onTextLayout Callback that is executed when a new text layout is calculated. A
+ * [TextLayoutResult] object that callback provides contains paragraph information, size of the
+ * text, baselines and other details. The callback can be used to add additional decoration or
+ * functionality to the text. For example, to draw selection around the text.
+ * @param onHover Callback that is executed when user hovers over the text with a mouse or trackpad.
+ * This callback is called with the hovered character's offset or null if the cursor is no longer
+ * hovering this.
+ * @param onClick Callback that is executed when users click the text. This callback is called
+ * with clicked character's offset.
+ */
+@ExperimentalFoundationApi // when removing this experimental annotation,
+// onHover should be nullable with null as default. The other ClickableText
+// should be deprecated as hidden and simply call this function.
+@Composable
+fun ClickableText(
+    text: AnnotatedString,
+    onHover: ((Int?) -> Unit),
+    modifier: Modifier = Modifier,
+    style: TextStyle = TextStyle.Default,
+    softWrap: Boolean = true,
+    overflow: TextOverflow = TextOverflow.Clip,
+    maxLines: Int = Int.MAX_VALUE,
+    onTextLayout: (TextLayoutResult) -> Unit = {},
+    onClick: (Int) -> Unit
+) {
+    val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
+    val coroutineScope = rememberCoroutineScope()
+
+    fun getOffset(positionOffset: Offset): Int? = layoutResult.value
+        ?.multiParagraph
+        ?.takeIf { it.containsWithinBounds(positionOffset) }
+        ?.getOffsetForPosition(positionOffset)
+
+    val pointerInputModifier = Modifier.pointerInput(onClick, onHover) {
+        coroutineScope.launch {
+            var previousIndex: Int? = null
+            detectMoves(PointerEventPass.Main) { pos ->
+                val index = getOffset(pos)
+                if (previousIndex != index) {
+                    previousIndex = index
+                    onHover(index)
+                }
+            }
+        }
+
+        detectTapGestures { pos -> getOffset(pos)?.let(onClick) }
+    }
+
+    BasicText(
+        text = text,
+        modifier = modifier.then(pointerInputModifier),
+        style = style,
+        softWrap = softWrap,
+        overflow = overflow,
+        maxLines = maxLines,
+        onTextLayout = {
+            layoutResult.value = it
+            onTextLayout(it)
+        }
+    )
+}
+
+private fun MultiParagraph.containsWithinBounds(positionOffset: Offset): Boolean =
+    positionOffset.let { (x, y) -> x > 0 && y >= 0 && x <= width && y <= height }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
index 28f4c56..cd39f68 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
@@ -36,6 +36,7 @@
 import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.clipRect
 import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.pointer.pointerHoverIcon
@@ -437,22 +438,32 @@
             state.layoutResult?.let {
                 state.drawScopeInvalidation
                 val selection = selectionRegistrar?.subselections?.get(state.selectableId)
+                val lastVisibleOffset = state.selectable?.getLastVisibleOffset() ?: 0
 
                 if (selection != null) {
                     val start = if (!selection.handlesCrossed) {
                         selection.start.offset
                     } else {
                         selection.end.offset
-                    }
+                    }.coerceIn(0, lastVisibleOffset)
+                    // selection path should end at the last visible character.
                     val end = if (!selection.handlesCrossed) {
                         selection.end.offset
                     } else {
                         selection.start.offset
-                    }
+                    }.coerceIn(0, lastVisibleOffset)
 
                     if (start != end) {
                         val selectionPath = it.multiParagraph.getPathForRange(start, end)
-                        drawPath(selectionPath, state.selectionBackgroundColor)
+                        // clip selection path drawing so that it doesn't overflow, unless
+                        // overflow is also TextOverflow.Visible
+                        if (it.layoutInput.overflow == TextOverflow.Visible) {
+                            drawPath(selectionPath, state.selectionBackgroundColor)
+                        } else {
+                            clipRect {
+                                drawPath(selectionPath, state.selectionBackgroundColor)
+                            }
+                        }
                     }
                 }
                 drawIntoCanvas { canvas ->
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/PointerMoveDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/PointerMoveDetector.kt
new file mode 100644
index 0000000..994642f
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/PointerMoveDetector.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.compose.foundation.text
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerEventType
+import androidx.compose.ui.input.pointer.PointerInputScope
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.isActive
+
+/**
+ * Detects pointer events that result from pointer movements and feed said events to the
+ * [onMove] function. When multiple pointers are being used, only the first one is tracked.
+ * If the first pointer is then removed, the second pointer will take its place as the first
+ * pointer and be tracked.
+ *
+ * @param pointerEventPass which pass to capture the pointer event from, see [PointerEventPass]
+ * @param onMove function that handles the position of move events
+ */
+internal suspend fun PointerInputScope.detectMoves(
+    pointerEventPass: PointerEventPass = PointerEventPass.Initial,
+    onMove: (Offset) -> Unit
+) = coroutineScope {
+    val currentContext = currentCoroutineContext()
+    awaitPointerEventScope {
+        var previousPosition: Offset? = null
+        while (currentContext.isActive) {
+            val event = awaitPointerEvent(pointerEventPass)
+            when (event.type) {
+                PointerEventType.Move, PointerEventType.Enter, PointerEventType.Exit ->
+                    event.changes.first().position
+                        .takeUnless { it == previousPosition }
+                        ?.let { position ->
+                            previousPosition = position
+                            onMove(position)
+                        }
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
index bf87a63..69c0ab3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
@@ -23,6 +23,7 @@
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.MotionDurationScale
 import androidx.compose.ui.composed
 import androidx.compose.ui.draw.drawWithContent
 import androidx.compose.ui.geometry.Offset
@@ -33,6 +34,7 @@
 import androidx.compose.ui.text.input.OffsetMapping
 import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.withContext
 
 @Suppress("ModifierInspectorInfo")
 internal fun Modifier.cursor(
@@ -46,10 +48,13 @@
     val isBrushSpecified = !(cursorBrush is SolidColor && cursorBrush.value.isUnspecified)
     if (state.hasFocus && value.selection.collapsed && isBrushSpecified) {
         LaunchedEffect(value.annotatedString, value.selection) {
-            // ensure that the value is always 1f _this_ frame by calling snapTo
-            cursorAlpha.snapTo(1f)
-            // then start the cursor blinking on animation clock (500ms on to start)
-            cursorAlpha.animateTo(0f, cursorAnimationSpec)
+            // Animate the cursor even when animations are disabled by the system.
+            withContext(FixedMotionDurationScale) {
+                // ensure that the value is always 1f _this_ frame by calling snapTo
+                cursorAlpha.snapTo(1f)
+                // then start the cursor blinking on animation clock (500ms on to start)
+                cursorAlpha.animateTo(0f, cursorAnimationSpec)
+            }
         }
         drawWithContent {
             this.drawContent()
@@ -88,3 +93,8 @@
 )
 
 internal val DefaultCursorThickness = 2.dp
+
+private object FixedMotionDurationScale : MotionDurationScale {
+    override val scaleFactor: Float
+        get() = 1f
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
index 4099d638..4d59f31 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
@@ -30,6 +30,40 @@
     private val layoutResultCallback: () -> TextLayoutResult?
 ) : Selectable {
 
+    private var _previousTextLayoutResult: TextLayoutResult? = null
+
+    // previously calculated `lastVisibleOffset` for the `_previousTextLayoutResult`
+    private var _previousLastVisibleOffset: Int = -1
+
+    /**
+     * TextLayoutResult is not expected to change repeatedly in a BasicText composable. At least
+     * most TextLayoutResult changes would likely affect Selection logic in some way. Therefore,
+     * this value only caches the last visible offset calculation for the latest seen
+     * TextLayoutResult instance. Object equality check is not worth the extra calculation as
+     * instance check is enough to accomplish whether a text layout has changed in a meaningful
+     * way.
+     */
+    private val TextLayoutResult.lastVisibleOffset: Int
+        @Synchronized get() {
+            if (_previousTextLayoutResult !== this) {
+                val lastVisibleLine = when {
+                    !didOverflowHeight || multiParagraph.didExceedMaxLines -> lineCount - 1
+                    else -> { // size.height < multiParagraph.height
+                        var finalVisibleLine = getLineForVerticalPosition(size.height.toFloat())
+                            .coerceAtMost(lineCount - 1)
+                        // if final visible line's top is equal to or larger than text layout
+                        // result's height, we need to check above lines one by one until we find
+                        // a line that fits in boundaries.
+                        while (getLineTop(finalVisibleLine) >= size.height) finalVisibleLine--
+                        finalVisibleLine
+                    }
+                }
+                _previousLastVisibleOffset = getLineEnd(lastVisibleLine, true)
+                _previousTextLayoutResult = this
+            }
+            return _previousLastVisibleOffset
+        }
+
     override fun updateSelection(
         startHandlePosition: Offset,
         endHandlePosition: Offset,
@@ -92,9 +126,11 @@
         if (getLayoutCoordinates() == null) return Offset.Zero
 
         val textLayoutResult = layoutResultCallback() ?: return Offset.Zero
+        val offset = if (isStartHandle) selection.start.offset else selection.end.offset
+        val coercedOffset = offset.coerceIn(0, textLayoutResult.lastVisibleOffset)
         return getSelectionHandleCoordinates(
             textLayoutResult = textLayoutResult,
-            offset = if (isStartHandle) selection.start.offset else selection.end.offset,
+            offset = coercedOffset,
             isStart = isStartHandle,
             areHandlesCrossed = selection.handlesCrossed
         )
@@ -122,14 +158,19 @@
 
     override fun getRangeOfLineContaining(offset: Int): TextRange {
         val textLayoutResult = layoutResultCallback() ?: return TextRange.Zero
-        val textLength = textLayoutResult.layoutInput.text.length
-        if (textLength < 1) return TextRange.Zero
-        val line = textLayoutResult.getLineForOffset(offset.coerceIn(0, textLength - 1))
+        val visibleTextLength = textLayoutResult.lastVisibleOffset
+        if (visibleTextLength < 1) return TextRange.Zero
+        val line = textLayoutResult.getLineForOffset(offset.coerceIn(0, visibleTextLength - 1))
         return TextRange(
             start = textLayoutResult.getLineStart(line),
             end = textLayoutResult.getLineEnd(line, visibleEnd = true)
         )
     }
+
+    override fun getLastVisibleOffset(): Int {
+        val textLayoutResult = layoutResultCallback() ?: return 0
+        return textLayoutResult.lastVisibleOffset
+    }
 }
 
 /**
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/Selectable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/Selectable.kt
index 5206418..0d227f8 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/Selectable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/Selectable.kt
@@ -20,7 +20,10 @@
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.MultiParagraph
+import androidx.compose.ui.text.TextLayoutInput
 import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.unit.Constraints
 
 /**
  * Provides [Selection] information for a composable to SelectionContainer. Composables who can
@@ -117,7 +120,17 @@
      * Return the offsets of the start and end of the line containing [offset], or [TextRange.Zero]
      * if the selectable is empty. These offsets are in the same "coordinate space" as
      * [getBoundingBox], and despite being returned in a [TextRange], may not refer to offsets in
-     * actual text if the selectable contains other types of content.
+     * actual text if the selectable contains other types of content. This function returns
+     * the last visible line's boundaries if offset is larger than text length or the character at
+     * given offset would fall on a line which is hidden by maxLines or Constraints.
      */
     fun getRangeOfLineContaining(offset: Int): TextRange
+
+    /**
+     * Returns the last visible character's offset. Some lines can be hidden due to either
+     * [TextLayoutInput.maxLines] or [Constraints.maxHeight] being smaller than
+     * [MultiParagraph.height]. If overflow is set to clip and a line is partially visible, it
+     * counts as the last visible line.
+     */
+    fun getLastVisibleOffset(): Int
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
index 87053fb..7e1069b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
@@ -334,10 +334,19 @@
         )
 
         val visibleBounds = containerCoordinates.visibleBounds()
-        this.startHandlePosition =
-            if (visibleBounds.containsInclusive(startHandlePosition)) startHandlePosition else null
-        this.endHandlePosition =
-            if (visibleBounds.containsInclusive(endHandlePosition)) endHandlePosition else null
+
+        // set the new handle position only if the handle is in visible bounds or
+        // the handle is still dragging. If handle goes out of visible bounds during drag, handle
+        // popup is also removed from composition, halting the drag gesture. This affects multiple
+        // text selection when selected text is configured with maxLines=1 and overflow=clip.
+        this.startHandlePosition = startHandlePosition.takeIf {
+            visibleBounds.containsInclusive(startHandlePosition) ||
+                draggingHandle == Handle.SelectionStart
+        }
+        this.endHandlePosition = endHandlePosition.takeIf {
+            visibleBounds.containsInclusive(endHandlePosition) ||
+                draggingHandle == Handle.SelectionEnd
+        }
     }
 
     /**
@@ -798,6 +807,8 @@
         // The end offset is exclusive.
         val offset = if (isStartHandle) anchor.offset else anchor.offset - 1
 
+        if (offset > selectable.getLastVisibleOffset()) return Offset.Unspecified
+
         // The horizontal position doesn't snap to cursor positions but should directly track the
         // actual drag.
         val localDragPosition = selectableCoordinates.localPositionOf(
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DesktopOverscroll.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DesktopOverscroll.kt
index 75bbcfc..bd9dcef 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DesktopOverscroll.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DesktopOverscroll.kt
@@ -17,37 +17,7 @@
 package androidx.compose.foundation
 
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.unit.Velocity
 
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
-internal actual fun rememberOverscrollEffect(): OverscrollEffect {
-    return remember {
-        DesktopEdgeEffectOverscrollEffect()
-    }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-private class DesktopEdgeEffectOverscrollEffect() : OverscrollEffect {
-    override fun consumePreScroll(
-        scrollDelta: Offset,
-        source: NestedScrollSource
-    ): Offset = Offset.Zero
-
-    override fun consumePostScroll(
-        initialDragDelta: Offset,
-        overscrollDelta: Offset,
-        source: NestedScrollSource
-    ) {}
-
-    override suspend fun consumePreFling(velocity: Velocity): Velocity = Velocity.Zero
-
-    override suspend fun consumePostFling(velocity: Velocity) {}
-
-    override val isInProgress = false
-    override val effectModifier = Modifier
-}
+internal actual fun rememberOverscrollEffect(): OverscrollEffect = NoOpOverscrollEffect
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/PointerMoveDetectorTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/PointerMoveDetectorTest.kt
new file mode 100644
index 0000000..aa03db7
--- /dev/null
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/PointerMoveDetectorTest.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.foundation.gestures.SuspendingGestureTestUtil
+import androidx.compose.ui.geometry.Offset
+import com.google.common.truth.Correspondence
+import com.google.common.truth.IterableSubject
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class PointerMoveDetectorTest {
+    @Test
+    fun whenSimpleMovement_allMovesAreReported() {
+        val actualMoves = mutableListOf<Offset>()
+        SuspendingGestureTestUtil {
+            detectMoves { actualMoves.add(it) }
+        }.executeInComposition {
+            down(5f, 5f)
+                .moveTo(4f, 4f)
+                .moveTo(3f, 3f)
+                .moveTo(2f, 2f)
+                .moveTo(1f, 1f)
+                .up()
+
+            assertThat(actualMoves).hasEqualOffsets(
+                listOf(
+                    Offset(4f, 4f),
+                    Offset(3f, 3f),
+                    Offset(2f, 2f),
+                    Offset(1f, 1f),
+                )
+            )
+        }
+    }
+
+    @Test
+    fun whenMultiplePointers_onlyUseFirst() {
+        val actualMoves = mutableListOf<Offset>()
+        SuspendingGestureTestUtil {
+            detectMoves { actualMoves.add(it) }
+        }.executeInComposition {
+            var m1 = down(5f, 5f)
+            var m2 = down(6f, 6f)
+            m1 = m1.moveTo(4f, 4f)
+            m2 = m2.moveTo(7f, 7f)
+            m1 = m1.moveTo(3f, 3f)
+            m2 = m2.moveTo(8f, 8f)
+            m1 = m1.moveTo(2f, 2f)
+            m2 = m2.moveTo(9f, 9f)
+            m1.moveTo(1f, 1f)
+            m2.moveTo(10f, 10f)
+            m1.up()
+            m2.up()
+
+            assertThat(actualMoves).hasEqualOffsets(
+                listOf(
+                    Offset(4f, 4f),
+                    Offset(3f, 3f),
+                    Offset(2f, 2f),
+                    Offset(1f, 1f),
+                )
+            )
+        }
+    }
+
+    @Test
+    fun whenMultiplePointers_thenFirstReleases_handOffToNextPointer() {
+        val actualMoves = mutableListOf<Offset>()
+        SuspendingGestureTestUtil {
+            detectMoves { actualMoves.add(it) }
+        }.executeInComposition {
+            var m1 = down(5f, 5f) // ignored because not a move
+            m1 = m1.moveTo(4f, 4f) // used
+            m1 = m1.moveTo(3f, 3f) // used
+            var m2 = down(4f, 4f) // ignored because still tracking m1
+            m1 = m1.moveTo(2f, 2f) // used
+            m2 = m2.moveTo(3f, 3f) // ignored because still tracking m1
+            m1.up() // ignored because not a move
+            m2.moveTo(2f, 2f) // ignored because equal to the previous used move
+            m2.moveTo(1f, 1f) // used
+            m2.up() // ignored because not a move
+
+            assertThat(actualMoves).hasEqualOffsets(
+                listOf(
+                    Offset(4f, 4f),
+                    Offset(3f, 3f),
+                    Offset(2f, 2f),
+                    Offset(1f, 1f),
+                )
+            )
+        }
+    }
+
+    private fun IterableSubject.hasEqualOffsets(expectedMoves: List<Offset>) {
+        comparingElementsUsing(offsetCorrespondence)
+            .containsExactly(*expectedMoves.toTypedArray())
+            .inOrder()
+    }
+
+    private val offsetCorrespondence: Correspondence<Offset, Offset> = Correspondence.from(
+        { o1, o2 -> o1!!.x == o2!!.x && o1.y == o2.y },
+        "has the offset of",
+    )
+}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt
index 6f49d4c..034e5cd 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt
@@ -223,6 +223,7 @@
     var getTextCalledTimes = 0
     var selectionToReturn: Selection? = null
     var textToReturn: AnnotatedString? = null
+    var lastVisibleOffsetToReturn: Int = 0
 
     var handlePosition = Offset.Zero
     var boundingBox = Rect.Zero
@@ -287,6 +288,10 @@
         return TextRange.Zero
     }
 
+    override fun getLastVisibleOffset(): Int {
+        return lastVisibleOffsetToReturn
+    }
+
     fun clear() {
         lastEndHandlePosition = null
         lastStartHandlePosition = null
@@ -299,5 +304,6 @@
         getTextCalledTimes = 0
         selectionToReturn = null
         textToReturn = null
+        lastVisibleOffsetToReturn = 0
     }
 }
\ No newline at end of file
diff --git a/compose/integration-tests/demos/build.gradle b/compose/integration-tests/demos/build.gradle
index 7b6b0ae..5be76f2 100644
--- a/compose/integration-tests/demos/build.gradle
+++ b/compose/integration-tests/demos/build.gradle
@@ -28,14 +28,6 @@
     implementation(project(":compose:ui:ui"))
 
     implementation("androidx.preference:preference-ktx:1.1.1")
-
-    androidTestImplementation(project(":compose:ui:ui-test-junit4"))
-    androidTestImplementation(libs.testCore)
-    androidTestImplementation(libs.testRules)
-    androidTestImplementation(libs.testRunner)
-    androidTestImplementation(libs.espressoCore)
-    androidTestImplementation(libs.junit)
-    androidTestImplementation(libs.truth)
 }
 
 android {
diff --git a/compose/integration-tests/demos/src/androidTest/AndroidManifest.xml b/compose/integration-tests/demos/src/androidTest/AndroidManifest.xml
deleted file mode 100644
index 7535bfb..0000000
--- a/compose/integration-tests/demos/src/androidTest/AndroidManifest.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright 2018 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<manifest />
diff --git a/compose/integration-tests/demos/src/androidTest/java/androidx/compose/integration/demos/test/DemoTest.kt b/compose/integration-tests/demos/src/androidTest/java/androidx/compose/integration/demos/test/DemoTest.kt
deleted file mode 100644
index 68967e3..0000000
--- a/compose/integration-tests/demos/src/androidTest/java/androidx/compose/integration/demos/test/DemoTest.kt
+++ /dev/null
@@ -1,319 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.integration.demos.test
-
-import android.content.Intent
-import androidx.compose.integration.demos.AllDemosCategory
-import androidx.compose.integration.demos.DemoActivity
-import androidx.compose.integration.demos.Tags
-import androidx.compose.integration.demos.common.ComposableDemo
-import androidx.compose.integration.demos.common.Demo
-import androidx.compose.integration.demos.common.DemoCategory
-import androidx.compose.integration.demos.common.allDemos
-import androidx.compose.integration.demos.common.allLaunchableDemos
-import androidx.compose.material3.demos.Material3Demos
-import androidx.compose.ui.test.ExperimentalTestApi
-import androidx.compose.ui.test.SemanticsNodeInteractionCollection
-import androidx.compose.ui.test.assertTextEquals
-import androidx.compose.ui.test.hasClickAction
-import androidx.compose.ui.test.hasText
-import androidx.compose.ui.test.isDialog
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.performScrollTo
-import androidx.compose.ui.test.runEmptyComposeUiTest
-import androidx.test.core.app.ActivityScenario
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.espresso.Espresso
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.LargeTest
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Ignore
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-private val ignoredDemos = listOf<String>(
-    // Not ignoring any of them \o/
-)
-
-@FlakyTest(bugId = 204322457)
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTestApi::class)
-class DemoTest {
-    // We need to provide the recompose factory first to use new clock.
-    @get:Rule
-    val rule = createAndroidComposeRule<DemoActivity>()
-
-    @Ignore // b/223396893
-    @Test
-    fun testFiltering() {
-        assertIsOnRootScreen()
-        // Enter filtering mode
-        rule.onNodeWithTag(Tags.FilterButton).performClick()
-
-        // TODO: use keyboard input APIs when available to actually filter the list
-        val testDemo = AllButIgnoredDemos.allLaunchableDemos()
-            // ActivityDemos don't set the title in the AppBar, so we can't verify if we've
-            // opened the right one. So, only use ComposableDemos
-            .filterIsInstance<ComposableDemo>()
-            .sortedBy { it.title }
-            .first()
-
-        // Click on the first demo.
-        val demoTitle = testDemo.title
-        rule.onNodeWithText(demoTitle).performScrollTo().performClick()
-        assertAppBarHasTitle(demoTitle)
-
-        // Navigate back to root screen.
-        Espresso.closeSoftKeyboard()
-        Espresso.pressBack()
-        rule.waitForIdle()
-        assertIsOnRootScreen()
-    }
-
-    @Test
-    @MediumTest
-    fun testAllDemosAreBeingTested() {
-        assertThat(
-            SplitDemoCategories.sumOf { it.allLaunchableDemos().size }
-        ).isEqualTo(AllButIgnoredDemos.allLaunchableDemos().size)
-    }
-
-    @Test
-    fun testPassingDemoNameDeeplinksForDemo() = runEmptyComposeUiTest {
-        val demo = AllButIgnoredDemos.allLaunchableDemos()[0]
-        val demoIntent = Intent(
-            ApplicationProvider.getApplicationContext(),
-            DemoActivity::class.java
-        ).apply { putExtra(DemoActivity.DEMO_NAME, demo.title) }
-        ActivityScenario.launch<DemoActivity>(demoIntent)
-        onNodeWithTag(Tags.AppBarTitle).assertTextEquals(demo.title)
-    }
-
-    @Test
-    fun navigateThroughAllDemos_1() {
-        navigateThroughAllDemos(SplitDemoCategories[0])
-    }
-
-    @Test
-    fun navigateThroughAllDemos_2() {
-        navigateThroughAllDemos(SplitDemoCategories[1])
-    }
-
-    @Test
-    fun navigateThroughAllDemos_3() {
-        navigateThroughAllDemos(SplitDemoCategories[2])
-    }
-
-    // Broken: b/206811195
-    @Ignore
-    @Test
-    fun navigateThroughAllDemos_4() {
-        navigateThroughAllDemos(SplitDemoCategories[3])
-    }
-
-    @Test
-    fun navigateThroughMaterial3Demos() {
-        val material3Demos = DemoCategory(
-            "Jetpack Compose Demos",
-            listOf(
-                Material3Demos,
-            )
-        )
-        navigateThroughAllDemos(material3Demos)
-    }
-
-    private fun navigateThroughAllDemos(root: DemoCategory, fastForwardClock: Boolean = false) {
-        // Keep track of each demo we visit
-        val visitedDemos = mutableListOf<Demo>()
-
-        // Visit all demos, ensuring we start and end up on the root screen
-        assertIsOnRootScreen()
-        root.visitDemos(
-            visitedDemos = visitedDemos,
-            path = listOf(root),
-            fastForwardClock = fastForwardClock
-        )
-        assertIsOnRootScreen()
-
-        // Ensure that we visited all the demos we expected to, in the order we expected to.
-        assertThat(visitedDemos).isEqualTo(root.allDemos())
-    }
-
-    /**
-     * DFS traversal of each demo in a [DemoCategory] using [Demo.visit]
-     *
-     * @param path The path of categories that leads to this demo
-     */
-    private fun DemoCategory.visitDemos(
-        visitedDemos: MutableList<Demo>,
-        path: List<DemoCategory>,
-        fastForwardClock: Boolean
-    ) {
-        demos.forEach { demo ->
-            visitedDemos.add(demo)
-            demo.visit(visitedDemos, path, fastForwardClock)
-        }
-    }
-
-    /**
-     * Visits a [Demo], and then navigates back up to the [DemoCategory] it was inside.
-     *
-     * If this [Demo] is a [DemoCategory], this will visit sub-[Demo]s first before continuing
-     * in the current category.
-     *
-     * @param path The path of categories that leads to this demo
-     */
-    private fun Demo.visit(
-        visitedDemos: MutableList<Demo>,
-        path: List<DemoCategory>,
-        fastForwardClock: Boolean
-    ) {
-        val navigationTitle = path.navigationTitle
-
-        if (fastForwardClock) {
-            // Skip through the enter animation of the list screen
-            fastForwardClock()
-        }
-
-        rule.onNode(hasText(title) and hasClickAction())
-            .assertExists("Couldn't find \"$title\" in \"$navigationTitle\"")
-            .performScrollTo()
-            .performClick()
-
-        if (this is DemoCategory) {
-            visitDemos(visitedDemos, path + this, fastForwardClock)
-        }
-
-        if (fastForwardClock) {
-            // Skip through the enter animation of the visited demo
-            fastForwardClock()
-        }
-
-        rule.waitForIdle()
-        while (rule.onAllNodes(isDialog()).isNotEmpty()) {
-            Espresso.pressBack()
-            rule.waitForIdle()
-        }
-
-        clearFocusFromDemo()
-        rule.waitForIdle()
-
-        Espresso.pressBack()
-        rule.waitForIdle()
-
-        if (fastForwardClock) {
-            // Pump press back
-            fastForwardClock(2000)
-        }
-
-        assertAppBarHasTitle(navigationTitle)
-    }
-
-    private fun fastForwardClock(millis: Long = 5000) {
-        rule.waitForIdle()
-        rule.mainClock.advanceTimeBy(millis)
-    }
-
-    /**
-     * Asserts that the app bar title matches the root category title, so we are on the root screen.
-     */
-    private fun assertIsOnRootScreen() = assertAppBarHasTitle(AllDemosCategory.title)
-
-    /**
-     * Asserts that the app bar title matches the given [title].
-     */
-    private fun assertAppBarHasTitle(title: String) =
-        rule.onNodeWithTag(Tags.AppBarTitle).assertTextEquals(title)
-
-    private fun SemanticsNodeInteractionCollection.isNotEmpty(): Boolean {
-        return fetchSemanticsNodes(atLeastOneRootRequired = false).isNotEmpty()
-    }
-
-    private fun clearFocusFromDemo() {
-        with(rule.activity) {
-            if (hostView.hasFocus()) {
-                if (hostView.isFocused) {
-                    // One of the Compose components has focus.
-                    focusManager.clearFocus(force = true)
-                } else {
-                    // A child view has focus. (View interop scenario).
-                    // We could also use hostViewGroup.focusedChild?.clearFocus(), but the
-                    // interop views might end up being focused if one of them is marked as
-                    // focusedByDefault. So we clear focus by requesting focus on the owner.
-                    rule.runOnUiThread { hostView.requestFocus() }
-                }
-            }
-        }
-    }
-}
-
-internal val AllButIgnoredDemos =
-    AllDemosCategory.filter { path, demo ->
-        demo.navigationTitle(path) !in ignoredDemos
-    }
-
-private val SplitDemoCategories = AllButIgnoredDemos.let { root ->
-    root.allLaunchableDemos().let { leaves ->
-        val size = leaves.size
-        leaves.withIndex()
-            .groupBy { it.index * 4 / size }
-            .map {
-                val selectedLeaves = it.value.map { it.value }
-                root.filter { _, demo ->
-                    demo in selectedLeaves
-                }
-            }
-    }
-}
-
-private fun Demo.navigationTitle(path: List<DemoCategory>): String {
-    return path.plus(this).navigationTitle
-}
-
-private val List<Demo>.navigationTitle: String
-    get() = if (size == 1) first().title else drop(1).joinToString(" > ")
-
-/**
- * Trims the tree of [Demo]s represented by this [DemoCategory] by cutting all leave demos for
- * which the [predicate] returns `false` and recursively removing all empty categories as a result.
- */
-private fun DemoCategory.filter(
-    path: List<DemoCategory> = emptyList(),
-    predicate: (path: List<DemoCategory>, demo: Demo) -> Boolean
-): DemoCategory {
-    val newPath = path + this
-    return DemoCategory(
-        title,
-        demos.mapNotNull {
-            when (it) {
-                is DemoCategory -> {
-                    it.filter(newPath, predicate).let { if (it.demos.isEmpty()) null else it }
-                }
-                else -> {
-                    if (predicate(newPath, it)) it else null
-                }
-            }
-        }
-    )
-}
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/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/PagerActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/PagerActivity.kt
index 3ffe581..a9466ca 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/PagerActivity.kt
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/PagerActivity.kt
@@ -23,8 +23,9 @@
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PageSize
 import androidx.compose.foundation.pager.rememberPagerState
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
@@ -44,12 +45,18 @@
 
         setContent {
             val pagerState = rememberPagerState()
-            Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+            Box(
+                modifier = Modifier
+                    .fillMaxSize(),
+                contentAlignment = Alignment.Center
+            ) {
                 HorizontalPager(
                     modifier = Modifier
+                        .height(400.dp)
                         .semantics { contentDescription = "Pager" }
                         .background(Color.White),
                     state = pagerState,
+                    pageSize = PageSize.Fill,
                     pageCount = itemCount
                 ) {
                     PagerItem(it)
@@ -69,9 +76,8 @@
 private fun PagerItem(index: Int) {
     Box(
         modifier = Modifier
-            .size(200.dp, 400.dp)
-            .background(Color.Black),
-        contentAlignment = Alignment.Center
+            .fillMaxSize()
+            .background(Color.Black)
     ) {
         Text(text = index.toString(), color = Color.White)
     }
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/ViewPagerActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/ViewPagerActivity.kt
index b1be881..01ac67f 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/ViewPagerActivity.kt
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/ViewPagerActivity.kt
@@ -22,20 +22,16 @@
 import android.view.ViewGroup
 import android.widget.TextView
 import androidx.appcompat.app.AppCompatActivity
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.PagerSnapHelper
 import androidx.recyclerview.widget.RecyclerView
+import androidx.viewpager2.widget.ViewPager2
 
 class ViewPagerActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_view_pager)
-        val pager = findViewById<RecyclerView>(R.id.pager)
-        pager.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
+        val pager = findViewById<ViewPager2>(R.id.pager)
         val itemCount = intent.getIntExtra(ExtraItemCount, 3000)
         val adapter = PagerAdapter(itemCount)
-        val scroller = PagerSnapHelper()
-        scroller.attachToRecyclerView(pager)
         pager.adapter = adapter
         launchIdlenessTracking()
     }
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/layout/activity_view_pager.xml b/compose/integration-tests/macrobenchmark-target/src/main/res/layout/activity_view_pager.xml
index 1ce3f46..50631f8 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/res/layout/activity_view_pager.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/layout/activity_view_pager.xml
@@ -20,14 +20,14 @@
     android:layout_gravity="center"
     android:gravity="center"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="400dp">
 
-    <androidx.recyclerview.widget.RecyclerView
+    <androidx.viewpager2.widget.ViewPager2
         xmlns:android="http://schemas.android.com/apk/res/android"
         android:id="@+id/pager"
         android:layout_gravity="center"
         android:gravity="center"
         android:orientation="horizontal"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content" />
+        android:layout_height="match_parent" />
 </FrameLayout>
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/layout/view_pager_item.xml b/compose/integration-tests/macrobenchmark-target/src/main/res/layout/view_pager_item.xml
index 33e2e5c..69067a4 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/res/layout/view_pager_item.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/layout/view_pager_item.xml
@@ -15,9 +15,9 @@
   -->
 
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="200dp"
+    android:layout_width="match_parent"
     android:background="#000000"
-    android:layout_height="400dp"
+    android:layout_height="match_parent"
     android:layout_gravity="center">
 
     <TextView
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/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt b/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
index 9c4339a..23b8cb7 100644
--- a/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
+++ b/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
@@ -26,8 +26,8 @@
 import com.intellij.psi.util.ClassUtil
 import kotlinx.metadata.KmDeclarationContainer
 import kotlinx.metadata.KmFunction
-import kotlinx.metadata.jvm.KotlinClassHeader
 import kotlinx.metadata.jvm.KotlinClassMetadata
+import kotlinx.metadata.jvm.Metadata
 import kotlinx.metadata.jvm.signature
 
 /**
@@ -48,12 +48,12 @@
  * represents a synthetic class.
  */
 private fun PsiClass.getKmDeclarationContainer(): KmDeclarationContainer? {
-    val classKotlinMetadataAnnotation = annotations.find {
+    val classKotlinMetadataPsiAnnotation = annotations.find {
         // hasQualifiedName() not available on the min version of Lint we compile against
         it.qualifiedName == KotlinMetadataFqn
     } ?: return null
 
-    val metadata = KotlinClassMetadata.read(classKotlinMetadataAnnotation.toHeader())
+    val metadata = KotlinClassMetadata.read(classKotlinMetadataPsiAnnotation.toMetadataAnnotation())
         ?: return null
 
     return when (metadata) {
@@ -67,12 +67,9 @@
 }
 
 /**
- * Returns a [KotlinClassHeader] by parsing the attributes of this @kotlin.Metadata annotation.
- *
- * See: https://github.com/udalov/kotlinx-metadata-examples/blob/master/src/main/java
- * /examples/FindKotlinGeneratedMethods.java
+ * Returns a [Metadata] by parsing the attributes of this @kotlin.Metadata PSI annotation.
  */
-private fun PsiAnnotation.toHeader(): KotlinClassHeader {
+private fun PsiAnnotation.toMetadataAnnotation(): Metadata {
     val attributes = attributes.associate { it.attributeName to it.attributeValue }
 
     fun JvmAnnotationAttributeValue.parseString(): String =
@@ -99,7 +96,7 @@
     val packageName = attributes["pn"]?.parseString()
     val extraInt = attributes["xi"]?.parseInt()
 
-    return KotlinClassHeader(
+    return Metadata(
         kind,
         metadataVersion,
         data1,
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-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt
index ae926e8..fa4fa68 100644
--- a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt
+++ b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt
@@ -29,11 +29,10 @@
 import androidx.annotation.RequiresApi
 import androidx.compose.foundation.interaction.PressInteraction
 import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.geometry.toRect
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.toAndroidRect
 import androidx.compose.ui.graphics.toArgb
 import java.lang.reflect.Method
+import kotlin.math.roundToInt
 
 /**
  * Empty [View] that hosts a [RippleDrawable] as its background. This is needed as
@@ -179,7 +178,12 @@
         // another invalidation, etc.
         ripple.trySetRadius(radius)
         ripple.setColor(color, alpha)
-        val newBounds = size.toRect().toAndroidRect()
+        val newBounds = Rect(
+            0,
+            0,
+            size.width.roundToInt(),
+            size.height.roundToInt()
+        )
         // Drawing the background causes the view to update the bounds of the drawable
         // based on the view's bounds, so we need to adjust the view itself to match the
         // canvas' bounds.
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index a062b45..b032320 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 {
@@ -983,7 +985,7 @@
 
   public final class PullRefreshKt {
     method @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.ui.Modifier pullRefresh(androidx.compose.ui.Modifier, androidx.compose.material.pullrefresh.PullRefreshState state, optional boolean enabled);
-    method @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.ui.Modifier pullRefresh(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> onPull, kotlin.jvm.functions.Function2<? super java.lang.Float,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onRelease, optional boolean enabled);
+    method @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.ui.Modifier pullRefresh(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> onPull, kotlin.jvm.functions.Function2<? super java.lang.Float,? super kotlin.coroutines.Continuation<? super java.lang.Float>,?> onRelease, optional boolean enabled);
   }
 
   @androidx.compose.material.ExperimentalMaterialApi public final class PullRefreshState {
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/samples/src/main/java/androidx/compose/material/samples/PullRefreshSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/PullRefreshSamples.kt
index 2abcf1f..cc5a0b7 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/PullRefreshSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/PullRefreshSamples.kt
@@ -115,16 +115,25 @@
         }
     }
 
-    suspend fun onRelease() {
-        if (refreshing) return // Already refreshing - don't call refresh again.
+    fun onRelease(velocity: Float): Float {
+        if (refreshing) return 0f // Already refreshing - don't call refresh again.
         if (currentDistance > threshold) refresh()
 
-        animate(initialValue = currentDistance, targetValue = 0f) { value, _ ->
-            currentDistance = value
+        refreshScope.launch {
+            animate(initialValue = currentDistance, targetValue = 0f) { value, _ ->
+                currentDistance = value
+            }
+        }
+
+        // Only consume if the fling is downwards and the indicator is visible
+        return if (velocity > 0f && currentDistance > 0f) {
+            velocity
+        } else {
+            0f
         }
     }
 
-    Box(Modifier.pullRefresh(::onPull, { onRelease() })) {
+    Box(Modifier.pullRefresh(::onPull, ::onRelease)) {
         LazyColumn {
             if (!refreshing) {
                 items(itemCount) {
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
index 44bff47..c5cbba9 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
@@ -267,7 +267,7 @@
                 sheetContent = {
                     Box(
                         Modifier
-                            .fillMaxSize()
+                            .fillMaxSize(0.6f)
                             .testTag(sheetTag)
                     )
                 }
@@ -1137,4 +1137,74 @@
             )
             .assertWidthIsEqualTo(expectedSheetWidth)
     }
+
+    @Test
+    fun modalBottomSheet_shortSheet_anchorChangeHandler_previousTargetNotInAnchors_reconciles() {
+        val sheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden)
+        var hasSheetContent by mutableStateOf(false) // Start out with empty sheet content
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            ModalBottomSheetLayout(
+                sheetState = sheetState,
+                sheetContent = {
+                    if (hasSheetContent) {
+                        Box(Modifier.fillMaxHeight(0.4f))
+                    }
+                },
+                content = {}
+            )
+        }
+
+        assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Hidden)
+        assertThat(sheetState.swipeableState.hasAnchorForValue(ModalBottomSheetValue.HalfExpanded))
+            .isFalse()
+        assertThat(sheetState.swipeableState.hasAnchorForValue(ModalBottomSheetValue.Expanded))
+            .isFalse()
+
+        scope.launch { sheetState.show() }
+        rule.waitForIdle()
+
+        assertThat(sheetState.isVisible).isTrue()
+        assertThat(sheetState.currentValue).isEqualTo(sheetState.targetValue)
+
+        hasSheetContent = true // Recompose with sheet content
+        rule.waitForIdle()
+        assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Expanded)
+    }
+
+    @Test
+    fun modalBottomSheet_tallSheet_anchorChangeHandler_previousTargetNotInAnchors_reconciles() {
+        val sheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden)
+        var hasSheetContent by mutableStateOf(false) // Start out with empty sheet content
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            ModalBottomSheetLayout(
+                sheetState = sheetState,
+                sheetContent = {
+                    if (hasSheetContent) {
+                        Box(Modifier.fillMaxHeight(0.6f))
+                    }
+                },
+                content = {}
+            )
+        }
+
+        assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Hidden)
+        assertThat(sheetState.swipeableState.hasAnchorForValue(ModalBottomSheetValue.HalfExpanded))
+            .isFalse()
+        assertThat(sheetState.swipeableState.hasAnchorForValue(ModalBottomSheetValue.Expanded))
+            .isFalse()
+
+        scope.launch { sheetState.show() }
+        rule.waitForIdle()
+
+        assertThat(sheetState.isVisible).isTrue()
+        assertThat(sheetState.currentValue).isEqualTo(sheetState.targetValue)
+
+        hasSheetContent = true // Recompose with sheet content
+        rule.waitForIdle()
+        assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.HalfExpanded)
+    }
 }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarHostTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarHostTest.kt
index 18ac04d..1ded9e5 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarHostTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarHostTest.kt
@@ -33,6 +33,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.MediumTest
+import androidx.test.filters.RequiresDevice
 import com.google.common.truth.Truth
 import com.nhaarman.mockitokotlin2.any
 import com.nhaarman.mockitokotlin2.doReturn
@@ -82,6 +83,7 @@
         rule.waitUntil { job.isCompleted }
     }
 
+    @RequiresDevice // b/264895456
     @Test
     fun snackbarHost_fifoQueueContract() {
         var resultedInvocation = ""
@@ -111,6 +113,7 @@
         Truth.assertThat(resultedInvocation).isEqualTo("0123456789")
     }
 
+    @RequiresDevice // b/264895456
     @Test
     @LargeTest
     fun snackbarHost_returnedResult() {
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
index 318ea0a..d9c5610 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
@@ -17,11 +17,17 @@
 package androidx.compose.material.pullrefresh
 
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.ExperimentalMaterialApi
 import androidx.compose.material.Text
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.platform.testTag
@@ -29,11 +35,14 @@
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipeDown
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
 import kotlin.math.abs
 import kotlin.math.pow
+import kotlinx.coroutines.runBlocking
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -166,7 +175,7 @@
             assertThat(refreshCount).isEqualTo(0)
         }
 
-        state.onRelease()
+        state.onRelease(0f)
 
         rule.runOnIdle {
             assertThat(state.progress).isEqualTo(0f)
@@ -216,7 +225,7 @@
             assertThat(refreshCount).isEqualTo(0)
         }
 
-        state.onRelease()
+        state.onRelease(0f)
 
         rule.runOnIdle {
             assertThat(state.progress).isEqualTo(0f)
@@ -255,7 +264,7 @@
             assertThat(refreshCount).isEqualTo(0)
         }
 
-        state.onRelease()
+        state.onRelease(0f)
 
         rule.runOnIdle {
             assertThat(state.progress).isEqualTo(0f)
@@ -385,6 +394,633 @@
         }
     }
 
+    @Test
+    fun nestedPreScroll_negativeDelta_notRefreshing() {
+        val refreshThreshold = 200f
+        lateinit var state: PullRefreshState
+
+        val dispatcher = NestedScrollDispatcher()
+        val connection = object : NestedScrollConnection {}
+
+        rule.setContent {
+            state = rememberPullRefreshState(
+                refreshing = false,
+                onRefresh = { },
+                refreshThreshold = with(LocalDensity.current) { refreshThreshold.toDp() }
+            )
+            Box(Modifier.size(200.dp).pullRefresh(state)) {
+                Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+            }
+        }
+
+        // 100 pixels up
+        val dragUpOffset = Offset(0f, -100f)
+
+        rule.runOnIdle {
+            val preConsumed = dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.Drag)
+            // Pull refresh is not showing, so we should consume nothing
+            assertThat(preConsumed).isEqualTo(Offset.Zero)
+            assertThat(state.position).isEqualTo(0f)
+        }
+
+        // Pull the state by a bit
+        state.onPull(200f)
+
+        rule.runOnIdle {
+            assertThat(state.position).isEqualTo(100f /* 200 / 2 for drag multiplier */)
+            val preConsumed = dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.Drag)
+            // Pull refresh is currently showing, so we should consume all the delta
+            assertThat(preConsumed).isEqualTo(dragUpOffset)
+            assertThat(state.position).isEqualTo(50f /* (200 - 100) / 2 for drag multiplier */)
+        }
+    }
+
+    @Test
+    fun nestedPreScroll_negativeDelta_refreshing() {
+        val refreshingOffset = 500f
+        lateinit var state: PullRefreshState
+
+        val dispatcher = NestedScrollDispatcher()
+        val connection = object : NestedScrollConnection {}
+
+        rule.setContent {
+            state = rememberPullRefreshState(
+                refreshing = true,
+                onRefresh = { },
+                refreshingOffset = with(LocalDensity.current) { refreshingOffset.toDp() }
+            )
+            Box(Modifier.size(200.dp).pullRefresh(state)) {
+                Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+            }
+        }
+
+        // 100 pixels up
+        val dragUpOffset = Offset(0f, -100f)
+
+        rule.runOnIdle {
+            val preConsumed = dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.Drag)
+            // Pull refresh is refreshing, so we should consume nothing
+            assertThat(preConsumed).isEqualTo(Offset.Zero)
+            assertThat(state.position).isEqualTo(refreshingOffset)
+        }
+    }
+
+    @Test
+    fun nestedPreScroll_positiveDelta_notRefreshing() {
+        val refreshThreshold = 200f
+        lateinit var state: PullRefreshState
+
+        val dispatcher = NestedScrollDispatcher()
+        val connection = object : NestedScrollConnection {}
+
+        rule.setContent {
+            state = rememberPullRefreshState(
+                refreshing = false,
+                onRefresh = { },
+                refreshThreshold = with(LocalDensity.current) { refreshThreshold.toDp() }
+            )
+            Box(Modifier.size(200.dp).pullRefresh(state)) {
+                Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+            }
+        }
+
+        // 100 pixels down
+        val dragUpOffset = Offset(0f, 100f)
+
+        rule.runOnIdle {
+            val preConsumed = dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.Drag)
+            // We should ignore positive delta in prescroll, so we should consume nothing
+            assertThat(preConsumed).isEqualTo(Offset.Zero)
+            assertThat(state.position).isEqualTo(0f)
+        }
+
+        // Pull the state by a bit
+        state.onPull(200f)
+
+        rule.runOnIdle {
+            assertThat(state.position).isEqualTo(100f /* 200 / 2 for drag multiplier */)
+            val preConsumed = dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.Drag)
+            // We should ignore positive delta in prescroll, so we should consume nothing
+            assertThat(preConsumed).isEqualTo(Offset.Zero)
+            assertThat(state.position).isEqualTo(100f /* 200 / 2 for drag multiplier */)
+        }
+    }
+
+    @Test
+    fun nestedPreScroll_positiveDelta_refreshing() {
+        val refreshingOffset = 500f
+        lateinit var state: PullRefreshState
+
+        val dispatcher = NestedScrollDispatcher()
+        val connection = object : NestedScrollConnection {}
+
+        rule.setContent {
+            state = rememberPullRefreshState(
+                refreshing = true,
+                onRefresh = { },
+                refreshingOffset = with(LocalDensity.current) { refreshingOffset.toDp() }
+            )
+            Box(Modifier.size(200.dp).pullRefresh(state)) {
+                Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+            }
+        }
+
+        // 100 pixels down
+        val dragUpOffset = Offset(0f, 100f)
+
+        rule.runOnIdle {
+            val preConsumed = dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.Drag)
+            // Pull refresh is refreshing, so we should consume nothing
+            assertThat(preConsumed).isEqualTo(Offset.Zero)
+            assertThat(state.position).isEqualTo(refreshingOffset)
+        }
+    }
+
+    @Test
+    fun nestedPostScroll_negativeDelta_notRefreshing() {
+        val refreshThreshold = 200f
+        lateinit var state: PullRefreshState
+
+        val dispatcher = NestedScrollDispatcher()
+        val connection = object : NestedScrollConnection {}
+
+        rule.setContent {
+            state = rememberPullRefreshState(
+                refreshing = false,
+                onRefresh = { },
+                refreshThreshold = with(LocalDensity.current) { refreshThreshold.toDp() }
+            )
+            Box(Modifier.size(200.dp).pullRefresh(state)) {
+                Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+            }
+        }
+
+        // 100 pixels up
+        val dragUpOffset = Offset(0f, -100f)
+
+        rule.runOnIdle {
+            val postConsumed = dispatcher.dispatchPostScroll(
+                Offset.Zero,
+                dragUpOffset,
+                NestedScrollSource.Drag
+            )
+            // We should ignore negative delta in postscroll, so we should consume nothing
+            assertThat(postConsumed).isEqualTo(Offset.Zero)
+            assertThat(state.position).isEqualTo(0f)
+        }
+
+        // Pull the state by a bit
+        state.onPull(200f)
+
+        rule.runOnIdle {
+            assertThat(state.position).isEqualTo(100f /* 200 / 2 for drag multiplier */)
+            val postConsumed = dispatcher.dispatchPostScroll(
+                Offset.Zero,
+                dragUpOffset,
+                NestedScrollSource.Drag
+            )
+            // We should ignore negative delta in postscroll, so we should consume nothing
+            assertThat(postConsumed).isEqualTo(Offset.Zero)
+            assertThat(state.position).isEqualTo(100f /* 200 / 2 for drag multiplier */)
+        }
+    }
+
+    @Test
+    fun nestedPostScroll_negativeDelta_refreshing() {
+        val refreshingOffset = 500f
+        lateinit var state: PullRefreshState
+
+        val dispatcher = NestedScrollDispatcher()
+        val connection = object : NestedScrollConnection {}
+
+        rule.setContent {
+            state = rememberPullRefreshState(
+                refreshing = true,
+                onRefresh = { },
+                refreshingOffset = with(LocalDensity.current) { refreshingOffset.toDp() }
+            )
+            Box(Modifier.size(200.dp).pullRefresh(state)) {
+                Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+            }
+        }
+
+        // 100 pixels up
+        val dragUpOffset = Offset(0f, -100f)
+
+        rule.runOnIdle {
+            val postConsumed = dispatcher.dispatchPostScroll(
+                Offset.Zero,
+                dragUpOffset,
+                NestedScrollSource.Drag
+            )
+            // Pull refresh is refreshing, so we should consume nothing
+            assertThat(postConsumed).isEqualTo(Offset.Zero)
+            assertThat(state.position).isEqualTo(refreshingOffset)
+        }
+    }
+
+    @Test
+    fun nestedPostScroll_positiveDelta_notRefreshing() {
+        val refreshThreshold = 200f
+        lateinit var state: PullRefreshState
+
+        val dispatcher = NestedScrollDispatcher()
+        val connection = object : NestedScrollConnection {}
+
+        rule.setContent {
+            state = rememberPullRefreshState(
+                refreshing = false,
+                onRefresh = { },
+                refreshThreshold = with(LocalDensity.current) { refreshThreshold.toDp() }
+            )
+            Box(Modifier.size(200.dp).pullRefresh(state)) {
+                Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+            }
+        }
+
+        // 100 pixels down
+        val dragUpOffset = Offset(0f, 100f)
+
+        rule.runOnIdle {
+            val postConsumed = dispatcher.dispatchPostScroll(
+                Offset.Zero,
+                dragUpOffset,
+                NestedScrollSource.Drag
+            )
+            // We should consume all the delta
+            assertThat(postConsumed).isEqualTo(dragUpOffset)
+            assertThat(state.position).isEqualTo(50f /* 100 / 2 for drag multiplier */)
+        }
+
+        // Pull the state by a bit
+        state.onPull(200f)
+
+        rule.runOnIdle {
+            assertThat(state.position).isEqualTo(150f /* (100 + 200) / 2 for drag multiplier */)
+            val postConsumed = dispatcher.dispatchPostScroll(
+                Offset.Zero,
+                dragUpOffset,
+                NestedScrollSource.Drag
+            )
+            // We should consume all the delta again
+            assertThat(postConsumed).isEqualTo(dragUpOffset)
+            assertThat(state.position)
+                .isEqualTo(200f /* (100 + 200 + 100) / 2 for drag multiplier */)
+        }
+    }
+
+    @Test
+    fun nestedPostScroll_positiveDelta_refreshing() {
+        val refreshingOffset = 500f
+        lateinit var state: PullRefreshState
+
+        val dispatcher = NestedScrollDispatcher()
+        val connection = object : NestedScrollConnection {}
+
+        rule.setContent {
+            state = rememberPullRefreshState(
+                refreshing = true,
+                onRefresh = { },
+                refreshingOffset = with(LocalDensity.current) { refreshingOffset.toDp() }
+            )
+            Box(Modifier.size(200.dp).pullRefresh(state)) {
+                Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+            }
+        }
+
+        // 100 pixels down
+        val dragUpOffset = Offset(0f, 100f)
+
+        rule.runOnIdle {
+            val postConsumed = dispatcher.dispatchPostScroll(
+                Offset.Zero,
+                dragUpOffset,
+                NestedScrollSource.Drag
+            )
+            // Pull refresh is refreshing, so we should consume nothing
+            assertThat(postConsumed).isEqualTo(Offset.Zero)
+            assertThat(state.position).isEqualTo(refreshingOffset)
+        }
+    }
+
+    @Test
+    fun nestedPreFling_negativeVelocity_notRefreshing() {
+        val refreshThreshold = 200f
+        lateinit var state: PullRefreshState
+        var onRefreshCalled = false
+
+        val dispatcher = NestedScrollDispatcher()
+        val connection = object : NestedScrollConnection {}
+
+        rule.setContent {
+            state = rememberPullRefreshState(
+                refreshing = false,
+                onRefresh = { onRefreshCalled = true },
+                refreshThreshold = with(LocalDensity.current) { refreshThreshold.toDp() }
+            )
+            Box(Modifier.size(200.dp).pullRefresh(state)) {
+                Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+            }
+        }
+
+        // Fling upwards
+        val flingUp = Velocity(0f, -100f)
+
+        rule.runOnIdle {
+            val preConsumed = runBlocking { dispatcher.dispatchPreFling(flingUp) }
+            // Pull refresh is not showing, so we should consume nothing
+            assertThat(preConsumed).isEqualTo(Velocity.Zero)
+            // Not past the threshold, so we shouldn't have called onRefresh
+            assertThat(onRefreshCalled).isFalse()
+        }
+
+        rule.runOnIdle {
+            assertThat(state.position).isEqualTo(0f)
+        }
+
+        // Pull the state but not past the threshold
+        state.onPull(refreshThreshold / 2f)
+
+        rule.runOnIdle {
+            assertThat(state.position)
+                .isEqualTo(refreshThreshold / 4f /* account for drag multiplier */)
+            val preConsumed = runBlocking { dispatcher.dispatchPreFling(flingUp) }
+            // Upwards fling, so we should consume nothing
+            assertThat(preConsumed).isEqualTo(Velocity.Zero)
+            // Not past the threshold, so we shouldn't have called onRefresh
+            assertThat(onRefreshCalled).isFalse()
+        }
+
+        rule.runOnIdle {
+            // Indicator should be reset
+            assertThat(state.position).isEqualTo(0f)
+        }
+
+        // Pull the state past the threshold
+        state.onPull(refreshThreshold * 3f)
+
+        rule.runOnIdle {
+            assertThat(state.position)
+                .isEqualTo(calculateIndicatorPosition(
+                    refreshThreshold * (3 / 2f) /* account for drag multiplier */,
+                    refreshThreshold
+                ))
+            val preConsumed = runBlocking { dispatcher.dispatchPreFling(flingUp) }
+            // Upwards fling, so we should consume nothing
+            assertThat(preConsumed).isEqualTo(Velocity.Zero)
+            // Past the threshold, so we should call onRefresh
+            assertThat(onRefreshCalled).isTrue()
+        }
+
+        rule.runOnIdle {
+            // Indicator should be reset since we never changed refreshing state
+            assertThat(state.position).isEqualTo(0f)
+        }
+    }
+
+    @Test
+    fun nestedPreFling_negativeVelocity_refreshing() {
+        val refreshingOffset = 500f
+        lateinit var state: PullRefreshState
+
+        val dispatcher = NestedScrollDispatcher()
+        val connection = object : NestedScrollConnection {}
+
+        rule.setContent {
+            state = rememberPullRefreshState(
+                refreshing = true,
+                onRefresh = {},
+                refreshingOffset = with(LocalDensity.current) { refreshingOffset.toDp() }
+            )
+            Box(Modifier.size(200.dp).pullRefresh(state)) {
+                Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+            }
+        }
+
+        // Fling upwards
+        val flingUp = Velocity(0f, -100f)
+
+        rule.runOnIdle {
+            assertThat(state.position).isEqualTo(refreshingOffset)
+            val preConsumed = runBlocking { dispatcher.dispatchPreFling(flingUp) }
+            // Currently refreshing, so we should consume nothing
+            assertThat(preConsumed).isEqualTo(Velocity.Zero)
+        }
+
+        rule.runOnIdle {
+            // Shouldn't change position since we are refreshing
+            assertThat(state.position).isEqualTo(refreshingOffset)
+        }
+    }
+
+    @Test
+    fun nestedPreFling_positiveVelocity_notRefreshing() {
+        val refreshThreshold = 200f
+        lateinit var state: PullRefreshState
+        var onRefreshCalled = false
+
+        val dispatcher = NestedScrollDispatcher()
+        val connection = object : NestedScrollConnection {}
+
+        rule.setContent {
+            state = rememberPullRefreshState(
+                refreshing = false,
+                onRefresh = { onRefreshCalled = true },
+                refreshThreshold = with(LocalDensity.current) { refreshThreshold.toDp() }
+            )
+            Box(Modifier.size(200.dp).pullRefresh(state)) {
+                Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+            }
+        }
+
+        // Fling downwards
+        val flingDown = Velocity(0f, 100f)
+
+        rule.runOnIdle {
+            val preConsumed = runBlocking { dispatcher.dispatchPreFling(flingDown) }
+            // Pull refresh is not showing, so we should consume nothing
+            assertThat(preConsumed).isEqualTo(Velocity.Zero)
+            // Not past the threshold, so we shouldn't have called onRefresh
+            assertThat(onRefreshCalled).isFalse()
+        }
+
+        rule.runOnIdle {
+            assertThat(state.position).isEqualTo(0f)
+        }
+
+        // Pull the state but not past the threshold
+        state.onPull(refreshThreshold / 2f)
+
+        rule.runOnIdle {
+            assertThat(state.position)
+                .isEqualTo(refreshThreshold / 4f /* account for drag multiplier */)
+            val preConsumed = runBlocking { dispatcher.dispatchPreFling(flingDown) }
+            // Downwards fling, and we are currently showing, so we should consume all
+            assertThat(preConsumed).isEqualTo(flingDown)
+            // Not past the threshold, so we shouldn't have called onRefresh
+            assertThat(onRefreshCalled).isFalse()
+        }
+
+        rule.runOnIdle {
+            // Indicator should be reset
+            assertThat(state.position).isEqualTo(0f)
+        }
+
+        // Pull the state past the threshold
+        state.onPull(refreshThreshold * 3f)
+
+        rule.runOnIdle {
+            assertThat(state.position)
+                .isEqualTo(calculateIndicatorPosition(
+                    refreshThreshold * (3 / 2f) /* account for drag multiplier */,
+                    refreshThreshold
+                ))
+            val preConsumed = runBlocking { dispatcher.dispatchPreFling(flingDown) }
+            // Downwards fling, and we are currently showing, so we should consume all
+            assertThat(preConsumed).isEqualTo(flingDown)
+            // Past the threshold, so we should call onRefresh
+            assertThat(onRefreshCalled).isTrue()
+        }
+
+        rule.runOnIdle {
+            // Indicator should be reset since we never changed refreshing state
+            assertThat(state.position).isEqualTo(0f)
+        }
+    }
+
+    @Test
+    fun nestedPreFling_positiveVelocity_refreshing() {
+        val refreshingOffset = 500f
+        lateinit var state: PullRefreshState
+
+        val dispatcher = NestedScrollDispatcher()
+        val connection = object : NestedScrollConnection {}
+
+        rule.setContent {
+            state = rememberPullRefreshState(
+                refreshing = true,
+                onRefresh = {},
+                refreshingOffset = with(LocalDensity.current) { refreshingOffset.toDp() }
+            )
+            Box(Modifier.size(200.dp).pullRefresh(state)) {
+                Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+            }
+        }
+
+        // Fling downwards
+        val flingUp = Velocity(0f, 100f)
+
+        rule.runOnIdle {
+            assertThat(state.position).isEqualTo(refreshingOffset)
+            val preConsumed = runBlocking { dispatcher.dispatchPreFling(flingUp) }
+            // Currently refreshing, so we should consume nothing
+            assertThat(preConsumed).isEqualTo(Velocity.Zero)
+        }
+
+        rule.runOnIdle {
+            // Shouldn't change position since we are refreshing
+            assertThat(state.position).isEqualTo(refreshingOffset)
+        }
+    }
+
+    @Test
+    fun nestedPostFling_noop() {
+        val refreshThreshold = 200f
+        lateinit var state: PullRefreshState
+        var onRefreshCalled = false
+
+        val dispatcher = NestedScrollDispatcher()
+        val connection = object : NestedScrollConnection {}
+
+        rule.setContent {
+            state = rememberPullRefreshState(
+                refreshing = false,
+                onRefresh = { onRefreshCalled = true },
+                refreshThreshold = with(LocalDensity.current) { refreshThreshold.toDp() }
+            )
+            Box(Modifier.size(200.dp).pullRefresh(state)) {
+                Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+            }
+        }
+
+        // Fling upwards
+        val flingUp = Velocity(0f, 100f)
+        // Fling downwards
+        val flingDown = Velocity(0f, 100f)
+
+        rule.runOnIdle {
+            val postConsumedUp = runBlocking {
+                dispatcher.dispatchPostFling(Velocity.Zero, flingUp)
+            }
+            // Noop
+            assertThat(postConsumedUp).isEqualTo(Velocity.Zero)
+            val postConsumedDown = runBlocking {
+                dispatcher.dispatchPostFling(Velocity.Zero, flingDown)
+            }
+            // Noop
+            assertThat(postConsumedDown).isEqualTo(Velocity.Zero)
+            // Noop
+            assertThat(onRefreshCalled).isFalse()
+        }
+
+        rule.runOnIdle {
+            assertThat(state.position).isEqualTo(0f)
+        }
+
+        // Pull the state but not past the threshold
+        state.onPull(refreshThreshold / 2f)
+
+        rule.runOnIdle {
+            assertThat(state.position)
+                .isEqualTo(refreshThreshold / 4f /* account for drag multiplier */)
+            val postConsumedUp = runBlocking {
+                dispatcher.dispatchPostFling(Velocity.Zero, flingUp)
+            }
+            // Noop
+            assertThat(postConsumedUp).isEqualTo(Velocity.Zero)
+            val postConsumedDown = runBlocking {
+                dispatcher.dispatchPostFling(Velocity.Zero, flingDown)
+            }
+            // Noop
+            assertThat(postConsumedDown).isEqualTo(Velocity.Zero)
+            // Noop
+            assertThat(onRefreshCalled).isFalse()
+        }
+
+        rule.runOnIdle {
+            // Position should stay the same
+            assertThat(state.position)
+                .isEqualTo(refreshThreshold / 4f /* account for drag multiplier */)
+        }
+
+        // Pull the state past the threshold (we have already pulled half of this, so this is now
+        // 1.5 x refreshThreshold for the pull)
+        state.onPull(refreshThreshold)
+
+        rule.runOnIdle {
+            assertThat(state.position)
+                .isEqualTo((refreshThreshold * (3 / 2f)) / 2f /* account for drag multiplier */)
+            val postConsumedUp = runBlocking {
+                dispatcher.dispatchPostFling(Velocity.Zero, flingUp)
+            }
+            // Noop
+            assertThat(postConsumedUp).isEqualTo(Velocity.Zero)
+            val postConsumedDown = runBlocking {
+                dispatcher.dispatchPostFling(Velocity.Zero, flingDown)
+            }
+            // Noop
+            assertThat(postConsumedDown).isEqualTo(Velocity.Zero)
+            // Noop
+            assertThat(onRefreshCalled).isFalse()
+        }
+
+        rule.runOnIdle {
+            // Position should be unchanged
+            assertThat(state.position)
+                .isEqualTo(refreshThreshold * (3 / 2f) / 2f /* account for drag multiplier */)
+        }
+    }
+
     /**
      * Taken from the private function of the same name in [PullRefreshState].
      */
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableBox.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableBox.kt
index 73e69950..6ea5bee 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableBox.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableBox.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalMaterialApi::class)
+
 package androidx.compose.material.swipeable
 
 import androidx.compose.foundation.background
@@ -35,7 +37,6 @@
 import androidx.compose.ui.unit.dp
 import kotlin.math.roundToInt
 
-@OptIn(ExperimentalMaterialApi::class)
 @Composable
 internal fun SwipeableBox(
     swipeableState: SwipeableV2State<TestState>,
@@ -47,7 +48,28 @@
     ),
     enabled: Boolean = true,
     reverseDirection: Boolean = false,
-    calculateAnchor: (state: TestState, layoutSize: IntSize) -> Float? = { state, layoutSize ->
+    anchors: Map<TestState, Float>
+) = SwipeableBox(
+    swipeableState = swipeableState,
+    orientation = orientation,
+    possibleStates = possibleStates,
+    enabled = enabled,
+    reverseDirection = reverseDirection,
+    calculateAnchor = { anchor, _ -> anchors[anchor] }
+)
+
+@Composable
+internal fun SwipeableBox(
+    swipeableState: SwipeableV2State<TestState>,
+    orientation: Orientation = Orientation.Horizontal,
+    possibleStates: Set<TestState> = setOf(
+        TestState.A,
+        TestState.B,
+        TestState.C
+    ),
+    enabled: Boolean = true,
+    reverseDirection: Boolean = false,
+    calculateAnchor: (anchor: TestState, layoutSize: IntSize) -> Float? = { state, layoutSize ->
         val size = (
             if (orientation == Orientation.Horizontal) layoutSize.width else layoutSize.height
             ).toFloat()
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt
index 3615573..7840ba1 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt
@@ -29,10 +29,11 @@
 import androidx.compose.material.swipeable.TestState.A
 import androidx.compose.material.swipeable.TestState.B
 import androidx.compose.material.swipeable.TestState.C
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.testutils.WithTouchSlop
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.test.TouchInjectionScope
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performTouchInput
@@ -64,104 +65,174 @@
     val rule = createComposeRule()
 
     @Test
-    fun swipeable_swipe_horizontal() = directionalSwipeTest(
-        SwipeableTestState(initialState = A),
-        orientation = Orientation.Horizontal,
-        verify = verifyOffsetMatchesAnchor()
-    )
+    fun swipeable_swipe_horizontal() {
+        val state = SwipeableTestState(initialState = A)
+        val anchors = mapOf(
+            A to 0f,
+            B to 250f,
+            C to 500f
+        )
 
-    @Test
-    fun swipeable_swipe_vertical() = directionalSwipeTest(
-        SwipeableTestState(initialState = A),
-        orientation = Orientation.Vertical,
-        verify = verifyOffsetMatchesAnchor()
-    )
-
-    @Test
-    fun swipeable_swipe_disabled_horizontal() = directionalSwipeTest(
-        SwipeableTestState(initialState = A),
-        orientation = Orientation.Horizontal,
-        enabled = false,
-        verify = verifyOffset0(),
-    )
-
-    @Test
-    fun swipeable_swipe_disabled_vertical(): Unit = directionalSwipeTest(
-        SwipeableTestState(initialState = A),
-        orientation = Orientation.Vertical,
-        enabled = false,
-        verify = verifyOffset0(),
-    )
-
-    @Test
-    fun swipeable_swipe_reverse_direction_horizontal() = directionalSwipeTest(
-        SwipeableTestState(initialState = A),
-        orientation = Orientation.Horizontal,
-        verify = verifyOffsetMatchesAnchor()
-    )
-
-    private fun verifyOffset0() = { state: SwipeableV2State<TestState>, _: TestState ->
-        assertThat(state.offset).isEqualTo(0f)
-    }
-
-    private fun verifyOffsetMatchesAnchor() =
-        { state: SwipeableV2State<TestState>, target: TestState ->
-            val swipeableSizePx = with(rule.density) { swipeableSize.roundToPx() }
-            val targetOffset = when (target) {
-                A -> 0f
-                B -> swipeableSizePx / 2
-                C -> swipeableSizePx
-            }
-            assertThat(state.offset).isEqualTo(targetOffset)
-        }
-
-    private fun directionalSwipeTest(
-        state: SwipeableV2State<TestState>,
-        orientation: Orientation,
-        enabled: Boolean = true,
-        reverseDirection: Boolean = false,
-        swipeForward: TouchInjectionScope.(end: Float) -> Unit = {
-            if (orientation == Orientation.Horizontal) swipeRight(endX = it)
-            else swipeDown(endY = it)
-        },
-        swipeBackward: TouchInjectionScope.(end: Float) -> Unit = {
-            if (orientation == Orientation.Horizontal) swipeLeft(endX = it) else swipeUp(endY = it)
-        },
-        verify: (state: SwipeableV2State<TestState>, target: TestState) -> Unit,
-    ) {
         rule.setContent {
-            SwipeableBox(state, orientation, enabled = enabled, reverseDirection = reverseDirection)
+            CompositionLocalProvider(LocalDensity provides NoOpDensity) {
+                WithTouchSlop(0f) {
+                    SwipeableBox(
+                        swipeableState = state,
+                        orientation = Orientation.Horizontal,
+                        anchors = anchors
+                    )
+                }
+            }
         }
 
-        rule.onNodeWithTag(swipeableTestTag)
-            .performTouchInput { swipeForward(endEdge(orientation) / 2) }
-        rule.waitForIdle()
-        verify(state, B)
+        assertThat(state.currentValue).isEqualTo(A)
 
         rule.onNodeWithTag(swipeableTestTag)
-            .performTouchInput { swipeForward(endEdge(orientation)) }
+            .performTouchInput { swipeRight(endX = right / 2) }
         rule.waitForIdle()
-        verify(state, C)
+
+        assertThat(state.currentValue).isEqualTo(B)
+        assertThat(state.offset).isEqualTo(anchors.getValue(B))
 
         rule.onNodeWithTag(swipeableTestTag)
-            .performTouchInput { swipeBackward(endEdge(orientation) / 2) }
+            .performTouchInput { swipeRight(startX = right / 2, endX = right) }
         rule.waitForIdle()
-        verify(state, B)
+        assertThat(state.currentValue).isEqualTo(C)
+        assertThat(state.offset).isEqualTo(anchors.getValue(C))
 
         rule.onNodeWithTag(swipeableTestTag)
-            .performTouchInput { swipeBackward(startEdge(orientation)) }
+            .performTouchInput { swipeLeft(endX = right / 2) }
         rule.waitForIdle()
-        verify(state, A)
+
+        assertThat(state.currentValue).isEqualTo(B)
+        assertThat(state.offset).isEqualTo(anchors.getValue(B))
+
+        rule.onNodeWithTag(swipeableTestTag)
+            .performTouchInput { swipeLeft(startX = right / 2) }
+        rule.waitForIdle()
+
+        assertThat(state.currentValue).isEqualTo(A)
+        assertThat(state.offset).isEqualTo(anchors.getValue(A))
     }
 
     @Test
-    fun swipeable_positionalThresholds_fractional_targetState() = fractionalThresholdsTest(0.5f)
+    fun swipeable_swipe_vertical() {
+        val state = SwipeableTestState(initialState = A)
+        val anchors = mapOf(
+            A to 0f,
+            B to 250f,
+            C to 500f
+        )
+
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides NoOpDensity) {
+                WithTouchSlop(0f) {
+                    SwipeableBox(
+                        swipeableState = state,
+                        orientation = Orientation.Vertical,
+                        anchors = anchors
+                    )
+                }
+            }
+        }
+
+        assertThat(state.currentValue).isEqualTo(A)
+
+        rule.onNodeWithTag(swipeableTestTag)
+            .performTouchInput { swipeDown(startY = top, endY = bottom / 2) }
+        rule.waitForIdle()
+
+        assertThat(state.currentValue).isEqualTo(B)
+        assertThat(state.offset).isEqualTo(anchors.getValue(B))
+
+        rule.onNodeWithTag(swipeableTestTag)
+            .performTouchInput { swipeDown(startY = bottom / 2, endY = bottom) }
+        rule.waitForIdle()
+        assertThat(state.currentValue).isEqualTo(C)
+        assertThat(state.offset).isEqualTo(anchors.getValue(C))
+
+        rule.onNodeWithTag(swipeableTestTag)
+            .performTouchInput { swipeUp(startY = bottom, endY = bottom / 2) }
+        rule.waitForIdle()
+
+        assertThat(state.currentValue).isEqualTo(B)
+        assertThat(state.offset).isEqualTo(anchors.getValue(B))
+
+        rule.onNodeWithTag(swipeableTestTag)
+            .performTouchInput { swipeUp(startY = bottom / 2, endY = top) }
+        rule.waitForIdle()
+
+        assertThat(state.currentValue).isEqualTo(A)
+        assertThat(state.offset).isEqualTo(anchors.getValue(A))
+    }
 
     @Test
-    fun swipeable_positionalThresholds_fractional_negativeThreshold_targetState() =
-        fractionalThresholdsTest(-0.5f)
+    fun swipeable_swipe_disabled_horizontal() {
+        val state = SwipeableTestState(initialState = A)
+        val anchors = mapOf(
+            A to 0f,
+            B to 250f,
+            C to 500f
+        )
 
-    private fun fractionalThresholdsTest(positionalThreshold: Float) {
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides NoOpDensity) {
+                WithTouchSlop(0f) {
+                    SwipeableBox(
+                        swipeableState = state,
+                        orientation = Orientation.Horizontal,
+                        anchors = anchors,
+                        enabled = false
+                    )
+                }
+            }
+        }
+
+        assertThat(state.currentValue).isEqualTo(A)
+
+        rule.onNodeWithTag(swipeableTestTag)
+            .performTouchInput { swipeRight(startX = left, endX = right) }
+        rule.waitForIdle()
+
+        assertThat(state.currentValue).isEqualTo(A)
+        assertThat(state.offset).isZero()
+    }
+
+    @Test
+    fun swipeable_swipe_disabled_vertical() {
+        val state = SwipeableTestState(initialState = A)
+        val anchors = mapOf(
+            A to 0f,
+            B to 250f,
+            C to 500f
+        )
+
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides NoOpDensity) {
+                WithTouchSlop(0f) {
+                    SwipeableBox(
+                        swipeableState = state,
+                        orientation = Orientation.Vertical,
+                        anchors = anchors,
+                        enabled = false
+                    )
+                }
+            }
+        }
+
+        assertThat(state.currentValue).isEqualTo(A)
+
+        rule.onNodeWithTag(swipeableTestTag)
+            .performTouchInput { swipeDown(startY = top, endY = bottom) }
+        rule.waitForIdle()
+
+        assertThat(state.currentValue).isEqualTo(A)
+        assertThat(state.offset).isZero()
+    }
+
+    @Test
+    fun swipeable_positionalThresholds_fractional_targetState() {
+        val positionalThreshold = 0.5f
         val absThreshold = abs(positionalThreshold)
         val state = SwipeableTestState(
             initialState = A,
@@ -185,6 +256,7 @@
         assertThat(state.targetValue).isEqualTo(B)
 
         runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
+        rule.waitForIdle()
 
         assertThat(state.currentValue).isEqualTo(B)
         assertThat(state.targetValue).isEqualTo(B)
@@ -202,19 +274,65 @@
         assertThat(state.targetValue).isEqualTo(A)
 
         runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
+        rule.waitForIdle()
 
         assertThat(state.currentValue).isEqualTo(A)
         assertThat(state.targetValue).isEqualTo(A)
     }
 
     @Test
-    fun swipeable_positionalThresholds_fixed_targetState() = fixedThresholdsTest(56.dp)
+    fun swipeable_positionalThresholds_fractional_negativeThreshold_targetState() {
+        val positionalThreshold = -0.5f
+        val absThreshold = abs(positionalThreshold)
+        val state = SwipeableTestState(
+            initialState = A,
+            positionalThreshold = fractionalPositionalThreshold(positionalThreshold)
+        )
+        rule.setContent { SwipeableBox(state) }
+
+        val positionOfA = state.anchors.getValue(A)
+        val positionOfB = state.anchors.getValue(B)
+        val distance = abs(positionOfA - positionOfB)
+        state.dispatchRawDelta(positionOfA + distance * (absThreshold * 0.9f))
+        rule.waitForIdle()
+
+        assertThat(state.currentValue).isEqualTo(A)
+        assertThat(state.targetValue).isEqualTo(A)
+
+        state.dispatchRawDelta(distance * 0.2f)
+        rule.waitForIdle()
+
+        assertThat(state.currentValue).isEqualTo(A)
+        assertThat(state.targetValue).isEqualTo(B)
+
+        runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
+        rule.waitForIdle()
+
+        assertThat(state.currentValue).isEqualTo(B)
+        assertThat(state.targetValue).isEqualTo(B)
+
+        state.dispatchRawDelta(-distance * (absThreshold * 0.9f))
+        rule.waitForIdle()
+
+        assertThat(state.currentValue).isEqualTo(B)
+        assertThat(state.targetValue).isEqualTo(B)
+
+        state.dispatchRawDelta(-distance * 0.2f)
+        rule.waitForIdle()
+
+        assertThat(state.currentValue).isEqualTo(B)
+        assertThat(state.targetValue).isEqualTo(A)
+
+        runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
+        rule.waitForIdle()
+
+        assertThat(state.currentValue).isEqualTo(A)
+        assertThat(state.targetValue).isEqualTo(A)
+    }
 
     @Test
-    fun swipeable_positionalThresholds_fixed_negativeThreshold_targetState() =
-        fixedThresholdsTest((-56).dp)
-
-    private fun fixedThresholdsTest(positionalThreshold: Dp) {
+    fun swipeable_positionalThresholds_fixed_targetState() {
+        val positionalThreshold = 56.dp
         val absThreshold = with(rule.density) { abs(positionalThreshold.toPx()) }
         val state = SwipeableTestState(
             initialState = A,
@@ -239,6 +357,7 @@
         assertThat(state.targetValue).isEqualTo(B)
 
         runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
+        rule.waitForIdle()
 
         assertThat(state.currentValue).isEqualTo(B)
         assertThat(state.targetValue).isEqualTo(B)
@@ -258,6 +377,60 @@
         assertThat(state.targetValue).isEqualTo(A)
 
         runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
+        rule.waitForIdle()
+
+        assertThat(state.currentValue).isEqualTo(A)
+        assertThat(state.targetValue).isEqualTo(A)
+    }
+
+    @Test
+    fun swipeable_positionalThresholds_fixed_negativeThreshold_targetState() {
+        val positionalThreshold = (-56).dp
+        val absThreshold = with(rule.density) { abs(positionalThreshold.toPx()) }
+        val state = SwipeableTestState(
+            initialState = A,
+            positionalThreshold = fixedPositionalThreshold(positionalThreshold)
+        )
+        rule.setContent { SwipeableBox(state) }
+
+        val initialOffset = state.requireOffset()
+
+        // Swipe towards B, close before threshold
+        state.dispatchRawDelta(initialOffset + (absThreshold * 0.9f))
+        rule.waitForIdle()
+
+        assertThat(state.currentValue).isEqualTo(A)
+        assertThat(state.targetValue).isEqualTo(A)
+
+        // Swipe towards B, close after threshold
+        state.dispatchRawDelta(absThreshold * 0.2f)
+        rule.waitForIdle()
+
+        assertThat(state.currentValue).isEqualTo(A)
+        assertThat(state.targetValue).isEqualTo(B)
+
+        runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
+        rule.waitForIdle()
+
+        assertThat(state.currentValue).isEqualTo(B)
+        assertThat(state.targetValue).isEqualTo(B)
+
+        // Swipe towards A, close before threshold
+        state.dispatchRawDelta(-(absThreshold * 0.9f))
+        rule.waitForIdle()
+
+        assertThat(state.currentValue).isEqualTo(B)
+        assertThat(state.targetValue).isEqualTo(B)
+
+        // Swipe towards A, close after threshold
+        state.dispatchRawDelta(-(absThreshold * 0.2f))
+        rule.waitForIdle()
+
+        assertThat(state.currentValue).isEqualTo(B)
+        assertThat(state.targetValue).isEqualTo(A)
+
+        runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
+        rule.waitForIdle()
 
         assertThat(state.currentValue).isEqualTo(A)
         assertThat(state.targetValue).isEqualTo(A)
@@ -268,19 +441,18 @@
         runBlocking(AutoTestFrameClock()) {
             val velocity = 100.dp
             val velocityPx = with(rule.density) { velocity.toPx() }
-            val state = with(rule) {
-                SwipeableTestState(
-                    initialState = A,
-                    anchors = mapOf(
-                        A to 0f,
-                        B to 100f,
-                        C to 200f
-                    ),
-                    velocityThreshold = velocity / 2
-                )
-            }
+            val state = SwipeableTestState(
+                initialState = A,
+                anchors = mapOf(
+                    A to 0f,
+                    B to 100f,
+                    C to 200f
+                ),
+                velocityThreshold = velocity / 2
+            )
             state.dispatchRawDelta(60f)
             state.settle(velocityPx)
+            rule.waitForIdle()
             assertThat(state.currentValue).isEqualTo(B)
         }
 
@@ -451,10 +623,9 @@
         if (anchors != null) updateAnchors(anchors)
         this.density = density
     }
+}
 
-    private fun TouchInjectionScope.endEdge(orientation: Orientation) =
-        if (orientation == Orientation.Horizontal) right else bottom
-
-    private fun TouchInjectionScope.startEdge(orientation: Orientation) =
-        if (orientation == Orientation.Horizontal) left else top
-}
\ No newline at end of file
+private val NoOpDensity = object : Density {
+    override val density = 1f
+    override val fontScale = 1f
+}
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/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
index d15e372..5ea9086 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
@@ -499,9 +499,9 @@
                     when (state) {
                         Hidden -> fullHeight
                         HalfExpanded -> when {
-                            sheetSize.height < fullHeight / 2 -> null
+                            sheetSize.height < fullHeight / 2f -> null
                             sheetState.isSkipHalfExpanded -> null
-                            else -> sheetSize.height / 2f
+                            else -> fullHeight / 2f
                         }
 
                         Expanded -> if (sheetSize.height != 0) {
@@ -656,7 +656,7 @@
     animateTo: (target: ModalBottomSheetValue, velocity: Float) -> Unit,
     snapTo: (target: ModalBottomSheetValue) -> Unit,
 ) = AnchorChangeHandler<ModalBottomSheetValue> { previousTarget, previousAnchors, newAnchors ->
-    val previousTargetOffset = previousAnchors.getValue(previousTarget)
+    val previousTargetOffset = previousAnchors[previousTarget]
     val newTarget = when (previousTarget) {
         Hidden -> Hidden
         HalfExpanded, Expanded -> {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefresh.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefresh.kt
index 8a94354..564d874 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefresh.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefresh.kt
@@ -49,7 +49,7 @@
     properties["state"] = state
     properties["enabled"] = enabled
 }) {
-    Modifier.pullRefresh(state::onPull, { state.onRelease() }, enabled)
+    Modifier.pullRefresh(state::onPull, state::onRelease, enabled)
 }
 
 /**
@@ -64,16 +64,20 @@
  * @param onPull Callback for dispatching vertical scroll delta, takes float pullDelta as argument.
  * Positive delta (pulling down) is dispatched only if the child does not consume it (i.e. pulling
  * down despite being at the top of a scrollable component), whereas negative delta (swiping up) is
- * dispatched first (in case it is needed to push the indicator back up), and then whatever is not
- * consumed is passed on to the child.
+ * dispatched first (in case it is needed to push the indicator back up), and then the unconsumed
+ * delta is passed on to the child. The callback returns how much delta was consumed.
  * @param onRelease Callback for when drag is released, takes float flingVelocity as argument.
+ * The callback returns how much velocity was consumed - in most cases this should only consume
+ * velocity if pull refresh has been dragged already and the velocity is positive (the fling is
+ * downwards), as an upwards fling should typically still scroll a scrollable component beneath the
+ * pullRefresh. This is invoked before any remaining velocity is passed to the child.
  * @param enabled If not enabled, all scroll delta and fling velocity will be ignored and neither
  * [onPull] nor [onRelease] will be invoked.
  */
 @ExperimentalMaterialApi
 fun Modifier.pullRefresh(
     onPull: (pullDelta: Float) -> Float,
-    onRelease: suspend (flingVelocity: Float) -> Unit,
+    onRelease: suspend (flingVelocity: Float) -> Float,
     enabled: Boolean = true
 ) = inspectable(inspectorInfo = debugInspectorInfo {
     name = "pullRefresh"
@@ -86,7 +90,7 @@
 
 private class PullRefreshNestedScrollConnection(
     private val onPull: (pullDelta: Float) -> Float,
-    private val onRelease: suspend (flingVelocity: Float) -> Unit,
+    private val onRelease: suspend (flingVelocity: Float) -> Float,
     private val enabled: Boolean
 ) : NestedScrollConnection {
 
@@ -110,7 +114,6 @@
     }
 
     override suspend fun onPreFling(available: Velocity): Velocity {
-        onRelease(available.y)
-        return Velocity.Zero
+        return Velocity(0f, onRelease(available.y))
     }
 }
\ No newline at end of file
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt
index 8249ba1..96983f1 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt
@@ -136,14 +136,25 @@
         return dragConsumed
     }
 
-    internal fun onRelease() {
-        if (!_refreshing) {
-            if (adjustedDistancePulled > threshold) {
-                onRefreshState.value()
-            }
-            animateIndicatorTo(0f)
+    internal fun onRelease(velocity: Float): Float {
+        if (refreshing) return 0f // Already refreshing, do nothing
+
+        if (adjustedDistancePulled > threshold) {
+            onRefreshState.value()
+        }
+        animateIndicatorTo(0f)
+        val consumed = when {
+            // We are flinging without having dragged the pull refresh (for example a fling inside
+            // a list) - don't consume
+            distancePulled == 0f -> 0f
+            // If the velocity is negative, the fling is upwards, and we don't want to prevent the
+            // the list from scrolling
+            velocity < 0f -> 0f
+            // We are showing the indicator, and the fling is downwards - consume everything
+            else -> velocity
         }
         distancePulled = 0f
+        return consumed
     }
 
     internal fun setRefreshing(refreshing: Boolean) {
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/current.txt b/compose/material3/material3/api/current.txt
index e3bd3b6..5508abe 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -232,12 +232,18 @@
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
   }
 
+  public final class DateInputKt {
+  }
+
   public final class DatePickerDialog_androidKt {
   }
 
   public final class DatePickerKt {
   }
 
+  public final class DateRangePickerKt {
+  }
+
   public final class DividerDefaults {
     method @androidx.compose.runtime.Composable public long getColor();
     method public float getThickness();
@@ -394,7 +400,24 @@
     method public static androidx.compose.ui.Modifier minimumInteractiveComponentSize(androidx.compose.ui.Modifier);
   }
 
+  @androidx.compose.runtime.Immutable public final class ListItemColors {
+  }
+
+  public final class ListItemDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.ListItemColors colors(optional long containerColor, optional long headlineColor, optional long leadingIconColor, optional long overlineColor, optional long supportingColor, optional long trailingIconColor, optional long disabledHeadlineColor, optional long disabledLeadingIconColor, optional long disabledTrailingIconColor);
+    method @androidx.compose.runtime.Composable public long getContainerColor();
+    method @androidx.compose.runtime.Composable public long getContentColor();
+    method public float getElevation();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
+    property public final float Elevation;
+    property @androidx.compose.runtime.Composable public final long containerColor;
+    property @androidx.compose.runtime.Composable public final long contentColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+    field public static final androidx.compose.material3.ListItemDefaults INSTANCE;
+  }
+
   public final class ListItemKt {
+    method @androidx.compose.runtime.Composable public static void ListItem(kotlin.jvm.functions.Function0<kotlin.Unit> headlineText, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? overlineText, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional androidx.compose.material3.ListItemColors colors, optional float tonalElevation, optional float shadowElevation);
   }
 
   public final class MaterialTheme {
@@ -424,6 +447,9 @@
   public final class MenuKt {
   }
 
+  public final class ModalBottomSheetKt {
+  }
+
   public final class NavigationBarDefaults {
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method public float getElevation();
@@ -503,7 +529,7 @@
     method public int getCircularDeterminateStrokeCap();
     method public int getCircularIndeterminateStrokeCap();
     method public float getCircularStrokeWidth();
-    method public long getCircularTrackColor();
+    method @androidx.compose.runtime.Composable public long getCircularTrackColor();
     method @androidx.compose.runtime.Composable public long getLinearColor();
     method public int getLinearStrokeCap();
     method @androidx.compose.runtime.Composable public long getLinearTrackColor();
@@ -514,7 +540,7 @@
     property public final int LinearStrokeCap;
     property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> ProgressAnimationSpec;
     property @androidx.compose.runtime.Composable public final long circularColor;
-    property public final long circularTrackColor;
+    property @androidx.compose.runtime.Composable public final long circularTrackColor;
     property @androidx.compose.runtime.Composable public final long linearColor;
     property @androidx.compose.runtime.Composable public final long linearTrackColor;
     field public static final androidx.compose.material3.ProgressIndicatorDefaults INSTANCE;
@@ -597,16 +623,31 @@
   public final class ShapesKt {
   }
 
+  public final class SheetDefaultsKt {
+  }
+
   @androidx.compose.runtime.Immutable public final class SliderColors {
   }
 
   @androidx.compose.runtime.Stable public final class SliderDefaults {
+    method @androidx.compose.runtime.Composable public void Thumb(androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled, optional long thumbSize);
+    method @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderPositions sliderPositions, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.SliderColors colors(optional long thumbColor, optional long activeTrackColor, optional long activeTickColor, optional long inactiveTrackColor, optional long inactiveTickColor, optional long disabledThumbColor, optional long disabledActiveTrackColor, optional long disabledActiveTickColor, optional long disabledInactiveTrackColor, optional long disabledInactiveTickColor);
     field public static final androidx.compose.material3.SliderDefaults INSTANCE;
   }
 
   public final class SliderKt {
-    method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+    method @androidx.compose.runtime.Composable public static void RangeSlider(kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> value, kotlin.jvm.functions.Function1<? super kotlin.ranges.ClosedFloatingPointRange<java.lang.Float>,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource startInteractionSource, optional androidx.compose.foundation.interaction.MutableInteractionSource endInteractionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> startThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> endThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> track, optional int steps);
+    method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> thumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> track, optional int steps);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+  }
+
+  @androidx.compose.runtime.Stable public final class SliderPositions {
+    ctor public SliderPositions(optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> initialActiveRange, optional float[] initialTickFractions);
+    method public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getActiveRange();
+    method public float[] getTickFractions();
+    property public final kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> activeRange;
+    property public final float[] tickFractions;
   }
 
   @androidx.compose.runtime.Stable public interface SnackbarData {
@@ -761,6 +802,28 @@
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
   }
 
+  public final class TimeFormat_androidKt {
+  }
+
+  public final class TimePickerKt {
+  }
+
+  @androidx.compose.runtime.Stable public final class TimePickerState {
+    ctor public TimePickerState(int initialHour, int initialMinute, boolean is24Hour);
+    method public int getHour();
+    method public int getMinute();
+    method public boolean is24hour();
+    method public suspend Object? settle(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public final int hour;
+    property public final boolean is24hour;
+    property public final int minute;
+    field public static final androidx.compose.material3.TimePickerState.Companion Companion;
+  }
+
+  public static final class TimePickerState.Companion {
+    method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.TimePickerState,?> Saver();
+  }
+
   public final class TonalPaletteKt {
   }
 
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index d102ce1..ad3d5cf 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -81,6 +81,21 @@
     field public static final androidx.compose.material3.BottomAppBarDefaults INSTANCE;
   }
 
+  @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class BottomSheetDefaults {
+    method @androidx.compose.runtime.Composable public void DragHandle(optional androidx.compose.ui.Modifier modifier, optional float width, optional float height, optional androidx.compose.ui.graphics.Shape shape, optional long color);
+    method @androidx.compose.runtime.Composable public long getContainerColor();
+    method public float getElevation();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getExpandedShape();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getMinimizedShape();
+    method @androidx.compose.runtime.Composable public long getScrimColor();
+    property @androidx.compose.runtime.Composable public final long ContainerColor;
+    property public final float Elevation;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ExpandedShape;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape MinimizedShape;
+    property @androidx.compose.runtime.Composable public final long ScrimColor;
+    field public static final androidx.compose.material3.BottomSheetDefaults INSTANCE;
+  }
+
   @androidx.compose.runtime.Immutable public final class ButtonColors {
   }
 
@@ -281,12 +296,15 @@
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
   }
 
+  public final class DateInputKt {
+  }
+
   @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class DatePickerColors {
   }
 
-  @androidx.compose.material3.ExperimentalMaterial3Api public final class DatePickerDefaults {
+  @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class DatePickerDefaults {
     method @androidx.compose.runtime.Composable public void DatePickerHeadline(androidx.compose.material3.DatePickerState state, androidx.compose.material3.DatePickerFormatter dateFormatter);
-    method @androidx.compose.runtime.Composable public void DatePickerTitle();
+    method @androidx.compose.runtime.Composable public void DatePickerTitle(androidx.compose.material3.DatePickerState state);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.DatePickerColors colors(optional long containerColor, optional long titleContentColor, optional long headlineContentColor, optional long weekdayContentColor, optional long subheadContentColor, optional long yearContentColor, optional long currentYearContentColor, optional long selectedYearContentColor, optional long selectedYearContainerColor, optional long dayContentColor, optional long disabledDayContentColor, optional long selectedDayContentColor, optional long disabledSelectedDayContentColor, optional long selectedDayContainerColor, optional long disabledSelectedDayContainerColor, optional long todayContentColor, optional long todayDateBorderColor);
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
     method public float getTonalElevation();
@@ -295,6 +313,9 @@
     property public final kotlin.ranges.IntRange YearRange;
     property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
     field public static final androidx.compose.material3.DatePickerDefaults INSTANCE;
+    field public static final String YearAbbrMonthDaySkeleton = "yMMMd";
+    field public static final String YearMonthSkeleton = "yMMMM";
+    field public static final String YearMonthWeekdayDaySkeleton = "yMMMMEEEEd";
   }
 
   public final class DatePickerDialog_androidKt {
@@ -302,20 +323,21 @@
   }
 
   @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class DatePickerFormatter {
-    ctor public DatePickerFormatter(optional String shortFormat, optional String mediumFormat, optional String monthYearFormat);
+    ctor public DatePickerFormatter(optional String yearSelectionSkeleton, optional String selectedDateSkeleton, optional String selectedDateDescriptionSkeleton);
   }
 
   public final class DatePickerKt {
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void DatePicker(androidx.compose.material3.DatePickerState datePickerState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.DatePickerFormatter dateFormatter, optional kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> dateValidator, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit> headline, optional androidx.compose.material3.DatePickerColors colors);
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.DatePickerState rememberDatePickerState(optional Long? initialSelectedDateMillis, optional Long? initialDisplayedMonthMillis, optional kotlin.ranges.IntRange yearRange);
+    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void DatePicker(androidx.compose.material3.DatePickerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.DatePickerFormatter dateFormatter, optional kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> dateValidator, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit> headline, optional boolean showModeToggle, optional androidx.compose.material3.DatePickerColors colors);
+    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.DatePickerState rememberDatePickerState(optional Long? initialSelectedDateMillis, optional Long? initialDisplayedMonthMillis, optional kotlin.ranges.IntRange yearRange, optional int initialDisplayMode);
   }
 
   @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class DatePickerState {
-    ctor public DatePickerState(Long? initialSelectedDateMillis, Long? initialDisplayedMonthMillis, kotlin.ranges.IntRange yearRange);
+    ctor public DatePickerState(Long? initialSelectedDateMillis, Long? initialDisplayedMonthMillis, kotlin.ranges.IntRange yearRange, int initialDisplayMode);
+    method public int getDisplayMode();
     method public Long? getSelectedDateMillis();
-    method public kotlin.ranges.IntRange getYearRange();
+    method public void setDisplayMode(int);
+    property public final int displayMode;
     property public final Long? selectedDateMillis;
-    property public final kotlin.ranges.IntRange yearRange;
     field public static final androidx.compose.material3.DatePickerState.Companion Companion;
   }
 
@@ -323,6 +345,9 @@
     method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.DatePickerState,?> Saver();
   }
 
+  public final class DateRangePickerKt {
+  }
+
   @androidx.compose.material3.ExperimentalMaterial3Api public enum DismissDirection {
     method public static androidx.compose.material3.DismissDirection valueOf(String name) throws java.lang.IllegalArgumentException;
     method public static androidx.compose.material3.DismissDirection[] values();
@@ -360,6 +385,17 @@
     enum_constant public static final androidx.compose.material3.DismissValue DismissedToStart;
   }
 
+  @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class DisplayMode {
+    field public static final androidx.compose.material3.DisplayMode.Companion Companion;
+  }
+
+  public static final class DisplayMode.Companion {
+    method public int getInput();
+    method public int getPicker();
+    property public final int Input;
+    property public final int Picker;
+  }
+
   public final class DividerDefaults {
     method @androidx.compose.runtime.Composable public long getColor();
     method public float getThickness();
@@ -574,14 +610,16 @@
 
   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 {
+  @androidx.compose.runtime.Immutable public final class ListItemColors {
   }
 
-  @androidx.compose.material3.ExperimentalMaterial3Api public final class ListItemDefaults {
+  public final class ListItemDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ListItemColors colors(optional long containerColor, optional long headlineColor, optional long leadingIconColor, optional long overlineColor, optional long supportingColor, optional long trailingIconColor, optional long disabledHeadlineColor, optional long disabledLeadingIconColor, optional long disabledTrailingIconColor);
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method @androidx.compose.runtime.Composable public long getContentColor();
@@ -595,7 +633,7 @@
   }
 
   public final class ListItemKt {
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ListItem(kotlin.jvm.functions.Function0<kotlin.Unit> headlineText, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? overlineText, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional androidx.compose.material3.ListItemColors colors, optional float tonalElevation, optional float shadowElevation);
+    method @androidx.compose.runtime.Composable public static void ListItem(kotlin.jvm.functions.Function0<kotlin.Unit> headlineText, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? overlineText, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional androidx.compose.material3.ListItemColors colors, optional float tonalElevation, optional float shadowElevation);
   }
 
   public final class MaterialTheme {
@@ -625,6 +663,10 @@
   public final class MenuKt {
   }
 
+  public final class ModalBottomSheetKt {
+    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ModalBottomSheet(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SheetState sheetState, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional float tonalElevation, optional long scrimColor, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dragHandle, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+  }
+
   public final class NavigationBarDefaults {
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method public float getElevation();
@@ -701,12 +743,20 @@
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void OutlinedTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
   }
 
+  @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class PlainTooltipState {
+    ctor public PlainTooltipState();
+    method public suspend Object? dismiss(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public boolean isVisible();
+    method public suspend Object? show(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean isVisible;
+  }
+
   public final class ProgressIndicatorDefaults {
     method @androidx.compose.runtime.Composable public long getCircularColor();
     method public int getCircularDeterminateStrokeCap();
     method public int getCircularIndeterminateStrokeCap();
     method public float getCircularStrokeWidth();
-    method public long getCircularTrackColor();
+    method @androidx.compose.runtime.Composable public long getCircularTrackColor();
     method @androidx.compose.runtime.Composable public long getLinearColor();
     method public int getLinearStrokeCap();
     method @androidx.compose.runtime.Composable public long getLinearTrackColor();
@@ -717,7 +767,7 @@
     property public final int LinearStrokeCap;
     property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> ProgressAnimationSpec;
     property @androidx.compose.runtime.Composable public final long circularColor;
-    property public final long circularTrackColor;
+    property @androidx.compose.runtime.Composable public final long circularTrackColor;
     property @androidx.compose.runtime.Composable public final long linearColor;
     property @androidx.compose.runtime.Composable public final long linearTrackColor;
     field public static final androidx.compose.material3.ProgressIndicatorDefaults INSTANCE;
@@ -746,6 +796,26 @@
     method @androidx.compose.runtime.Composable public static void RadioButton(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit>? onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.RadioButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
   }
 
+  @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable @androidx.compose.runtime.Stable public final class RichTooltipColors {
+    ctor public RichTooltipColors(long containerColor, long contentColor, long titleContentColor, long actionContentColor);
+    method public long getActionContentColor();
+    method public long getContainerColor();
+    method public long getContentColor();
+    method public long getTitleContentColor();
+    property public final long actionContentColor;
+    property public final long containerColor;
+    property public final long contentColor;
+    property public final long titleContentColor;
+  }
+
+  @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class RichTooltipState {
+    ctor public RichTooltipState();
+    method public suspend Object? dismiss(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public boolean isVisible();
+    method public suspend Object? show(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean isVisible;
+  }
+
   @androidx.compose.material3.ExperimentalMaterial3Api public final class ScaffoldDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getContentWindowInsets();
     property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets contentWindowInsets;
@@ -829,24 +899,57 @@
   public final class ShapesKt {
   }
 
+  public final class SheetDefaultsKt {
+    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SheetState rememberSheetState(optional boolean skipHalfExpanded, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SheetValue,java.lang.Boolean> confirmValueChange);
+  }
+
+  @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SheetState {
+    ctor public SheetState(boolean skipCollapsed, optional androidx.compose.material3.SheetValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SheetValue,java.lang.Boolean> confirmValueChange);
+    method public suspend Object? collapse(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public androidx.compose.material3.SheetValue getCurrentValue();
+    method public androidx.compose.material3.SheetValue getTargetValue();
+    method public suspend Object? hide(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public boolean isVisible();
+    method public float requireOffset();
+    method public suspend Object? show(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public final androidx.compose.material3.SheetValue currentValue;
+    property public final boolean isVisible;
+    property public final androidx.compose.material3.SheetValue targetValue;
+    field public static final androidx.compose.material3.SheetState.Companion Companion;
+  }
+
+  public static final class SheetState.Companion {
+    method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.SheetState,androidx.compose.material3.SheetValue> Saver(boolean skipHalfExpanded, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SheetValue,java.lang.Boolean> confirmValueChange);
+  }
+
+  @androidx.compose.material3.ExperimentalMaterial3Api public enum SheetValue {
+    method public static androidx.compose.material3.SheetValue valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.compose.material3.SheetValue[] values();
+    enum_constant public static final androidx.compose.material3.SheetValue Collapsed;
+    enum_constant public static final androidx.compose.material3.SheetValue Expanded;
+    enum_constant public static final androidx.compose.material3.SheetValue Hidden;
+  }
+
   @androidx.compose.runtime.Immutable public final class SliderColors {
   }
 
   @androidx.compose.runtime.Stable public final class SliderDefaults {
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void Thumb(androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled, optional long thumbSize);
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderPositions sliderPositions, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
+    method @androidx.compose.runtime.Composable public void Thumb(androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled, optional long thumbSize);
+    method @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderPositions sliderPositions, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.SliderColors colors(optional long thumbColor, optional long activeTrackColor, optional long activeTickColor, optional long inactiveTrackColor, optional long inactiveTickColor, optional long disabledThumbColor, optional long disabledActiveTrackColor, optional long disabledActiveTickColor, optional long disabledInactiveTrackColor, optional long disabledInactiveTickColor);
     field public static final androidx.compose.material3.SliderDefaults INSTANCE;
   }
 
   public final class SliderKt {
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RangeSlider(kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> value, kotlin.jvm.functions.Function1<? super kotlin.ranges.ClosedFloatingPointRange<java.lang.Float>,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource startInteractionSource, optional androidx.compose.foundation.interaction.MutableInteractionSource endInteractionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> startThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> endThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> track, optional int steps);
-    method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> thumb);
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> track, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> thumb);
+    method @androidx.compose.runtime.Composable public static void RangeSlider(kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> value, kotlin.jvm.functions.Function1<? super kotlin.ranges.ClosedFloatingPointRange<java.lang.Float>,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource startInteractionSource, optional androidx.compose.foundation.interaction.MutableInteractionSource endInteractionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> startThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> endThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> track, optional int steps);
+    method @Deprecated @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RangeSlider(kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> value, kotlin.jvm.functions.Function1<? super kotlin.ranges.ClosedFloatingPointRange<java.lang.Float>,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors);
+    method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> thumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> track, optional int steps);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+    method @Deprecated @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,? extends kotlin.Unit> thumb);
+    method @Deprecated @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends kotlin.Unit> onValueChange, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,? extends kotlin.Unit> track, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,? extends kotlin.Unit> thumb);
   }
 
-  @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SliderPositions {
+  @androidx.compose.runtime.Stable public final class SliderPositions {
     ctor public SliderPositions(optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> initialActiveRange, optional float[] initialTickFractions);
     method public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getActiveRange();
     method public float[] getTickFractions();
@@ -1061,29 +1164,61 @@
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
   }
 
+  public final class TimeFormat_androidKt {
+  }
+
+  @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class TimePickerColors {
+  }
+
+  @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class TimePickerDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.TimePickerColors colors(optional long clockDialColor, optional long clockDialSelectedContentColor, optional long clockDialUnselectedContentColor, optional long selectorColor, optional long containerColor, optional long periodSelectorBorderColor, optional long periodSelectorSelectedContainerColor, optional long periodSelectorUnselectedContainerColor, optional long periodSelectorSelectedContentColor, optional long periodSelectorUnselectedContentColor, optional long timeSelectorSelectedContainerColor, optional long timeSelectorUnselectedContainerColor, optional long timeSelectorSelectedContentColor, optional long timeSelectorUnselectedContentColor);
+    field public static final androidx.compose.material3.TimePickerDefaults INSTANCE;
+  }
+
+  public final class TimePickerKt {
+    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TimePicker(androidx.compose.material3.TimePickerState state, optional androidx.compose.material3.TimePickerColors colors);
+    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.TimePickerState rememberTimePickerState(optional int initialHour, optional int initialMinute, optional boolean is24Hour);
+  }
+
+  @androidx.compose.runtime.Stable public final class TimePickerState {
+    ctor public TimePickerState(int initialHour, int initialMinute, boolean is24Hour);
+    method public int getHour();
+    method public int getMinute();
+    method public boolean is24hour();
+    method public suspend Object? settle(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public final int hour;
+    property public final boolean is24hour;
+    property public final int minute;
+    field public static final androidx.compose.material3.TimePickerState.Companion Companion;
+  }
+
+  public static final class TimePickerState.Companion {
+    method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.TimePickerState,?> Saver();
+  }
+
   public final class TonalPaletteKt {
   }
 
+  @androidx.compose.material3.ExperimentalMaterial3Api public interface TooltipBoxScope {
+    method public androidx.compose.ui.Modifier tooltipAnchor(androidx.compose.ui.Modifier);
+  }
+
   @androidx.compose.material3.ExperimentalMaterial3Api public final class TooltipDefaults {
     method @androidx.compose.runtime.Composable public long getPlainTooltipContainerColor();
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getPlainTooltipContainerShape();
     method @androidx.compose.runtime.Composable public long getPlainTooltipContentColor();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getRichTooltipContainerShape();
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.RichTooltipColors richTooltipColors(optional long containerColor, optional long contentColor, optional long titleContentColor, optional long actionContentColor);
     property @androidx.compose.runtime.Composable public final long plainTooltipContainerColor;
     property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape plainTooltipContainerShape;
     property @androidx.compose.runtime.Composable public final long plainTooltipContentColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape richTooltipContainerShape;
     field public static final androidx.compose.material3.TooltipDefaults INSTANCE;
   }
 
   public final class TooltipKt {
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltipBox(kotlin.jvm.functions.Function0<kotlin.Unit> tooltip, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.TooltipState? tooltipState, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
-  }
-
-  @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class TooltipState {
-    ctor public TooltipState();
-    method public suspend Object? dismiss(kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public boolean isVisible();
-    method public suspend Object? show(kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    property public final boolean isVisible;
+    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltipBox(kotlin.jvm.functions.Function0<kotlin.Unit> tooltip, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.PlainTooltipState tooltipState, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.material3.TooltipBoxScope,kotlin.Unit> content);
+    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltipBox(kotlin.jvm.functions.Function0<kotlin.Unit> text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.RichTooltipState tooltipState, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.TooltipBoxScope,kotlin.Unit> content);
   }
 
   @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class TopAppBarColors {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index e3bd3b6..5508abe 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -232,12 +232,18 @@
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
   }
 
+  public final class DateInputKt {
+  }
+
   public final class DatePickerDialog_androidKt {
   }
 
   public final class DatePickerKt {
   }
 
+  public final class DateRangePickerKt {
+  }
+
   public final class DividerDefaults {
     method @androidx.compose.runtime.Composable public long getColor();
     method public float getThickness();
@@ -394,7 +400,24 @@
     method public static androidx.compose.ui.Modifier minimumInteractiveComponentSize(androidx.compose.ui.Modifier);
   }
 
+  @androidx.compose.runtime.Immutable public final class ListItemColors {
+  }
+
+  public final class ListItemDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.ListItemColors colors(optional long containerColor, optional long headlineColor, optional long leadingIconColor, optional long overlineColor, optional long supportingColor, optional long trailingIconColor, optional long disabledHeadlineColor, optional long disabledLeadingIconColor, optional long disabledTrailingIconColor);
+    method @androidx.compose.runtime.Composable public long getContainerColor();
+    method @androidx.compose.runtime.Composable public long getContentColor();
+    method public float getElevation();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
+    property public final float Elevation;
+    property @androidx.compose.runtime.Composable public final long containerColor;
+    property @androidx.compose.runtime.Composable public final long contentColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+    field public static final androidx.compose.material3.ListItemDefaults INSTANCE;
+  }
+
   public final class ListItemKt {
+    method @androidx.compose.runtime.Composable public static void ListItem(kotlin.jvm.functions.Function0<kotlin.Unit> headlineText, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? overlineText, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional androidx.compose.material3.ListItemColors colors, optional float tonalElevation, optional float shadowElevation);
   }
 
   public final class MaterialTheme {
@@ -424,6 +447,9 @@
   public final class MenuKt {
   }
 
+  public final class ModalBottomSheetKt {
+  }
+
   public final class NavigationBarDefaults {
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method public float getElevation();
@@ -503,7 +529,7 @@
     method public int getCircularDeterminateStrokeCap();
     method public int getCircularIndeterminateStrokeCap();
     method public float getCircularStrokeWidth();
-    method public long getCircularTrackColor();
+    method @androidx.compose.runtime.Composable public long getCircularTrackColor();
     method @androidx.compose.runtime.Composable public long getLinearColor();
     method public int getLinearStrokeCap();
     method @androidx.compose.runtime.Composable public long getLinearTrackColor();
@@ -514,7 +540,7 @@
     property public final int LinearStrokeCap;
     property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> ProgressAnimationSpec;
     property @androidx.compose.runtime.Composable public final long circularColor;
-    property public final long circularTrackColor;
+    property @androidx.compose.runtime.Composable public final long circularTrackColor;
     property @androidx.compose.runtime.Composable public final long linearColor;
     property @androidx.compose.runtime.Composable public final long linearTrackColor;
     field public static final androidx.compose.material3.ProgressIndicatorDefaults INSTANCE;
@@ -597,16 +623,31 @@
   public final class ShapesKt {
   }
 
+  public final class SheetDefaultsKt {
+  }
+
   @androidx.compose.runtime.Immutable public final class SliderColors {
   }
 
   @androidx.compose.runtime.Stable public final class SliderDefaults {
+    method @androidx.compose.runtime.Composable public void Thumb(androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled, optional long thumbSize);
+    method @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderPositions sliderPositions, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.SliderColors colors(optional long thumbColor, optional long activeTrackColor, optional long activeTickColor, optional long inactiveTrackColor, optional long inactiveTickColor, optional long disabledThumbColor, optional long disabledActiveTrackColor, optional long disabledActiveTickColor, optional long disabledInactiveTrackColor, optional long disabledInactiveTickColor);
     field public static final androidx.compose.material3.SliderDefaults INSTANCE;
   }
 
   public final class SliderKt {
-    method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+    method @androidx.compose.runtime.Composable public static void RangeSlider(kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> value, kotlin.jvm.functions.Function1<? super kotlin.ranges.ClosedFloatingPointRange<java.lang.Float>,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource startInteractionSource, optional androidx.compose.foundation.interaction.MutableInteractionSource endInteractionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> startThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> endThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> track, optional int steps);
+    method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> thumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> track, optional int steps);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+  }
+
+  @androidx.compose.runtime.Stable public final class SliderPositions {
+    ctor public SliderPositions(optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> initialActiveRange, optional float[] initialTickFractions);
+    method public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getActiveRange();
+    method public float[] getTickFractions();
+    property public final kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> activeRange;
+    property public final float[] tickFractions;
   }
 
   @androidx.compose.runtime.Stable public interface SnackbarData {
@@ -761,6 +802,28 @@
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
   }
 
+  public final class TimeFormat_androidKt {
+  }
+
+  public final class TimePickerKt {
+  }
+
+  @androidx.compose.runtime.Stable public final class TimePickerState {
+    ctor public TimePickerState(int initialHour, int initialMinute, boolean is24Hour);
+    method public int getHour();
+    method public int getMinute();
+    method public boolean is24hour();
+    method public suspend Object? settle(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public final int hour;
+    property public final boolean is24hour;
+    property public final int minute;
+    field public static final androidx.compose.material3.TimePickerState.Companion Companion;
+  }
+
+  public static final class TimePickerState.Companion {
+    method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.TimePickerState,?> Saver();
+  }
+
   public final class TonalPaletteKt {
   }
 
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/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
index 2255feb..100d571 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
@@ -69,6 +69,18 @@
     examples = BottomAppBarsExamples
 )
 
+private val BottomSheets = Component(
+    id = nextId(),
+    name = "Bottom Sheet",
+    description = "Bottom sheets are surfaces containing supplementary content, anchored to the " +
+        "bottom of the screen.",
+    // No bottom sheet icon
+    guidelinesUrl = "$ComponentGuidelinesUrl/bottom-sheets",
+    docsUrl = "$DocsUrl#bottomsheet",
+    sourceUrl = "$Material3SourceUrl/ModalBottomSheet.kt",
+    examples = BottomSheetExamples
+)
+
 private val Buttons = Component(
     id = nextId(),
     name = "Buttons",
@@ -335,6 +347,17 @@
     examples = TooltipsExamples
 )
 
+private val TimePickers = Component(
+    id = nextId(),
+    name = "Time Picker",
+    description = "Time picker allows the user to choose time of day.",
+    // No time picker icon
+    guidelinesUrl = "$ComponentGuidelinesUrl/time-picker",
+    docsUrl = "$DocsUrl#time-pickers",
+    sourceUrl = "$Material3SourceUrl/TimePicker.kt",
+    examples = TimePickerExamples
+)
+
 private val TopAppBar = Component(
     id = nextId(),
     name = "Top app bar",
@@ -350,6 +373,7 @@
 val Components = listOf(
     Badge,
     BottomAppBars,
+    BottomSheets,
     Buttons,
     Card,
     Checkboxes,
@@ -373,5 +397,6 @@
     Tabs,
     TextFields,
     Tooltips,
+    TimePickers,
     TopAppBar
 )
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 33ddf18..f264ce1 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -28,6 +28,7 @@
 import androidx.compose.material3.samples.AnimatedExtendedFloatingActionButtonSample
 import androidx.compose.material3.samples.AssistChipSample
 import androidx.compose.material3.samples.BottomAppBarWithFAB
+import androidx.compose.material3.samples.ModalBottomSheetSample
 import androidx.compose.material3.samples.ButtonSample
 import androidx.compose.material3.samples.ButtonWithIconSample
 import androidx.compose.material3.samples.CardSample
@@ -38,6 +39,7 @@
 import androidx.compose.material3.samples.ClickableCardSample
 import androidx.compose.material3.samples.ClickableElevatedCardSample
 import androidx.compose.material3.samples.ClickableOutlinedCardSample
+import androidx.compose.material3.samples.DateInputSample
 import androidx.compose.material3.samples.DatePickerDialogSample
 import androidx.compose.material3.samples.DatePickerSample
 import androidx.compose.material3.samples.DatePickerWithDateValidatorSample
@@ -99,6 +101,8 @@
 import androidx.compose.material3.samples.RadioGroupSample
 import androidx.compose.material3.samples.RangeSliderSample
 import androidx.compose.material3.samples.RangeSliderWithCustomComponents
+import androidx.compose.material3.samples.RichTooltipSample
+import androidx.compose.material3.samples.RichTooltipWithManualInvocationSample
 import androidx.compose.material3.samples.ScaffoldWithCoroutinesSnackbar
 import androidx.compose.material3.samples.ScaffoldWithCustomSnackbar
 import androidx.compose.material3.samples.ScaffoldWithIndefiniteSnackbar
@@ -131,6 +135,7 @@
 import androidx.compose.material3.samples.TextFieldWithSupportingText
 import androidx.compose.material3.samples.TextTabs
 import androidx.compose.material3.samples.ThreeLineListItem
+import androidx.compose.material3.samples.TimePickerSample
 import androidx.compose.material3.samples.TriStateCheckboxSample
 import androidx.compose.material3.samples.TwoLineListItem
 import androidx.compose.runtime.Composable
@@ -155,6 +160,17 @@
         ) { NavigationBarItemWithBadge() }
     )
 
+private const val BottomSheetExampleDescription = "Bottom Sheet examples"
+private const val BottomSheetExampleSourceUrl = "$SampleSourceUrl/BottomSheetSamples.kt"
+val BottomSheetExamples =
+    listOf(
+        Example(
+            name = ::ModalBottomSheetSample.name,
+            description = BottomSheetExampleDescription,
+            sourceUrl = BottomSheetExampleSourceUrl
+        ) { ModalBottomSheetSample() }
+    )
+
 private const val ButtonsExampleDescription = "Button examples"
 private const val ButtonsExampleSourceUrl = "$SampleSourceUrl/ButtonSamples.kt"
 val ButtonsExamples =
@@ -363,6 +379,13 @@
     ) {
         DatePickerWithDateValidatorSample()
     },
+    Example(
+        name = ::DateInputSample.name,
+        description = DatePickerExampleDescription,
+        sourceUrl = DatePickerExampleSourceUrl
+    ) {
+        DateInputSample()
+    },
 )
 
 private const val DialogExampleDescription = "Dialog examples"
@@ -888,6 +911,18 @@
     }
 )
 
+private const val TimePickerDescription = "Time Picker examples"
+private const val TimePickerSourceUrl = "$SampleSourceUrl/TimePicker.kt"
+val TimePickerExamples = listOf(
+    Example(
+        name = ::TimePickerSample.name,
+        description = TimePickerDescription,
+        sourceUrl = TimePickerSourceUrl
+    ) {
+        TimePickerSample()
+    },
+)
+
 private const val TextFieldsExampleDescription = "Text fields examples"
 private const val TextFieldsExampleSourceUrl = "$SampleSourceUrl/TextFieldSamples.kt"
 val TextFieldsExamples = listOf(
@@ -997,5 +1032,19 @@
         sourceUrl = TooltipsExampleSourceUrl
     ) {
         PlainTooltipWithManualInvocationSample()
+    },
+    Example(
+        name = ::RichTooltipSample.name,
+        description = TooltipsExampleDescription,
+        sourceUrl = TooltipsExampleSourceUrl
+    ) {
+        RichTooltipSample()
+    },
+    Example(
+        name = ::RichTooltipWithManualInvocationSample.name,
+        description = TooltipsExampleDescription,
+        sourceUrl = TooltipsExampleSourceUrl
+    ) {
+        RichTooltipWithManualInvocationSample()
     }
 )
diff --git a/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/SwipeToDismissDemo.kt b/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/SwipeToDismissDemo.kt
index b3796d1..69eff5a 100644
--- a/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/SwipeToDismissDemo.kt
+++ b/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/SwipeToDismissDemo.kt
@@ -42,13 +42,18 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.scale
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.CustomAccessibilityAction
+import androidx.compose.ui.semantics.customActions
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
 
 private val items = listOf(
     "Cupcake",
@@ -79,6 +84,8 @@
     LazyColumn {
         items(items) { item ->
             var unread by remember { mutableStateOf(false) }
+            val scope = rememberCoroutineScope()
+
             val dismissState = rememberDismissState(
                 confirmValueChange = {
                     if (it == DismissValue.DismissedToEnd) unread = !unread
@@ -135,6 +142,19 @@
                                 headlineText = {
                                     Text(item, fontWeight = if (unread) FontWeight.Bold else null)
                                 },
+                                modifier = Modifier.semantics {
+                                    // Provide accessible alternatives to swipe actions.
+                                    val label = if (unread) "Mark Read" else "Mark Unread"
+                                    customActions = listOf(
+                                        CustomAccessibilityAction(label) { unread = !unread; true },
+                                        CustomAccessibilityAction("Delete") {
+                                            scope.launch {
+                                                dismissState.dismiss(DismissDirection.EndToStart)
+                                            }
+                                            true
+                                        }
+                                    )
+                                },
                                 supportingText = { Text("Swipe me left or right!") },
                             )
                         }
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/BottomSheetSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/BottomSheetSamples.kt
new file mode 100644
index 0000000..ba4e86b
--- /dev/null
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/BottomSheetSamples.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.selection.toggleable
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material3.Button
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
+
+@Preview
+@Sampled
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ModalBottomSheetSample() {
+    var openBottomSheet by rememberSaveable { mutableStateOf(false) }
+    var skipHalfExpanded by remember { mutableStateOf(false) }
+    val scope = rememberCoroutineScope()
+    val bottomSheetState = rememberSheetState(skipHalfExpanded = skipHalfExpanded)
+
+    // App content
+    Column(
+        modifier = Modifier.fillMaxSize(),
+        horizontalAlignment = Alignment.CenterHorizontally,
+        verticalArrangement = Arrangement.Center
+    ) {
+        Row(
+            Modifier.toggleable(
+                value = skipHalfExpanded,
+                role = Role.Checkbox,
+                onValueChange = { checked -> skipHalfExpanded = checked }
+            )
+        ) {
+            Checkbox(checked = skipHalfExpanded, onCheckedChange = null)
+            Spacer(Modifier.width(16.dp))
+            Text("Skip Half Expanded State")
+        }
+        Button(onClick = { openBottomSheet = !openBottomSheet }) {
+            Text(text = "Show Bottom Sheet")
+        }
+    }
+
+    // Sheet content
+    if (openBottomSheet) {
+        ModalBottomSheet(
+            onDismissRequest = { openBottomSheet = false },
+            sheetState = bottomSheetState,
+        ) {
+            Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
+                Button(
+                    // Note: If you provide logic outside of onDismissRequest to remove the sheet,
+                    // you must additionally handle intended state cleanup, if any.
+                    onClick = {
+                        scope.launch { bottomSheetState.hide() }.invokeOnCompletion {
+                            if (!bottomSheetState.isVisible) {
+                                openBottomSheet = false
+                            }
+                        }
+                    }
+                ) {
+                    Text("Hide Bottom Sheet")
+                }
+            }
+            LazyColumn {
+                items(50) {
+                    ListItem(
+                        headlineText = { Text("Item $it") },
+                        leadingContent = {
+                            Icon(
+                                Icons.Default.Favorite,
+                                contentDescription = "Localized description"
+                            )
+                        }
+                    )
+                }
+            }
+        }
+    }
+}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DatePickerSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DatePickerSamples.kt
index e067984..b3b24eb 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DatePickerSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DatePickerSamples.kt
@@ -22,6 +22,7 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.DatePicker
 import androidx.compose.material3.DatePickerDialog
+import androidx.compose.material3.DisplayMode
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.SnackbarHost
 import androidx.compose.material3.SnackbarHostState
@@ -49,9 +50,10 @@
 @Composable
 fun DatePickerSample() {
     Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
-        // Pre-select a date with January 4, 2020
+        // Pre-select a date for January 4, 2020
         val datePickerState = rememberDatePickerState(initialSelectedDateMillis = 1578096000000)
-        DatePicker(datePickerState = datePickerState, modifier = Modifier.padding(16.dp))
+        DatePicker(state = datePickerState, modifier = Modifier.padding(16.dp))
+
         Text("Selected date timestamp: ${datePickerState.selectedDateMillis ?: "no selection"}")
     }
 }
@@ -102,7 +104,7 @@
                 }
             }
         ) {
-            DatePicker(datePickerState = datePickerState)
+            DatePicker(state = datePickerState)
         }
     }
 }
@@ -116,7 +118,7 @@
     val datePickerState = rememberDatePickerState()
     Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
         DatePicker(
-            datePickerState = datePickerState,
+            state = datePickerState,
             // Blocks Sunday and Saturday from being selected.
             dateValidator = { utcDateInMills ->
                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -134,3 +136,16 @@
         Text("Selected date timestamp: ${datePickerState.selectedDateMillis ?: "no selection"}")
     }
 }
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Sampled
+@Composable
+fun DateInputSample() {
+    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+        val state = rememberDatePickerState(initialDisplayMode = DisplayMode.Input)
+        DatePicker(state = state, modifier = Modifier.padding(16.dp))
+
+        Text("Entered date timestamp: ${state.selectedDateMillis ?: "no input"}")
+    }
+}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TimePickerSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TimePickerSamples.kt
new file mode 100644
index 0000000..21a56c0
--- /dev/null
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TimePickerSamples.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.Button
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TimePicker
+import androidx.compose.material3.TimePickerState
+import androidx.compose.material3.rememberTimePickerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Sampled
+@Composable
+fun TimePickerSample() {
+    var showTimePicker by remember { mutableStateOf(false) }
+    val state = rememberTimePickerState()
+    val formatter = remember { SimpleDateFormat("hh:mm a", Locale.getDefault()) }
+    val snackState = remember { SnackbarHostState() }
+    val snackScope = rememberCoroutineScope()
+
+    Box(propagateMinConstraints = false) {
+        Button(
+            modifier = Modifier.align(Alignment.Center),
+            onClick = { showTimePicker = true }
+        ) { Text("Set Time") }
+        SnackbarHost(hostState = snackState)
+    }
+
+    if (showTimePicker) {
+        TimePickerDialog(state, onCancel = { showTimePicker = false }) {
+            val cal = Calendar.getInstance()
+            cal.set(Calendar.HOUR_OF_DAY, state.hour)
+            cal.set(Calendar.MINUTE, state.minute)
+            cal.isLenient = false
+            snackScope.launch {
+                snackState.showSnackbar("Entered time: ${formatter.format(cal.time)}")
+            }
+            showTimePicker = false
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TimePickerDialog(
+    state: TimePickerState,
+    onCancel: () -> Unit = {},
+    onConfirm: () -> Unit = {}
+) {
+    Dialog(
+        onDismissRequest = onCancel,
+        properties = DialogProperties(usePlatformDefaultWidth = false),
+    ) {
+        Surface(
+            shape = MaterialTheme.shapes.extraLarge,
+            tonalElevation = 6.dp,
+            modifier = Modifier
+                .width(328.dp)
+                .background(
+                    shape = MaterialTheme.shapes.extraLarge,
+                    color = MaterialTheme.colorScheme.surface
+                ),
+        ) {
+            Column(
+                modifier = Modifier.padding(24.dp),
+                horizontalAlignment = Alignment.CenterHorizontally
+            ) {
+                Text(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .padding(bottom = 20.dp),
+                    text = "Select Time",
+                    style = MaterialTheme.typography.labelMedium
+                )
+                TimePicker(state)
+                Row(modifier = Modifier.fillMaxWidth()) {
+                    Spacer(modifier = Modifier.weight(1f))
+                    TextButton(onClick = onCancel) { Text("Cancel") }
+                    TextButton(onClick = onConfirm) { Text("OK") }
+                }
+            }
+        }
+    }
+}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TooltipSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TooltipSamples.kt
index a8426f0..2eaaeaa 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TooltipSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TooltipSamples.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 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.
@@ -17,19 +17,23 @@
 package androidx.compose.material3.samples
 
 import androidx.annotation.Sampled
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.requiredHeight
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.AddCircle
 import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Info
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.OutlinedButton
 import androidx.compose.material3.PlainTooltipBox
+import androidx.compose.material3.PlainTooltipState
+import androidx.compose.material3.RichTooltipBox
+import androidx.compose.material3.RichTooltipState
 import androidx.compose.material3.Text
-import androidx.compose.material3.TooltipState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
@@ -48,11 +52,12 @@
         tooltip = { Text("Add to favorites") }
     ) {
         IconButton(
-            onClick = { /* Icon button's click event */ }
+            onClick = { /* Icon button's click event */ },
+            modifier = Modifier.tooltipAnchor()
         ) {
             Icon(
                 imageVector = Icons.Filled.Favorite,
-                contentDescription = null
+                contentDescription = "Localized Description"
             )
         }
     }
@@ -63,9 +68,8 @@
 @Sampled
 @Composable
 fun PlainTooltipWithManualInvocationSample() {
-    val tooltipState = remember { TooltipState() }
+    val tooltipState = remember { PlainTooltipState() }
     val scope = rememberCoroutineScope()
-
     Column(
         horizontalAlignment = Alignment.CenterHorizontally
     ) {
@@ -75,7 +79,7 @@
         ) {
             Icon(
                 imageVector = Icons.Filled.AddCircle,
-                contentDescription = null
+                contentDescription = "Localized Description"
             )
         }
         Spacer(Modifier.requiredHeight(30.dp))
@@ -86,3 +90,76 @@
         }
     }
 }
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Sampled
+@Composable
+fun RichTooltipSample() {
+    val tooltipState = remember { RichTooltipState() }
+    val scope = rememberCoroutineScope()
+    RichTooltipBox(
+        title = { Text(richTooltipSubheadText) },
+        action = {
+            Text(
+                text = richTooltipActionText,
+                modifier = Modifier.clickable { scope.launch { tooltipState.dismiss() } }
+            )
+        },
+        text = { Text(richTooltipText) },
+        tooltipState = tooltipState
+    ) {
+        IconButton(
+            onClick = { /* Icon button's click event */ },
+            modifier = Modifier.tooltipAnchor()
+        ) {
+            Icon(
+                imageVector = Icons.Filled.Info,
+                contentDescription = "Localized Description"
+            )
+        }
+    }
+}
+@OptIn(ExperimentalMaterial3Api::class)
+@Sampled
+@Composable
+fun RichTooltipWithManualInvocationSample() {
+    val tooltipState = remember { RichTooltipState() }
+    val scope = rememberCoroutineScope()
+    Column(
+        horizontalAlignment = Alignment.CenterHorizontally
+    ) {
+        RichTooltipBox(
+            title = { Text(richTooltipSubheadText) },
+            action = {
+                Text(
+                    text = richTooltipActionText,
+                    modifier = Modifier.clickable {
+                        scope.launch {
+                            tooltipState.dismiss()
+                        }
+                    }
+                )
+            },
+            text = { Text(richTooltipText) },
+            tooltipState = tooltipState
+        ) {
+            Icon(
+                imageVector = Icons.Filled.Info,
+                contentDescription = "Localized Description"
+            )
+        }
+        Spacer(Modifier.requiredHeight(30.dp))
+        OutlinedButton(
+            onClick = { scope.launch { tooltipState.show() } }
+        ) {
+            Text("Display tooltip")
+        }
+    }
+}
+
+const val richTooltipSubheadText = "Permissions"
+const val richTooltipText =
+    "Configure permissions for selected service accounts. " +
+        "You can add and remove service account members and assign roles to them. " +
+        "Visit go/permissions for details"
+const val richTooltipActionText = "Request Access"
\ No newline at end of file
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CalendarModelTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CalendarModelTest.kt
index fd49794..d6e6ffd 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CalendarModelTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CalendarModelTest.kt
@@ -108,9 +108,22 @@
     fun formatDate() {
         val date =
             CalendarDate(year = 2022, month = 1, dayOfMonth = 1, utcTimeMillis = January2022Millis)
-        val month = model.plusMonths(model.getMonth(date), 2)
-        assertThat(model.format(date, "MM/dd/yyyy")).isEqualTo("01/01/2022")
-        assertThat(model.format(month, "MM/dd/yyyy")).isEqualTo("03/01/2022")
+        assertThat(model.formatWithSkeleton(date, "yMMMd")).isEqualTo("Jan 1, 2022")
+        assertThat(model.formatWithSkeleton(date, "dMMMy")).isEqualTo("Jan 1, 2022")
+        assertThat(model.formatWithSkeleton(date, "yMMMMEEEEd"))
+            .isEqualTo("Saturday, January 1, 2022")
+        // Check that the direct formatting is equal to the one the model does.
+        assertThat(model.formatWithSkeleton(date, "yMMMd")).isEqualTo(date.format(model, "yMMMd"))
+    }
+
+    @Test
+    fun formatMonth() {
+        val month = model.getMonth(year = 2022, month = 3)
+        assertThat(model.formatWithSkeleton(month, "yMMMM")).isEqualTo("March 2022")
+        assertThat(model.formatWithSkeleton(month, "MMMMy")).isEqualTo("March 2022")
+        // Check that the direct formatting is equal to the one the model does.
+        assertThat(model.formatWithSkeleton(month, "yMMMM"))
+            .isEqualTo(month.format(model, "yMMMM"))
     }
 
     @Test
@@ -131,29 +144,29 @@
     @Test
     fun dateInputFormat() {
         Locale.setDefault(Locale.US)
-        assertThat(model.dateInputFormat.patternWithDelimiters).isEqualTo("MM/dd/yyyy")
-        assertThat(model.dateInputFormat.patternWithoutDelimiters).isEqualTo("MMddyyyy")
-        assertThat(model.dateInputFormat.delimiter).isEqualTo('/')
+        assertThat(model.getDateInputFormat().patternWithDelimiters).isEqualTo("MM/dd/yyyy")
+        assertThat(model.getDateInputFormat().patternWithoutDelimiters).isEqualTo("MMddyyyy")
+        assertThat(model.getDateInputFormat().delimiter).isEqualTo('/')
 
         Locale.setDefault(Locale.CHINA)
-        assertThat(model.dateInputFormat.patternWithDelimiters).isEqualTo("yyyy/MM/dd")
-        assertThat(model.dateInputFormat.patternWithoutDelimiters).isEqualTo("yyyyMMdd")
-        assertThat(model.dateInputFormat.delimiter).isEqualTo('/')
+        assertThat(model.getDateInputFormat().patternWithDelimiters).isEqualTo("yyyy/MM/dd")
+        assertThat(model.getDateInputFormat().patternWithoutDelimiters).isEqualTo("yyyyMMdd")
+        assertThat(model.getDateInputFormat().delimiter).isEqualTo('/')
 
         Locale.setDefault(Locale.UK)
-        assertThat(model.dateInputFormat.patternWithDelimiters).isEqualTo("dd/MM/yyyy")
-        assertThat(model.dateInputFormat.patternWithoutDelimiters).isEqualTo("ddMMyyyy")
-        assertThat(model.dateInputFormat.delimiter).isEqualTo('/')
+        assertThat(model.getDateInputFormat().patternWithDelimiters).isEqualTo("dd/MM/yyyy")
+        assertThat(model.getDateInputFormat().patternWithoutDelimiters).isEqualTo("ddMMyyyy")
+        assertThat(model.getDateInputFormat().delimiter).isEqualTo('/')
 
         Locale.setDefault(Locale.KOREA)
-        assertThat(model.dateInputFormat.patternWithDelimiters).isEqualTo("yyyy.MM.dd")
-        assertThat(model.dateInputFormat.patternWithoutDelimiters).isEqualTo("yyyyMMdd")
-        assertThat(model.dateInputFormat.delimiter).isEqualTo('.')
+        assertThat(model.getDateInputFormat().patternWithDelimiters).isEqualTo("yyyy.MM.dd")
+        assertThat(model.getDateInputFormat().patternWithoutDelimiters).isEqualTo("yyyyMMdd")
+        assertThat(model.getDateInputFormat().delimiter).isEqualTo('.')
 
         Locale.setDefault(Locale("es", "CL"))
-        assertThat(model.dateInputFormat.patternWithDelimiters).isEqualTo("dd-MM-yyyy")
-        assertThat(model.dateInputFormat.patternWithoutDelimiters).isEqualTo("ddMMyyyy")
-        assertThat(model.dateInputFormat.delimiter).isEqualTo('-')
+        assertThat(model.getDateInputFormat().patternWithDelimiters).isEqualTo("dd-MM-yyyy")
+        assertThat(model.getDateInputFormat().patternWithoutDelimiters).isEqualTo("ddMMyyyy")
+        assertThat(model.getDateInputFormat().delimiter).isEqualTo('-')
     }
 
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@@ -171,18 +184,18 @@
 
         assertThat(newModel.today).isEqualTo(legacyModel.today)
         assertThat(month).isEqualTo(legacyMonth)
-        assertThat(newModel.dateInputFormat).isEqualTo(legacyModel.dateInputFormat)
+        assertThat(newModel.getDateInputFormat()).isEqualTo(legacyModel.getDateInputFormat())
         assertThat(newModel.plusMonths(month, 3)).isEqualTo(legacyModel.plusMonths(month, 3))
         assertThat(date).isEqualTo(legacyDate)
         assertThat(newModel.getDayOfWeek(date)).isEqualTo(legacyModel.getDayOfWeek(date))
-        assertThat(newModel.format(date, "MMM d, yyyy")).isEqualTo(
-            legacyModel.format(
+        assertThat(newModel.formatWithSkeleton(date, "MMM d, yyyy")).isEqualTo(
+            legacyModel.formatWithSkeleton(
                 date,
                 "MMM d, yyyy"
             )
         )
-        assertThat(newModel.format(month, "MMM yyyy")).isEqualTo(
-            legacyModel.format(
+        assertThat(newModel.formatWithSkeleton(month, "MMM yyyy")).isEqualTo(
+            legacyModel.formatWithSkeleton(
                 month,
                 "MMM yyyy"
             )
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputScreenshotTest.kt
new file mode 100644
index 0000000..18c5b0b8
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputScreenshotTest.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import android.os.Build
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.isDialog
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import java.time.LocalDate
+import java.time.LocalTime
+import java.time.ZoneId
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@LargeTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@OptIn(ExperimentalMaterial3Api::class)
+class DateInputScreenshotTest(private val scheme: ColorSchemeWrapper) {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @get:Rule
+    val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
+
+    private val wrap = Modifier.wrapContentSize(Alignment.Center)
+    private val wrapperTestTag = "dateInputWrapper"
+
+    @Test
+    fun dateInput_initialState() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                DatePicker(
+                    state = rememberDatePickerState(initialDisplayMode = DisplayMode.Input),
+                    showModeToggle = false
+                )
+            }
+        }
+        assertAgainstGolden("dateInput_initialState_${scheme.name}")
+    }
+
+    @Test
+    fun dateInput_withModeToggle() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                DatePicker(
+                    state = rememberDatePickerState(initialDisplayMode = DisplayMode.Input)
+                )
+            }
+        }
+        assertAgainstGolden("dateInput_withModeToggle_${scheme.name}")
+    }
+
+    @Test
+    fun dateInput_withEnteredDate() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                val dayMillis = dayInUtcMilliseconds(year = 2021, month = 3, dayOfMonth = 6)
+                DatePicker(
+                    state = rememberDatePickerState(
+                        initialSelectedDateMillis = dayMillis,
+                        initialDisplayMode = DisplayMode.Input
+                    ),
+                    showModeToggle = false
+                )
+            }
+        }
+        assertAgainstGolden("dateInput_withEnteredDate_${scheme.name}")
+    }
+
+    @Test
+    fun dateInput_invalidDatePicker() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                val monthInUtcMillis = dayInUtcMilliseconds(year = 2000, month = 6, dayOfMonth = 1)
+                DatePicker(
+                    state = rememberDatePickerState(
+                        initialDisplayedMonthMillis = monthInUtcMillis,
+                        initialDisplayMode = DisplayMode.Input
+                    ),
+                    dateValidator = { false },
+                    showModeToggle = false
+                )
+            }
+        }
+        assertAgainstGolden("dateInput_invalidDateInput_${scheme.name}")
+    }
+
+    @Test
+    fun dateInput_inDialog() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            val selectedDayMillis = dayInUtcMilliseconds(year = 2021, month = 3, dayOfMonth = 6)
+            DatePickerDialog(
+                onDismissRequest = { },
+                confirmButton = { TextButton(onClick = {}) { Text("OK") } },
+                dismissButton = { TextButton(onClick = {}) { Text("Cancel") } }
+            ) {
+                DatePicker(
+                    state = rememberDatePickerState(
+                        initialSelectedDateMillis = selectedDayMillis,
+                        initialDisplayMode = DisplayMode.Input
+                    ),
+                    showModeToggle = false
+                )
+            }
+        }
+        rule.onNode(isDialog())
+            .captureToImage()
+            .assertAgainstGolden(
+                rule = screenshotRule,
+                goldenIdentifier = "dateInput_inDialog_${scheme.name}"
+            )
+    }
+
+    // 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 =
+        LocalDate.of(year, month, dayOfMonth)
+            .atTime(LocalTime.MIDNIGHT)
+            .atZone(ZoneId.of("UTC"))
+            .toInstant()
+            .toEpochMilli()
+
+    private fun assertAgainstGolden(goldenName: String) {
+        rule.onNodeWithTag(wrapperTestTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, goldenName)
+    }
+
+    // Provide the ColorScheme and their name parameter in a ColorSchemeWrapper.
+    // This makes sure that the default method name and the initial Scuba image generated
+    // name is as expected.
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun parameters() = arrayOf(
+            ColorSchemeWrapper("lightTheme", lightColorScheme()),
+            ColorSchemeWrapper("darkTheme", darkColorScheme()),
+        )
+    }
+
+    class ColorSchemeWrapper(val name: String, val colorScheme: ColorScheme) {
+        override fun toString(): String {
+            return name
+        }
+    }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputTest.kt
new file mode 100644
index 0000000..af1833c
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputTest.kt
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher.Companion.expectValue
+import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertContentDescriptionEquals
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTextInput
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import java.util.Calendar
+import java.util.Locale
+import java.util.TimeZone
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalMaterial3Api::class)
+class DateInputTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun dateInput() {
+        lateinit var defaultHeadline: String
+        lateinit var dateInputLabel: String
+        lateinit var state: DatePickerState
+        rule.setMaterialContent(lightColorScheme()) {
+            defaultHeadline = getString(string = Strings.DateInputHeadline)
+            dateInputLabel = getString(string = Strings.DateInputLabel)
+            val monthInUtcMillis = dayInUtcMilliseconds(year = 2019, month = 1, dayOfMonth = 1)
+            state = rememberDatePickerState(
+                initialDisplayedMonthMillis = monthInUtcMillis,
+                initialDisplayMode = DisplayMode.Input
+            )
+            DatePicker(state = state)
+        }
+
+        rule.onNodeWithText(defaultHeadline).assertExists()
+
+        // Enter a date.
+        rule.onNodeWithText(dateInputLabel).performClick().performTextInput("01272019")
+
+        rule.runOnIdle {
+            assertThat(state.selectedDateMillis).isEqualTo(
+                dayInUtcMilliseconds(
+                    year = 2019,
+                    month = 1,
+                    dayOfMonth = 27
+                )
+            )
+        }
+
+        rule.onNodeWithText(defaultHeadline).assertDoesNotExist()
+        rule.onNodeWithText("Jan 27, 2019").assertExists()
+    }
+
+    @Test
+    fun dateInputWithInitialDate() {
+        lateinit var state: DatePickerState
+        rule.setMaterialContent(lightColorScheme()) {
+            val initialDateMillis = dayInUtcMilliseconds(year = 2010, month = 5, dayOfMonth = 11)
+            state = rememberDatePickerState(
+                initialSelectedDateMillis = initialDateMillis,
+                initialDisplayMode = DisplayMode.Input
+            )
+            DatePicker(state = state)
+        }
+
+        rule.onNodeWithText("05/11/2010").assertExists()
+        rule.onNodeWithText("May 11, 2010").assertExists()
+    }
+
+    @Test
+    fun inputDateNotAllowed() {
+        lateinit var dateInputLabel: String
+        lateinit var errorMessage: String
+        lateinit var state: DatePickerState
+        rule.setMaterialContent(lightColorScheme()) {
+            dateInputLabel = getString(string = Strings.DateInputLabel)
+            errorMessage = getString(string = Strings.DateInputInvalidNotAllowed)
+            state = rememberDatePickerState(initialDisplayMode = DisplayMode.Input)
+            DatePicker(state = state,
+                // All dates are invalid for the sake of this test.
+                dateValidator = { false }
+            )
+        }
+
+        rule.onNodeWithText(dateInputLabel).performClick().performTextInput("02272020")
+
+        rule.runOnIdle {
+            assertThat(state.selectedDateMillis).isNull()
+        }
+        rule.onNodeWithText("02/27/2020")
+            .assert(keyIsDefined(SemanticsProperties.Error))
+            .assert(
+                expectValue(
+                    SemanticsProperties.Error,
+                    errorMessage.format("Feb 27, 2020")
+                )
+            )
+    }
+
+    @Test
+    fun inputDateOutOfRange() {
+        lateinit var dateInputLabel: String
+        lateinit var errorMessage: String
+        lateinit var state: DatePickerState
+        rule.setMaterialContent(lightColorScheme()) {
+            dateInputLabel = getString(string = Strings.DateInputLabel)
+            errorMessage = getString(string = Strings.DateInputInvalidYearRange)
+            state = rememberDatePickerState(
+                // Limit the years selection to 2018-2023
+                yearRange = IntRange(2018, 2023),
+                initialDisplayMode = DisplayMode.Input
+            )
+            DatePicker(state = state)
+        }
+
+        rule.onNodeWithText(dateInputLabel).performClick().performTextInput("02272030")
+
+        rule.runOnIdle {
+            assertThat(state.selectedDateMillis).isNull()
+        }
+        rule.onNodeWithText("02/27/2030")
+            .assert(keyIsDefined(SemanticsProperties.Error))
+            .assert(
+                expectValue(
+                    SemanticsProperties.Error,
+                    errorMessage.format(
+                        state.stateData.yearRange.first,
+                        state.stateData.yearRange.last
+                    )
+                )
+            )
+    }
+
+    @Test
+    fun inputDateInvalidForPattern() {
+        lateinit var dateInputLabel: String
+        lateinit var errorMessage: String
+        lateinit var state: DatePickerState
+        rule.setMaterialContent(lightColorScheme()) {
+            dateInputLabel = getString(string = Strings.DateInputLabel)
+            errorMessage =
+                getString(string = Strings.DateInputInvalidForPattern).format("MM/DD/YYYY")
+            state = rememberDatePickerState(initialDisplayMode = DisplayMode.Input)
+            DatePicker(state = state)
+        }
+
+        rule.onNodeWithText(dateInputLabel).performClick().performTextInput("99272030")
+
+        rule.runOnIdle {
+            assertThat(state.selectedDateMillis).isNull()
+        }
+        rule.onNodeWithText("99/27/2030")
+            .assert(keyIsDefined(SemanticsProperties.Error))
+            .assert(expectValue(SemanticsProperties.Error, errorMessage))
+    }
+
+    @Test
+    fun switchToDatePicker() {
+        lateinit var switchToPickerDescription: String
+        lateinit var dateInputLabel: String
+        rule.setMaterialContent(lightColorScheme()) {
+            switchToPickerDescription = getString(string = Strings.DatePickerSwitchToCalendarMode)
+            dateInputLabel = getString(string = Strings.DateInputLabel)
+            DatePicker(state = rememberDatePickerState(initialDisplayMode = DisplayMode.Input))
+        }
+
+        // Click to switch to DatePicker.
+        rule.onNodeWithContentDescription(label = switchToPickerDescription).performClick()
+
+        rule.waitForIdle()
+        rule.onNodeWithContentDescription(label = "next", substring = true, ignoreCase = true)
+            .assertIsDisplayed()
+        rule.onNodeWithContentDescription(label = "previous", substring = true, ignoreCase = true)
+            .assertIsDisplayed()
+        rule.onNodeWithText(dateInputLabel).assertDoesNotExist()
+    }
+
+    @Test
+    fun defaultSemantics() {
+        val selectedDateInUtcMillis = dayInUtcMilliseconds(year = 2010, month = 5, dayOfMonth = 11)
+        lateinit var expectedHeadlineStringFormat: String
+        rule.setMaterialContent(lightColorScheme()) {
+            // e.g. "Entered date: %1$s"
+            expectedHeadlineStringFormat = getString(Strings.DateInputHeadlineDescription)
+            DatePicker(
+                state = rememberDatePickerState(
+                    initialSelectedDateMillis = selectedDateInUtcMillis,
+                    initialDisplayMode = DisplayMode.Input
+                )
+            )
+        }
+
+        val fullDateDescription = formatWithSkeleton(
+            selectedDateInUtcMillis,
+            DatePickerDefaults.YearMonthWeekdayDaySkeleton,
+            Locale.US
+        )
+
+        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 {
+        val firstDayCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
+        firstDayCalendar.clear()
+        firstDayCalendar[Calendar.YEAR] = year
+        firstDayCalendar[Calendar.MONTH] = month - 1
+        firstDayCalendar[Calendar.DAY_OF_MONTH] = dayOfMonth
+        return firstDayCalendar.timeInMillis
+    }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerScreenshotTest.kt
index 7a55b57..f82ec5c 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerScreenshotTest.kt
@@ -61,13 +61,29 @@
             Box(wrap.testTag(wrapperTestTag)) {
                 val monthInUtcMillis = dayInUtcMilliseconds(year = 2021, month = 1, dayOfMonth = 1)
                 DatePicker(
-                    datePickerState = rememberDatePickerState(
+                    state = rememberDatePickerState(
+                        initialDisplayedMonthMillis = monthInUtcMillis
+                    ),
+                    showModeToggle = false
+                )
+            }
+        }
+        assertAgainstGolden("datePicker_initialMonth_${scheme.name}")
+    }
+
+    @Test
+    fun datePicker_withModeToggle() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                val monthInUtcMillis = dayInUtcMilliseconds(year = 2021, month = 1, dayOfMonth = 1)
+                DatePicker(
+                    state = rememberDatePickerState(
                         initialDisplayedMonthMillis = monthInUtcMillis
                     )
                 )
             }
         }
-        assertAgainstGolden("datePicker_initialMonth_${scheme.name}")
+        assertAgainstGolden("datePicker_withModeToggle_${scheme.name}")
     }
 
     @Test
@@ -77,10 +93,11 @@
                 val monthInUtcMillis = dayInUtcMilliseconds(year = 2021, month = 3, dayOfMonth = 1)
                 val selectedDayMillis = dayInUtcMilliseconds(year = 2021, month = 3, dayOfMonth = 6)
                 DatePicker(
-                    datePickerState = rememberDatePickerState(
+                    state = rememberDatePickerState(
                         initialDisplayedMonthMillis = monthInUtcMillis,
                         initialSelectedDateMillis = selectedDayMillis
-                    )
+                    ),
+                    showModeToggle = false
                 )
             }
         }
@@ -93,10 +110,11 @@
             Box(wrap.testTag(wrapperTestTag)) {
                 val monthInUtcMillis = dayInUtcMilliseconds(year = 2000, month = 6, dayOfMonth = 1)
                 DatePicker(
-                    datePickerState = rememberDatePickerState(
+                    state = rememberDatePickerState(
                         initialDisplayedMonthMillis = monthInUtcMillis
                     ),
-                    dateValidator = { false }
+                    dateValidator = { false },
+                    showModeToggle = false
                 )
             }
         }
@@ -109,9 +127,10 @@
             Box(wrap.testTag(wrapperTestTag)) {
                 val monthInUtcMillis = dayInUtcMilliseconds(year = 2000, month = 5, dayOfMonth = 1)
                 DatePicker(
-                    datePickerState = rememberDatePickerState(
+                    state = rememberDatePickerState(
                         initialDisplayedMonthMillis = monthInUtcMillis
-                    )
+                    ),
+                    showModeToggle = false
                 )
             }
         }
@@ -131,10 +150,11 @@
                 dismissButton = { TextButton(onClick = {}) { Text("Cancel") } }
             ) {
                 DatePicker(
-                    datePickerState = rememberDatePickerState(
+                    state = rememberDatePickerState(
                         initialDisplayedMonthMillis = monthInUtcMillis,
                         initialSelectedDateMillis = selectedDayMillis
-                    )
+                    ),
+                    showModeToggle = false
                 )
             }
         }
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..27760ff 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,16 @@
 
 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.assertIsDisplayed
 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 +37,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
@@ -54,11 +61,11 @@
                 initialSelectedDateMillis = initialDateMillis,
                 initialDisplayedMonthMillis = monthInUtcMillis
             )
-            DatePicker(datePickerState = datePickerState)
+            DatePicker(state = datePickerState)
         }
 
         // Select the 11th day of the displayed month is selected.
-        rule.onNodeWithText("11").assertIsOn()
+        rule.onNodeWithText("11").assertIsSelected()
         rule.onNodeWithText("May 11, 2010").assertExists()
     }
 
@@ -72,13 +79,13 @@
             datePickerState = rememberDatePickerState(
                 initialDisplayedMonthMillis = monthInUtcMillis
             )
-            DatePicker(datePickerState = datePickerState)
+            DatePicker(state = datePickerState)
         }
 
         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 +100,7 @@
 
         rule.onNodeWithText(defaultHeadline).assertDoesNotExist()
         rule.onNodeWithText("Jan 27, 2019").assertExists()
-        rule.onNodeWithText("27").assertIsOn()
+        rule.onNodeWithText("27").assertIsSelected()
     }
 
     @Test
@@ -106,7 +113,7 @@
             datePickerState = rememberDatePickerState(
                 initialDisplayedMonthMillis = monthInUtcMillis
             )
-            DatePicker(datePickerState = datePickerState,
+            DatePicker(state = datePickerState,
                 // All dates are invalid for the sake of this test.
                 dateValidator = { false }
             )
@@ -132,11 +139,11 @@
             datePickerState = rememberDatePickerState(
                 initialDisplayedMonthMillis = monthInUtcMillis
             )
-            DatePicker(datePickerState = datePickerState)
+            DatePicker(state = datePickerState)
         }
 
         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 +160,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
@@ -162,7 +169,7 @@
         rule.setMaterialContent(lightColorScheme()) {
             val monthInUtcMillis = dayInUtcMilliseconds(year = 2019, month = 1, dayOfMonth = 1)
             DatePicker(
-                datePickerState = rememberDatePickerState(
+                state = rememberDatePickerState(
                     initialDisplayedMonthMillis = monthInUtcMillis,
                     // Limit the years selection to 2018-2023
                     yearRange = IntRange(2018, 2023)
@@ -183,7 +190,7 @@
         rule.setMaterialContent(lightColorScheme()) {
             val monthInUtcMillis = dayInUtcMilliseconds(year = 2018, month = 1, dayOfMonth = 1)
             DatePicker(
-                datePickerState = rememberDatePickerState(
+                state = rememberDatePickerState(
                     initialDisplayedMonthMillis = monthInUtcMillis
                 )
             )
@@ -214,7 +221,7 @@
         rule.setMaterialContent(lightColorScheme()) {
             val monthInUtcMillis = dayInUtcMilliseconds(year = 2018, month = 1, dayOfMonth = 1)
             DatePicker(
-                datePickerState = rememberDatePickerState(
+                state = rememberDatePickerState(
                     initialDisplayedMonthMillis = monthInUtcMillis,
                     // Limit the years to just 2018
                     yearRange = IntRange(2018, 2018)
@@ -242,6 +249,27 @@
     }
 
     @Test
+    fun switchToDateInput() {
+        lateinit var switchToInputDescription: String
+        lateinit var dateInputLabel: String
+        rule.setMaterialContent(lightColorScheme()) {
+            switchToInputDescription = getString(string = Strings.DatePickerSwitchToInputMode)
+            dateInputLabel = getString(string = Strings.DateInputLabel)
+            DatePicker(state = rememberDatePickerState())
+        }
+
+        // Click to switch to DateInput.
+        rule.onNodeWithContentDescription(label = switchToInputDescription).performClick()
+
+        rule.waitForIdle()
+        rule.onNodeWithText(dateInputLabel).assertIsDisplayed()
+        rule.onNodeWithContentDescription(label = "next", substring = true, ignoreCase = true)
+            .assertDoesNotExist()
+        rule.onNodeWithContentDescription(label = "previous", substring = true, ignoreCase = true)
+            .assertDoesNotExist()
+    }
+
+    @Test
     fun state_initWithSelectedDate() {
         lateinit var datePickerState: DatePickerState
         rule.setMaterialContent(lightColorScheme()) {
@@ -250,8 +278,8 @@
         }
         with(datePickerState) {
             assertThat(selectedDateMillis).isEqualTo(1649721600000L)
-            assertThat(displayedMonth).isEqualTo(
-                calendarModel.getMonth(year = 2022, month = 4)
+            assertThat(stateData.displayedMonth).isEqualTo(
+                stateData.calendarModel.getMonth(year = 2022, month = 4)
             )
         }
     }
@@ -268,8 +296,8 @@
             // Assert that the actual selectedDateMillis was rounded down to the start of day
             // timestamp
             assertThat(selectedDateMillis).isEqualTo(1649721600000L)
-            assertThat(displayedMonth).isEqualTo(
-                calendarModel.getMonth(year = 2022, month = 4)
+            assertThat(stateData.displayedMonth).isEqualTo(
+                stateData.calendarModel.getMonth(year = 2022, month = 4)
             )
         }
     }
@@ -288,8 +316,8 @@
         with(datePickerState) {
             assertThat(selectedDateMillis).isEqualTo(1649721600000L)
             // Assert that the displayed month is the current month as of today.
-            assertThat(displayedMonth).isEqualTo(
-                calendarModel.getMonth(calendarModel.today.utcTimeMillis)
+            assertThat(stateData.displayedMonth).isEqualTo(
+                stateData.calendarModel.getMonth(stateData.calendarModel.today.utcTimeMillis)
             )
         }
     }
@@ -308,8 +336,8 @@
         with(datePickerState) {
             assertThat(selectedDateMillis).isNull()
             // Assert that the displayed month is the current month as of today.
-            assertThat(displayedMonth).isEqualTo(
-                calendarModel.getMonth(calendarModel.today.utcTimeMillis)
+            assertThat(stateData.displayedMonth).isEqualTo(
+                stateData.calendarModel.getMonth(stateData.calendarModel.today.utcTimeMillis)
             )
         }
     }
@@ -322,34 +350,35 @@
             datePickerState = rememberDatePickerState()
         }
 
-        val date = datePickerState!!.calendarModel.getCanonicalDate(1649721600000L) // 04/12/2022
-        val displayedMonth = datePickerState!!.calendarModel.getMonth(date)
-        rule.runOnIdle {
-            datePickerState!!.selectedDate = date
-            datePickerState!!.displayedMonth = displayedMonth
-        }
+        with(datePickerState!!) {
+            val date =
+                stateData.calendarModel.getCanonicalDate(1649721600000L) // 04/12/2022
+            val displayedMonth = stateData.calendarModel.getMonth(date)
+            rule.runOnIdle {
+                stateData.selectedStartDate = date
+                stateData.displayedMonth = displayedMonth
+            }
 
-        datePickerState = null
+            datePickerState = null
 
-        restorationTester.emulateSavedInstanceStateRestore()
+            restorationTester.emulateSavedInstanceStateRestore()
 
-        rule.runOnIdle {
-            assertThat(datePickerState!!.selectedDate).isEqualTo(date)
-            assertThat(datePickerState!!.displayedMonth).isEqualTo(displayedMonth)
-            assertThat(datePickerState!!.selectedDateMillis).isEqualTo(1649721600000L)
+            rule.runOnIdle {
+                assertThat(stateData.selectedStartDate).isEqualTo(date)
+                assertThat(stateData.displayedMonth).isEqualTo(displayedMonth)
+                assertThat(datePickerState!!.selectedDateMillis).isEqualTo(1649721600000L)
+            }
         }
     }
 
     @Test(expected = IllegalArgumentException::class)
-    fun initialDateOutObBounds() {
-        lateinit var datePickerState: DatePickerState
+    fun initialDateOutOfBounds() {
         rule.setMaterialContent(lightColorScheme()) {
             val initialDateMillis = dayInUtcMilliseconds(year = 2051, month = 5, dayOfMonth = 11)
-            datePickerState = rememberDatePickerState(
+            rememberDatePickerState(
                 initialSelectedDateMillis = initialDateMillis,
                 yearRange = IntRange(2000, 2050)
             )
-            DatePicker(datePickerState = datePickerState)
         }
     }
 
@@ -362,10 +391,47 @@
                 initialDisplayedMonthMillis = monthInUtcMillis,
                 yearRange = IntRange(2000, 2050)
             )
-            DatePicker(datePickerState = datePickerState)
+            DatePicker(state = datePickerState)
         }
     }
 
+    @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(
+                state = 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/DateRangePickerTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateRangePickerTest.kt
new file mode 100644
index 0000000..61f6b74
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateRangePickerTest.kt
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import java.util.Calendar
+import java.util.TimeZone
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalMaterial3Api::class)
+class DateRangePickerTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun state_initWithSelectedDates() {
+        lateinit var dateRangePickerState: DateRangePickerState
+        rule.setMaterialContent(lightColorScheme()) {
+            // 04/12/2022
+            dateRangePickerState = rememberDateRangePickerState(
+                // 04/12/2022
+                initialSelectedStartDateMillis = 1649721600000L,
+                // 04/13/2022
+                initialSelectedEndDateMillis = 1649721600000L + MillisecondsIn24Hours
+            )
+        }
+        with(dateRangePickerState) {
+            assertThat(selectedStartDateMillis).isEqualTo(1649721600000L)
+            assertThat(selectedEndDateMillis).isEqualTo(1649721600000L + MillisecondsIn24Hours)
+            assertThat(stateData.displayedMonth).isEqualTo(
+                stateData.calendarModel.getMonth(year = 2022, month = 4)
+            )
+        }
+    }
+
+    @Test
+    fun state_initWithSelectedDates_roundingToUtcMidnight() {
+        lateinit var dateRangePickerState: DateRangePickerState
+        rule.setMaterialContent(lightColorScheme()) {
+            dateRangePickerState =
+                rememberDateRangePickerState(
+                    // 04/12/2022
+                    initialSelectedStartDateMillis = 1649721600000L + 10000L,
+                    // 04/13/2022
+                    initialSelectedEndDateMillis = 1649721600000L + MillisecondsIn24Hours + 10000L
+                )
+        }
+        with(dateRangePickerState) {
+            // Assert that the actual selectedDateMillis was rounded down to the start of day
+            // timestamp
+            assertThat(selectedStartDateMillis).isEqualTo(1649721600000L)
+            assertThat(selectedEndDateMillis).isEqualTo(1649721600000L + MillisecondsIn24Hours)
+            assertThat(stateData.displayedMonth).isEqualTo(
+                stateData.calendarModel.getMonth(year = 2022, month = 4)
+            )
+        }
+    }
+
+    @Test
+    fun state_initWithEndDateOnly() {
+        lateinit var dateRangePickerState: DateRangePickerState
+        rule.setMaterialContent(lightColorScheme()) {
+            // 04/12/2022
+            dateRangePickerState = rememberDateRangePickerState(
+                // 04/12/2022
+                initialSelectedEndDateMillis = 1649721600000L
+            )
+        }
+        with(dateRangePickerState) {
+            // Expecting null for both start and end dates when providing just an initial end date.
+            assertThat(selectedStartDateMillis).isNull()
+            assertThat(selectedEndDateMillis).isNull()
+        }
+    }
+
+    @Test
+    fun state_initWithEndDateBeforeStartDate() {
+        lateinit var dateRangePickerState: DateRangePickerState
+        rule.setMaterialContent(lightColorScheme()) {
+            // 04/12/2022
+            dateRangePickerState = rememberDateRangePickerState(
+                // 04/12/2022
+                initialSelectedStartDateMillis = 1649721600000L,
+                // 04/11/2022
+                initialSelectedEndDateMillis = 1649721600000L - MillisecondsIn24Hours
+            )
+        }
+        with(dateRangePickerState) {
+            assertThat(selectedStartDateMillis).isEqualTo(1649721600000L)
+            // Expecting the end date to be null, as it was initialized with date that is earlier
+            // than the start date.
+            assertThat(selectedEndDateMillis).isNull()
+            assertThat(stateData.displayedMonth).isEqualTo(
+                stateData.calendarModel.getMonth(year = 2022, month = 4)
+            )
+        }
+    }
+
+    @Test
+    fun state_initWithEqualStartAndEndDates() {
+        lateinit var dateRangePickerState: DateRangePickerState
+        rule.setMaterialContent(lightColorScheme()) {
+            // 04/12/2022
+            dateRangePickerState = rememberDateRangePickerState(
+                // 04/12/2022 + a few added milliseconds to ensure that the state is checking the
+                // canonical date.
+                initialSelectedStartDateMillis = 1649721600000L + 1000,
+                // 04/12/2022
+                initialSelectedEndDateMillis = 1649721600000L
+            )
+        }
+        with(dateRangePickerState) {
+            assertThat(selectedStartDateMillis).isEqualTo(1649721600000L)
+            // Expecting the end date to be null, as it was initialized with the same canonical date
+            // as the start date.
+            assertThat(selectedEndDateMillis).isNull()
+            assertThat(stateData.displayedMonth).isEqualTo(
+                stateData.calendarModel.getMonth(year = 2022, month = 4)
+            )
+        }
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun initialStartDateOutOfBounds() {
+        rule.setMaterialContent(lightColorScheme()) {
+            val initialStartDateMillis =
+                dayInUtcMilliseconds(year = 1999, month = 5, dayOfMonth = 11)
+            val initialEndDateMillis = dayInUtcMilliseconds(year = 2020, month = 5, dayOfMonth = 12)
+            rememberDateRangePickerState(
+                initialSelectedStartDateMillis = initialStartDateMillis,
+                initialSelectedEndDateMillis = initialEndDateMillis,
+                yearRange = IntRange(2000, 2050)
+            )
+        }
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun initialEndDateOutOfBounds() {
+        rule.setMaterialContent(lightColorScheme()) {
+            val initialStartDateMillis =
+                dayInUtcMilliseconds(year = 2020, month = 1, dayOfMonth = 10)
+            val initialEndDateMillis = dayInUtcMilliseconds(year = 2051, month = 5, dayOfMonth = 12)
+            rememberDateRangePickerState(
+                initialSelectedStartDateMillis = initialStartDateMillis,
+                initialSelectedEndDateMillis = initialEndDateMillis,
+                yearRange = IntRange(2000, 2050)
+            )
+        }
+    }
+
+    @Test
+    fun state_restoresDatePickerState() {
+        val restorationTester = StateRestorationTester(rule)
+        var dateRangePickerState: DateRangePickerState? = null
+        restorationTester.setContent {
+            dateRangePickerState = rememberDateRangePickerState()
+        }
+
+        with(dateRangePickerState!!) {
+            // 04/12/2022
+            val startDate =
+                stateData.calendarModel.getCanonicalDate(1649721600000L)
+            // 04/13/2022
+            val endDate =
+                stateData.calendarModel.getCanonicalDate(1649721600000L + MillisecondsIn24Hours)
+            val displayedMonth = stateData.calendarModel.getMonth(startDate)
+            rule.runOnIdle {
+                stateData.selectedStartDate = startDate
+                stateData.selectedEndDate = endDate
+                stateData.displayedMonth = displayedMonth
+            }
+
+            dateRangePickerState = null
+
+            restorationTester.emulateSavedInstanceStateRestore()
+
+            rule.runOnIdle {
+                assertThat(stateData.selectedStartDate).isEqualTo(startDate)
+                assertThat(stateData.selectedEndDate).isEqualTo(endDate)
+                assertThat(stateData.displayedMonth).isEqualTo(displayedMonth)
+                assertThat(dateRangePickerState!!.selectedStartDateMillis)
+                    .isEqualTo(1649721600000L)
+                assertThat(dateRangePickerState!!.selectedEndDateMillis)
+                    .isEqualTo(1649721600000L + MillisecondsIn24Hours)
+            }
+        }
+    }
+
+    // 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 {
+        val firstDayCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
+        firstDayCalendar.clear()
+        firstDayCalendar[Calendar.YEAR] = year
+        firstDayCalendar[Calendar.MONTH] = month - 1
+        firstDayCalendar[Calendar.DAY_OF_MONTH] = dayOfMonth
+        return firstDayCalendar.timeInMillis
+    }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
new file mode 100644
index 0000000..ed5213e
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
@@ -0,0 +1,848 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import android.content.ComponentCallbacks2
+import android.content.pm.ActivityInfo
+import android.content.res.Configuration
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.isPopup
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onFirst
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onParent
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeDown
+import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.unit.coerceAtMost
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.width
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import junit.framework.TestCase.fail
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalMaterial3Api::class)
+class ModalBottomSheetTest {
+
+    @get:Rule
+    val rule = createAndroidComposeRule<ComponentActivity>()
+
+    private val sheetHeight = 256.dp
+    private val sheetTag = "sheetContentTag"
+    private val BackTestTag = "Back"
+
+    @Test
+    fun modalBottomSheet_isDismissedOnTapOutside() {
+        var showBottomSheet by mutableStateOf(true)
+
+        rule.setContent {
+            if (showBottomSheet) {
+                ModalBottomSheet(onDismissRequest = { showBottomSheet = false }) {
+                    Box(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(sheetHeight)
+                            .testTag(sheetTag)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(sheetTag).assertIsDisplayed()
+
+        val outsideY = with(rule.density) {
+            rule.onAllNodes(isPopup()).onFirst().getUnclippedBoundsInRoot().height.roundToPx() / 4
+        }
+
+        UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).click(0, outsideY)
+        rule.waitForIdle()
+
+        // Bottom sheet should not exist
+        rule.onNodeWithTag(sheetTag).assertDoesNotExist()
+    }
+
+    @Test
+    fun modalBottomSheet_fillsScreenWidth() {
+        var boxWidth = 0
+        var screenWidth by mutableStateOf(0)
+
+        rule.setContent {
+            val context = LocalContext.current
+            val density = LocalDensity.current
+            val resScreenWidth = context.resources.configuration.screenWidthDp
+            with(density) { screenWidth = resScreenWidth.dp.roundToPx() }
+
+            ModalBottomSheet(onDismissRequest = {}) {
+                Box(
+                    Modifier
+                        .fillMaxWidth()
+                        .height(sheetHeight)
+                        .onSizeChanged { boxWidth = it.width }
+                )
+            }
+        }
+        assertThat(boxWidth).isEqualTo(screenWidth)
+    }
+
+    @Test
+    fun modalBottomSheet_wideScreen_sheetRespectsMaxWidthAndIsCentered() {
+        rule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+        val latch = CountDownLatch(1)
+
+        rule.activity.application.registerComponentCallbacks(object : ComponentCallbacks2 {
+            override fun onConfigurationChanged(p0: Configuration) {
+                latch.countDown()
+            }
+
+            override fun onLowMemory() {
+                // NO-OP
+            }
+
+            override fun onTrimMemory(p0: Int) {
+                // NO-OP
+            }
+        })
+
+        try {
+            latch.await(1500, TimeUnit.MILLISECONDS)
+            rule.setContent {
+                ModalBottomSheet(onDismissRequest = {}) {
+                    Box(
+                        Modifier
+                            .testTag(sheetTag)
+                            .fillMaxHeight(0.4f)
+                    )
+                }
+            }
+
+            val simulatedRootWidth = rule.onNode(isPopup()).getUnclippedBoundsInRoot().width
+            val maxSheetWidth = 640.dp
+            val expectedSheetWidth = maxSheetWidth.coerceAtMost(simulatedRootWidth)
+            // Our sheet should be max 640 dp but fill the width if the container is less wide
+            val expectedSheetLeft = if (simulatedRootWidth <= expectedSheetWidth) {
+                0.dp
+            } else {
+                (simulatedRootWidth - expectedSheetWidth) / 2
+            }
+
+            rule.onNodeWithTag(sheetTag)
+                .onParent()
+                .assertLeftPositionInRootIsEqualTo(
+                    expectedLeft = expectedSheetLeft
+                )
+                .assertWidthIsEqualTo(expectedSheetWidth)
+        } catch (e: InterruptedException) {
+            fail("Unable to verify sheet width in landscape orientation")
+        } finally {
+            rule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_defaultStateForSmallContentIsFullExpanded() {
+        lateinit var sheetState: SheetState
+
+        rule.setContent {
+            sheetState = rememberSheetState()
+            ModalBottomSheet(onDismissRequest = {}, sheetState = sheetState, dragHandle = null) {
+                Box(
+                    Modifier
+                        .fillMaxWidth()
+                        .testTag(sheetTag)
+                        .height(sheetHeight)
+                )
+            }
+        }
+
+        val height = rule.onNode(isPopup()).getUnclippedBoundsInRoot().height
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+        rule.onNodeWithTag(sheetTag).assertTopPositionInRootIsEqualTo(height - sheetHeight)
+    }
+
+    @Test
+    fun modalBottomSheet_defaultStateForLargeContentIsHalfExpanded() {
+        lateinit var sheetState: SheetState
+        var screenHeightPx by mutableStateOf(0f)
+
+        rule.setContent {
+            sheetState = rememberSheetState()
+            val context = LocalContext.current
+            val density = LocalDensity.current
+            val resScreenHeight = context.resources.configuration.screenHeightDp
+            with(density) {
+                screenHeightPx = resScreenHeight.dp.roundToPx().toFloat()
+            }
+            ModalBottomSheet(onDismissRequest = {}, sheetState = sheetState) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(sheetTag))
+            }
+        }
+        rule.waitForIdle()
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Collapsed)
+        assertThat(sheetState.requireOffset())
+            .isWithin(1f)
+            .of(screenHeightPx / 2f)
+    }
+
+    @Test
+    fun modalBottomSheet_isDismissedOnBackPress() {
+        var showBottomSheet by mutableStateOf(true)
+        rule.setContent {
+            val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
+            if (showBottomSheet) {
+                ModalBottomSheet(onDismissRequest = { showBottomSheet = false }) {
+                    Box(
+                        Modifier
+                            .size(sheetHeight)
+                            .testTag(sheetTag)) {
+                        Button(
+                            onClick = { dispatcher.onBackPressed() },
+                            modifier = Modifier.testTag(BackTestTag),
+                            content = { Text("Content") },
+                        )
+                    }
+                }
+            }
+        }
+
+        // Popup should be visible
+        rule.onNodeWithTag(sheetTag).assertIsDisplayed()
+
+        rule.onNodeWithTag(BackTestTag).performClick()
+        rule.onNodeWithTag(BackTestTag).assertDoesNotExist()
+
+        // Popup should not exist
+        rule.onNodeWithTag(sheetTag).assertDoesNotExist()
+    }
+
+    @Test
+    fun modalBottomSheet_shortSheet_sizeChanges_snapsToNewTarget() {
+        lateinit var state: SheetState
+        var size by mutableStateOf(56.dp)
+        var screenHeight by mutableStateOf(0.dp)
+        val expectedExpandedAnchor by derivedStateOf {
+            with(rule.density) {
+                (screenHeight - size).toPx()
+            }
+        }
+
+        rule.setContent {
+            val context = LocalContext.current
+            screenHeight = context.resources.configuration.screenHeightDp.dp
+            state = rememberSheetState()
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = state,
+                dragHandle = null
+            ) {
+                Box(
+                    Modifier
+                        .height(size)
+                        .fillMaxWidth()
+                )
+            }
+        }
+        assertThat(state.requireOffset()).isWithin(0.5f).of(expectedExpandedAnchor)
+
+        size = 100.dp
+        rule.waitForIdle()
+        assertThat(state.requireOffset()).isWithin(0.5f).of(expectedExpandedAnchor)
+
+        size = 30.dp
+        rule.waitForIdle()
+        assertThat(state.requireOffset()).isWithin(0.5f).of(expectedExpandedAnchor)
+    }
+
+    @Test
+    fun modalBottomSheet_emptySheet_expandDoesNotAnimate() {
+        lateinit var state: SheetState
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            state = rememberSheetState()
+            scope = rememberCoroutineScope()
+
+            ModalBottomSheet(onDismissRequest = {}, sheetState = state, dragHandle = null) {}
+        }
+        assertThat(state.swipeableState.currentValue).isEqualTo(SheetValue.Hidden)
+        val hiddenOffset = state.requireOffset()
+        scope.launch { state.show() }
+        rule.waitForIdle()
+
+        assertThat(state.swipeableState.currentValue).isEqualTo(SheetValue.Expanded)
+        val expandedOffset = state.requireOffset()
+
+        assertThat(hiddenOffset).isEqualTo(expandedOffset)
+    }
+
+    @Test
+    fun modalBottomSheet_anchorsChange_retainsCurrentValue() {
+        lateinit var state: SheetState
+        var amountOfItems by mutableStateOf(0)
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            state = rememberSheetState()
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = state,
+                dragHandle = null,
+            ) {
+                scope = rememberCoroutineScope()
+                LazyColumn {
+                    items(amountOfItems) {
+                        ListItem(headlineText = { Text("$it") })
+                    }
+                }
+            }
+        }
+
+        assertThat(state.currentValue).isEqualTo(SheetValue.Hidden)
+
+        amountOfItems = 50
+        rule.waitForIdle()
+        scope.launch {
+            state.show()
+        }
+        // The anchors should now be {Hidden, HalfExpanded, Expanded}
+
+        rule.waitForIdle()
+        assertThat(state.currentValue).isEqualTo(SheetValue.Collapsed)
+
+        amountOfItems = 100 // The anchors should now be {Hidden, HalfExpanded, Expanded}
+
+        rule.waitForIdle()
+        assertThat(state.currentValue).isEqualTo(SheetValue.Collapsed) // We should
+        // retain the current value if possible
+        assertThat(state.swipeableState.anchors).containsKey(SheetValue.Hidden)
+        assertThat(state.swipeableState.anchors).containsKey(SheetValue.Collapsed)
+        assertThat(state.swipeableState.anchors).containsKey(SheetValue.Expanded)
+
+        amountOfItems = 0 // When the sheet height is 0, we should only have a hidden anchor
+        rule.waitForIdle()
+        assertThat(state.currentValue).isEqualTo(SheetValue.Hidden)
+        assertThat(state.swipeableState.anchors).containsKey(SheetValue.Hidden)
+        assertThat(state.swipeableState.anchors)
+            .doesNotContainKey(SheetValue.Collapsed)
+        assertThat(state.swipeableState.anchors).doesNotContainKey(SheetValue.Expanded)
+    }
+
+    @Test
+    fun modalBottomSheet_nestedScroll_consumesWithinBounds_scrollsOutsideBounds() {
+        lateinit var sheetState: SheetState
+        lateinit var scrollState: ScrollState
+        rule.setContent {
+            sheetState = rememberSheetState()
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = sheetState,
+            ) {
+                scrollState = rememberScrollState()
+                Column(
+                    Modifier
+                        .verticalScroll(scrollState)
+                        .testTag(sheetTag)
+                ) {
+                    repeat(100) {
+                        Text(it.toString(), Modifier.requiredHeight(50.dp))
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+
+        assertThat(scrollState.value).isEqualTo(0)
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Collapsed)
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput {
+                swipeUp(startY = bottom, endY = bottom / 2)
+            }
+        rule.waitForIdle()
+        assertThat(scrollState.value).isEqualTo(0)
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput {
+                swipeUp(startY = bottom, endY = top)
+            }
+        rule.waitForIdle()
+        assertThat(scrollState.value).isGreaterThan(0)
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput {
+                swipeDown(startY = top, endY = bottom)
+            }
+        rule.waitForIdle()
+        assertThat(scrollState.value).isEqualTo(0)
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput {
+                swipeDown(startY = top, endY = bottom / 2)
+            }
+        rule.waitForIdle()
+        assertThat(scrollState.value).isEqualTo(0)
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Collapsed)
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput {
+                swipeDown(startY = bottom / 2, endY = bottom)
+            }
+        rule.waitForIdle()
+        assertThat(scrollState.value).isEqualTo(0)
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
+    }
+
+    @Test
+    fun modalBottomSheet_missingAnchors_findsClosest() {
+        val topTag = "ModalBottomSheetLayout"
+        var showShortContent by mutableStateOf(false)
+        val sheetState = SheetState(skipCollapsed = false)
+        lateinit var scope: CoroutineScope
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            ModalBottomSheet(
+                onDismissRequest = {},
+                modifier = Modifier.testTag(topTag),
+                sheetState = sheetState,
+            ) {
+                if (showShortContent) {
+                    Box(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(100.dp)
+                    )
+                } else {
+                    Box(
+                        Modifier
+                            .fillMaxSize()
+                            .testTag(sheetTag)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(topTag).performTouchInput {
+            swipeDown()
+            swipeDown()
+        }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
+        }
+
+        showShortContent = true
+        scope.launch { sheetState.show() } // We can't use LaunchedEffect with Swipeable in tests
+        // yet, so we're invoking this outside of composition. See b/254115946.
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_expandBySwiping() {
+        lateinit var sheetState: SheetState
+        rule.setContent {
+            sheetState = rememberSheetState()
+            ModalBottomSheet(onDismissRequest = {}, sheetState = sheetState) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(sheetTag)
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Collapsed)
+        }
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput { swipeUp() }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_respectsConfirmStateChange() {
+        lateinit var sheetState: SheetState
+        rule.setContent {
+            sheetState = rememberSheetState(
+                confirmValueChange = { newState ->
+                    newState != SheetValue.Hidden
+                }
+            )
+            ModalBottomSheet(onDismissRequest = {}, sheetState = sheetState) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(sheetTag)
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Collapsed)
+        }
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput { swipeDown() }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Collapsed)
+        }
+
+        rule.onNodeWithTag(sheetTag).onParent()
+            .performSemanticsAction(SemanticsActions.Dismiss)
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Collapsed)
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_hideBySwiping_tallBottomSheet() {
+        lateinit var sheetState: SheetState
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            sheetState = rememberSheetState()
+            scope = rememberCoroutineScope()
+            ModalBottomSheet(onDismissRequest = {}, sheetState = sheetState) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(sheetTag)
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Collapsed)
+        }
+
+        scope.launch { sheetState.expand() }
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+        }
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput { swipeDown() }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_hideBySwiping_skipHalfExpanded() {
+        lateinit var sheetState: SheetState
+        rule.setContent {
+            sheetState = rememberSheetState(skipHalfExpanded = true)
+            ModalBottomSheet(onDismissRequest = {}, sheetState = sheetState) {
+                Box(
+                    Modifier
+                        .fillMaxWidth()
+                        .height(sheetHeight)
+                        .testTag(sheetTag)
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+        }
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput { swipeDown() }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_hideManually_skipHalfExpanded(): Unit = runBlocking(
+        AutoTestFrameClock()
+    ) {
+        lateinit var sheetState: SheetState
+        rule.setContent {
+            sheetState = rememberSheetState(skipHalfExpanded = true)
+            ModalBottomSheet(onDismissRequest = {}, sheetState = sheetState) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(sheetTag)
+                )
+            }
+        }
+        assertThat(sheetState.currentValue == SheetValue.Expanded)
+
+        sheetState.hide()
+
+        assertThat(sheetState.currentValue == SheetValue.Hidden)
+    }
+
+    @Test
+    fun modalBottomSheet_testDismissAction_tallBottomSheet_whenHalfExpanded() {
+        rule.setContent {
+            ModalBottomSheet(onDismissRequest = {}) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(sheetTag)
+                )
+            }
+        }
+
+        rule.onNodeWithTag(sheetTag).onParent()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Collapse))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Expand))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
+            .performSemanticsAction(SemanticsActions.Dismiss)
+    }
+
+    @Test
+    fun modalBottomSheet_testExpandAction_tallBottomSheet_whenHalfExpanded() {
+        lateinit var sheetState: SheetState
+        rule.setContent {
+            sheetState = rememberSheetState()
+            ModalBottomSheet(onDismissRequest = {}, sheetState = sheetState) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(sheetTag)
+                )
+            }
+        }
+
+        rule.onNodeWithTag(sheetTag).onParent()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Collapse))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Expand))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
+            .performSemanticsAction(SemanticsActions.Expand)
+
+        rule.runOnIdle {
+            assertThat(sheetState.requireOffset()).isEqualTo(0f)
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_testDismissAction_tallBottomSheet_whenExpanded() {
+        lateinit var sheetState: SheetState
+        lateinit var scope: CoroutineScope
+
+        var screenHeightPx by mutableStateOf(0f)
+
+        rule.setContent {
+            sheetState = rememberSheetState()
+            scope = rememberCoroutineScope()
+            val context = LocalContext.current
+            val density = LocalDensity.current
+            val resScreenHeight = context.resources.configuration.screenHeightDp
+            with(density) {
+                screenHeightPx = resScreenHeight.dp.roundToPx().toFloat()
+            }
+
+            ModalBottomSheet(onDismissRequest = {}, sheetState = sheetState) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(sheetTag)
+                )
+            }
+        }
+        scope.launch {
+            sheetState.expand()
+        }
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(sheetTag).onParent()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Expand))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Collapse))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
+            .performSemanticsAction(SemanticsActions.Dismiss)
+
+        rule.runOnIdle {
+            assertThat(sheetState.requireOffset()).isWithin(1f).of(screenHeightPx)
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_testCollapseAction_tallBottomSheet_whenExpanded() {
+        lateinit var sheetState: SheetState
+        lateinit var scope: CoroutineScope
+
+        var screenHeightPx by mutableStateOf(0f)
+
+        rule.setContent {
+            sheetState = rememberSheetState()
+            scope = rememberCoroutineScope()
+            val context = LocalContext.current
+            val density = LocalDensity.current
+            val resScreenHeight = context.resources.configuration.screenHeightDp
+            with(density) {
+                screenHeightPx = resScreenHeight.dp.roundToPx().toFloat()
+            }
+
+            ModalBottomSheet(onDismissRequest = {}, sheetState = sheetState) {
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag(sheetTag)
+                )
+            }
+        }
+        scope.launch {
+            sheetState.expand()
+        }
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(sheetTag).onParent()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Expand))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Collapse))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
+            .performSemanticsAction(SemanticsActions.Collapse)
+
+        rule.runOnIdle {
+            assertThat(sheetState.requireOffset()).isWithin(1f).of(screenHeightPx / 2)
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_shortSheet_anchorChangeHandler_previousTargetNotInAnchors_reconciles() {
+        val sheetState = SheetState(skipCollapsed = false)
+        var hasSheetContent by mutableStateOf(false) // Start out with empty sheet content
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = sheetState,
+                dragHandle = null,
+            ) {
+                if (hasSheetContent) {
+                    Box(Modifier.fillMaxHeight(0.4f))
+                }
+            }
+        }
+
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
+        assertThat(sheetState.swipeableState.hasAnchorForValue(SheetValue.Collapsed))
+            .isFalse()
+        assertThat(sheetState.swipeableState.hasAnchorForValue(SheetValue.Expanded))
+            .isFalse()
+
+        scope.launch { sheetState.show() }
+        rule.waitForIdle()
+
+        assertThat(sheetState.isVisible).isTrue()
+        assertThat(sheetState.currentValue).isEqualTo(sheetState.targetValue)
+
+        hasSheetContent = true // Recompose with sheet content
+        rule.waitForIdle()
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+    }
+
+    @Test
+    fun modalBottomSheet_tallSheet_anchorChangeHandler_previousTargetNotInAnchors_reconciles() {
+        val sheetState = SheetState(skipCollapsed = false)
+        var hasSheetContent by mutableStateOf(false) // Start out with empty sheet content
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            ModalBottomSheet(
+                onDismissRequest = {},
+                sheetState = sheetState,
+                dragHandle = null,
+            ) {
+                if (hasSheetContent) {
+                    Box(Modifier.fillMaxHeight(0.6f))
+                }
+            }
+        }
+
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
+        assertThat(sheetState.swipeableState.hasAnchorForValue(SheetValue.Collapsed))
+            .isFalse()
+        assertThat(sheetState.swipeableState.hasAnchorForValue(SheetValue.Expanded))
+            .isFalse()
+
+        scope.launch { sheetState.show() }
+        rule.waitForIdle()
+
+        assertThat(sheetState.isVisible).isTrue()
+        assertThat(sheetState.currentValue).isEqualTo(sheetState.targetValue)
+
+        hasSheetContent = true // Recompose with sheet content
+        rule.waitForIdle()
+        assertThat(sheetState.currentValue).isEqualTo(SheetValue.Collapsed)
+    }
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TimePickerTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TimePickerTest.kt
new file mode 100644
index 0000000..92a8a0d
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TimePickerTest.kt
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import android.provider.Settings.System.TIME_12_24
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.SemanticsProperties.SelectableGroup
+import androidx.compose.ui.test.SemanticsMatcher.Companion.expectValue
+import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertAll
+import androidx.compose.ui.test.assertCountEquals
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsNotSelected
+import androidx.compose.ui.test.assertIsSelectable
+import androidx.compose.ui.test.assertIsSelected
+import androidx.compose.ui.test.filter
+import androidx.compose.ui.test.hasContentDescription
+import androidx.compose.ui.test.isFocusable
+import androidx.compose.ui.test.isSelectable
+import androidx.compose.ui.test.isSelected
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onAllNodesWithContentDescription
+import androidx.compose.ui.test.onAllNodesWithText
+import androidx.compose.ui.test.onChildren
+import androidx.compose.ui.test.onLast
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.onSiblings
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalMaterial3Api::class)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class TimePickerTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun timePicker_initialState() {
+        val state = TimePickerState(initialHour = 14, initialMinute = 23, is24Hour = false)
+        rule.setMaterialContent(lightColorScheme()) {
+            TimePicker(state)
+        }
+
+        rule.onAllNodesWithText("23").assertCountEquals(1)
+
+        rule.onNodeWithText("02").assertIsSelected()
+
+        rule.onNodeWithText("AM").assertExists()
+
+        rule.onNodeWithText("PM").assertExists().assertIsSelected()
+    }
+
+    @Test
+    fun timePicker_switchToMinutes() {
+        val state = TimePickerState(initialHour = 14, initialMinute = 23, is24Hour = false)
+        rule.setMaterialContent(lightColorScheme()) {
+            TimePicker(state)
+        }
+
+        rule.onNodeWithText("23").performClick()
+
+        rule.onNodeWithText("55").assertExists()
+    }
+
+    @Test
+    fun timePicker_selectHour() {
+        val state = TimePickerState(initialHour = 14, initialMinute = 23, is24Hour = false)
+        rule.setMaterialContent(lightColorScheme()) {
+            TimePicker(state)
+        }
+
+        rule.onNodeWithText("6").performClick()
+
+        // shows 06 in display
+        rule.onNodeWithText("06").assertExists()
+
+        // switches to minutes
+        rule.onNodeWithText("23").assertIsSelected()
+
+        // state updated
+        assertThat(state.hour).isEqualTo(18)
+    }
+
+    @Test
+    fun timePicker_switchToAM() {
+        val state = TimePickerState(initialHour = 14, initialMinute = 23, is24Hour = false)
+        rule.setMaterialContent(lightColorScheme()) {
+            TimePicker(state)
+        }
+
+        assertThat(state.hour).isEqualTo(14)
+
+        rule.onNodeWithText("AM").performClick()
+
+        assertThat(state.hour).isEqualTo(2)
+    }
+
+    @Test
+    fun timePicker_dragging() {
+        val state = TimePickerState(initialHour = 0, initialMinute = 23, is24Hour = false)
+        rule.setMaterialContent(lightColorScheme()) {
+            TimePicker(state)
+        }
+
+        rule.onAllNodes(keyIsDefined(SelectableGroup), useUnmergedTree = true)
+            .onLast()
+            .performTouchInput {
+                down(topCenter)
+                // 3 O'Clock
+                moveTo(centerRight)
+                up()
+            }
+
+        rule.runOnIdle {
+            assertThat(state.hour).isEqualTo(3)
+        }
+    }
+
+    @Test
+    fun timePickerState_format_12h() {
+        lateinit var state: TimePickerState
+        getInstrumentation().uiAutomation.executeShellCommand(
+            "settings put system $TIME_12_24 12"
+        )
+        rule.setContent {
+            state = rememberTimePickerState()
+        }
+
+        assertThat(state.is24hour).isFalse()
+    }
+
+    @Test
+    fun timePickerState_format_24h() {
+        lateinit var state: TimePickerState
+        getInstrumentation().uiAutomation.executeShellCommand(
+            "settings put system $TIME_12_24 24"
+        )
+        rule.setContent {
+            state = rememberTimePickerState()
+        }
+
+        assertThat(state.is24hour).isTrue()
+    }
+
+    @Test
+    fun timePicker_toggle_semantics() {
+        val state = TimePickerState(initialHour = 14, initialMinute = 23, is24Hour = false)
+        lateinit var contentDescription: String
+        rule.setMaterialContent(lightColorScheme()) {
+            contentDescription = getString(Strings.TimePickerPeriodToggle)
+            TimePicker(state)
+        }
+
+        rule.onNodeWithContentDescription(contentDescription)
+            .onChildren()
+            .assertAll(isSelectable())
+    }
+
+    @Test
+    fun timePicker_display_semantics() {
+        val state = TimePickerState(initialHour = 14, initialMinute = 23, is24Hour = false)
+        lateinit var minuteDescription: String
+        lateinit var hourDescription: String
+        rule.setMaterialContent(lightColorScheme()) {
+            minuteDescription = getString(Strings.TimePickerMinuteSelection)
+            hourDescription = getString(Strings.TimePickerHourSelection)
+            TimePicker(state)
+        }
+
+        rule.onNodeWithContentDescription(minuteDescription)
+            .assertIsSelectable()
+            .assertIsNotSelected()
+            .assert(expectValue(SemanticsProperties.Role, Role.RadioButton))
+            .assertHasClickAction()
+
+        rule.onNodeWithContentDescription(hourDescription)
+            .assertIsSelectable()
+            .assertIsSelected()
+            .assert(expectValue(SemanticsProperties.Role, Role.RadioButton))
+            .assertHasClickAction()
+    }
+
+    @Test
+    fun timePicker_clockFace_hour_semantics() {
+        val state = TimePickerState(initialHour = 14, initialMinute = 23, is24Hour = false)
+        lateinit var hourDescription: String
+
+        rule.setMaterialContent(lightColorScheme()) {
+            hourDescription = getString(Strings.TimePickerHourSuffix, 2)
+            TimePicker(state)
+        }
+
+        rule.onAllNodesWithContentDescription(hourDescription)
+            .onLast()
+            .onSiblings()
+            .filter(isFocusable())
+            .assertCountEquals(11)
+            .assertAll(
+                hasContentDescription(
+                    value = "o'clock",
+                    substring = true,
+                    ignoreCase = true
+                )
+            )
+    }
+
+    @Test
+    fun timePicker_clockFace_selected_semantics() {
+        val state = TimePickerState(initialHour = 14, initialMinute = 23, is24Hour = true)
+
+        rule.setMaterialContent(lightColorScheme()) {
+            TimePicker(state)
+        }
+
+        rule.onAllNodesWithText("14")
+            .assertAll(isSelected())
+    }
+
+    @Test
+    fun timePicker_clockFace_minutes_semantics() {
+        val state = TimePickerState(initialHour = 14, initialMinute = 23, is24Hour = false)
+        lateinit var minuteDescription: String
+
+        rule.setMaterialContent(lightColorScheme()) {
+            minuteDescription = getString(Strings.TimePickerMinuteSuffix, 55)
+            TimePicker(state)
+        }
+
+        // Switch to minutes
+        rule.onNodeWithText("23").performClick()
+
+        rule.waitForIdle()
+
+        rule.onNodeWithContentDescription(minuteDescription)
+            .assertExists()
+            .onSiblings()
+            .assertCountEquals(11)
+            .assertAll(
+                hasContentDescription(
+                    value = "minutes",
+                    substring = true,
+                    ignoreCase = true
+                )
+            )
+    }
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipScreenshotTest.kt
index 49505df..eafc433 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipScreenshotTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 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.
@@ -18,6 +18,7 @@
 
 import android.os.Build
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Modifier
@@ -45,24 +46,34 @@
     @get:Rule
     val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
 
-    private val tooltipState = TooltipState()
-
     @Test
     fun plainTooltip_lightTheme() {
-        rule.setMaterialContent(lightColorScheme()) { TestTooltips() }
+        rule.setMaterialContent(lightColorScheme()) { TestPlainTooltips() }
         assertAgainstGolden("plainTooltip_lightTheme")
     }
 
     @Test
     fun plainTooltip_darkTheme() {
-        rule.setMaterialContent(darkColorScheme()) { TestTooltips() }
+        rule.setMaterialContent(darkColorScheme()) { TestPlainTooltips() }
         assertAgainstGolden("plainTooltip_darkTheme")
     }
 
-    @Composable
-    private fun TestTooltips() {
-        val scope = rememberCoroutineScope()
+    @Test
+    fun richTooltip_lightTheme() {
+        rule.setMaterialContent(lightColorScheme()) { TestRichTooltips() }
+        assertAgainstGolden("richTooltip_lightTheme")
+    }
 
+    @Test
+    fun richTooltip_darkTheme() {
+        rule.setMaterialContent(darkColorScheme()) { TestRichTooltips() }
+        assertAgainstGolden("richTooltip_darkTheme")
+    }
+
+    @Composable
+    private fun TestPlainTooltips() {
+        val scope = rememberCoroutineScope()
+        val tooltipState = remember { PlainTooltipState() }
         PlainTooltipBox(
             tooltip = { Text("Tooltip Text") },
             modifier = Modifier.testTag(TooltipTestTag),
@@ -72,6 +83,26 @@
         scope.launch { tooltipState.show() }
     }
 
+    @Composable
+    private fun TestRichTooltips() {
+        val scope = rememberCoroutineScope()
+        val tooltipState = remember { RichTooltipState() }
+        RichTooltipBox(
+            title = { Text("Title") },
+            text = {
+                Text(
+                    "Area for supportive text, providing a descriptive " +
+                        "message for the composable that the tooltip is anchored to."
+                )
+            },
+            action = { Text("Action Text") },
+            tooltipState = tooltipState,
+            modifier = Modifier.testTag(TooltipTestTag)
+        ) {}
+
+        scope.launch { tooltipState.show() }
+    }
+
     private fun assertAgainstGolden(goldenName: String) {
         rule.onNodeWithTag(TooltipTestTag)
             .captureToImage()
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..666507d 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
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 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,6 +22,7 @@
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
@@ -29,15 +30,18 @@
 import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
 import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
 import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.click
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.longClick
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.launch
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -50,11 +54,9 @@
     @get:Rule
     val rule = createComposeRule()
 
-    private val tooltipState = TooltipState()
-
     @Test
     fun plainTooltip_noContent_size() {
-        rule.setMaterialContent(lightColorScheme()) { TestTooltip() }
+        rule.setMaterialContent(lightColorScheme()) { TestPlainTooltip() }
 
         rule.onNodeWithTag(ContainerTestTag)
             .assertHeightIsEqualTo(TooltipMinHeight)
@@ -62,12 +64,20 @@
     }
 
     @Test
+    fun richTooltip_noContent_size() {
+        rule.setMaterialContent(lightColorScheme()) { TestRichTooltip() }
+        rule.onNodeWithTag(ContainerTestTag)
+            .assertHeightIsEqualTo(TooltipMinHeight)
+            .assertWidthIsEqualTo(TooltipMinWidth)
+    }
+
+    @Test
     fun plainTooltip_customSize_size() {
         val customWidth = 100.dp
         val customHeight = 100.dp
 
         rule.setMaterialContent(lightColorScheme()) {
-            TestTooltip(modifier = Modifier.size(customWidth, customHeight))
+            TestPlainTooltip(modifier = Modifier.size(customWidth, customHeight))
         }
 
         rule.onNodeWithTag(ContainerTestTag)
@@ -75,11 +85,25 @@
             .assertWidthIsEqualTo(customWidth)
     }
 
-    @FlakyTest(bugId = 264907895)
+    @Test
+    fun richTooltip_customSize_size() {
+        val customWidth = 100.dp
+        val customHeight = 100.dp
+
+        rule.setMaterialContent(lightColorScheme()) {
+            TestRichTooltip(modifier = Modifier.size(customWidth, customHeight))
+        }
+
+        rule.onNodeWithTag(ContainerTestTag)
+            .assertHeightIsEqualTo(customHeight)
+            .assertWidthIsEqualTo(customWidth)
+    }
+
+    @Ignore // b/264907895
     @Test
     fun plainTooltip_content_padding() {
         rule.setMaterialContent(lightColorScheme()) {
-            TestTooltip(
+            TestPlainTooltip(
                 tooltipContent = {
                     Text(
                         text = "Test",
@@ -94,33 +118,149 @@
             .assertTopPositionInRootIsEqualTo(4.dp)
     }
 
-    @FlakyTest(bugId = 264887805)
+    @Test
+    fun richTooltip_content_padding() {
+        rule.setMaterialContent(lightColorScheme()) {
+            TestRichTooltip(
+                title = { Text(text = "Subhead", modifier = Modifier.testTag(SubheadTestTag)) },
+                text = { Text(text = "Text", modifier = Modifier.testTag(TextTestTag)) },
+                action = { Text(text = "Action", modifier = Modifier.testTag(ActionTestTag)) },
+            )
+        }
+
+        val subhead = rule.onNodeWithTag(SubheadTestTag)
+        val text = rule.onNodeWithTag(TextTestTag)
+
+        val subheadBaseline = subhead.getFirstBaselinePosition()
+        val textBaseLine = text.getFirstBaselinePosition()
+
+        val subheadBound = subhead.getUnclippedBoundsInRoot()
+        val textBound = text.getUnclippedBoundsInRoot()
+
+        rule.onNodeWithTag(SubheadTestTag)
+            .assertLeftPositionInRootIsEqualTo(RichTooltipHorizontalPadding)
+            .assertTopPositionInRootIsEqualTo(28.dp - subheadBaseline)
+
+        rule.onNodeWithTag(TextTestTag)
+            .assertLeftPositionInRootIsEqualTo(RichTooltipHorizontalPadding)
+            .assertTopPositionInRootIsEqualTo(subheadBound.bottom + 24.dp - textBaseLine)
+
+        rule.onNodeWithTag(ActionTestTag)
+            .assertLeftPositionInRootIsEqualTo(RichTooltipHorizontalPadding)
+            .assertTopPositionInRootIsEqualTo(textBound.bottom + 16.dp)
+    }
+
     @Test
     fun plainTooltip_behavior() {
+        val tooltipState = PlainTooltipState()
         rule.setMaterialContent(lightColorScheme()) {
             PlainTooltipBox(
                 tooltip = { Text(text = "Test", modifier = Modifier.testTag(TextTestTag)) },
                 tooltipState = tooltipState,
                 modifier = Modifier.testTag(ContainerTestTag)
-            ) { Anchor() }
+            ) { Anchor(tooltipState) }
         }
 
         // Tooltip should initially be not visible
-        assert(!tooltipState.isVisible)
+        assertThat(tooltipState.isVisible).isFalse()
 
-        // Long press the icon and check that the tooltip is now showing
+        // Test will manually advance the time to check the timeout
+        rule.mainClock.autoAdvance = false
+
+        // Long press the icon
         rule.onNodeWithTag(AnchorTestTag)
             .performTouchInput { longClick() }
 
-        assert(tooltipState.isVisible)
+        // Check that the tooltip is now showing
+        rule.waitForIdle()
+        assertThat(tooltipState.isVisible).isTrue()
 
         // Tooltip should dismiss itself after 1.5s
-        rule.waitUntil(TooltipDuration + 100L) { !tooltipState.isVisible }
+        rule.mainClock.advanceTimeBy(milliseconds = TooltipDuration)
+        rule.waitForIdle()
+        assertThat(tooltipState.isVisible).isFalse()
+    }
+
+    @Test
+    fun richTooltip_behavior_noAction() {
+        val tooltipState = RichTooltipState()
+        rule.setMaterialContent(lightColorScheme()) {
+            RichTooltipBox(
+                title = { Text(text = "Subhead", modifier = Modifier.testTag(SubheadTestTag)) },
+                text = { Text(text = "Text", modifier = Modifier.testTag(TextTestTag)) },
+                tooltipState = tooltipState,
+                modifier = Modifier.testTag(ContainerTestTag)
+            ) { Anchor(tooltipState) }
+        }
+
+        // Tooltip should initially be not visible
+        assertThat(tooltipState.isVisible).isFalse()
+
+        // Test will manually advance the time to check the timeout
+        rule.mainClock.autoAdvance = false
+
+        // Long press the icon
+        rule.onNodeWithTag(AnchorTestTag)
+            .performTouchInput { longClick() }
+
+        // Check that the tooltip is now showing
+        rule.waitForIdle()
+        assertThat(tooltipState.isVisible).isTrue()
+
+        // Tooltip should dismiss itself after 1.5s
+        rule.mainClock.advanceTimeBy(milliseconds = TooltipDuration)
+        rule.waitForIdle()
+        assertThat(tooltipState.isVisible).isFalse()
+    }
+
+    @Test
+    fun richTooltip_behavior_persistent() {
+        val tooltipState = RichTooltipState()
+        rule.setMaterialContent(lightColorScheme()) {
+            val scope = rememberCoroutineScope()
+            RichTooltipBox(
+                title = { Text(text = "Subhead", modifier = Modifier.testTag(SubheadTestTag)) },
+                text = { Text(text = "Text", modifier = Modifier.testTag(TextTestTag)) },
+                action = {
+                    TextButton(
+                        onClick = { scope.launch { tooltipState.dismiss() } },
+                        modifier = Modifier.testTag(ActionTestTag)
+                    ) { Text(text = "Action") }
+                },
+                tooltipState = tooltipState,
+                modifier = Modifier.testTag(ContainerTestTag)
+            ) { Anchor(tooltipState) }
+        }
+
+        // Tooltip should initially be not visible
+        assertThat(tooltipState.isVisible).isFalse()
+
+        // Test will manually advance the time to check the timeout
+        rule.mainClock.autoAdvance = false
+
+        // Long press the icon
+        rule.onNodeWithTag(AnchorTestTag)
+            .performTouchInput { longClick() }
+
+        // Check that the tooltip is now showing
+        rule.waitForIdle()
+        assertThat(tooltipState.isVisible).isTrue()
+
+        // Tooltip should still be visible after the normal TooltipDuration, since we have an action.
+        rule.mainClock.advanceTimeBy(milliseconds = TooltipDuration)
+        rule.waitForIdle()
+        assertThat(tooltipState.isVisible).isTrue()
+
+        // Click the action and check that it closed the tooltip
+        rule.onNodeWithTag(ActionTestTag)
+            .performTouchInput { click() }
+        assertThat(tooltipState.isVisible).isFalse()
     }
 
     @Composable
-    fun TestTooltip(
+    private fun TestPlainTooltip(
         modifier: Modifier = Modifier,
+        tooltipState: PlainTooltipState = remember { PlainTooltipState() },
         tooltipContent: @Composable () -> Unit = {}
     ) {
         val scope = rememberCoroutineScope()
@@ -134,9 +274,32 @@
         scope.launch { tooltipState.show() }
     }
 
+    @Composable
+    private fun TestRichTooltip(
+        modifier: Modifier = Modifier,
+        tooltipState: RichTooltipState = remember { RichTooltipState() },
+        text: @Composable () -> Unit = {},
+        action: (@Composable () -> Unit)? = null,
+        title: (@Composable () -> Unit)? = null
+    ) {
+        val scope = rememberCoroutineScope()
+
+        RichTooltipBox(
+            text = text,
+            action = action,
+            title = title,
+            modifier = modifier.testTag(ContainerTestTag),
+            tooltipState = tooltipState
+        ) {}
+
+        scope.launch { tooltipState.show() }
+    }
+
     @OptIn(ExperimentalFoundationApi::class)
     @Composable
-    fun Anchor() {
+    private fun Anchor(
+        tooltipState: TooltipState
+    ) {
         val scope = rememberCoroutineScope()
 
         Icon(
@@ -156,6 +319,8 @@
     }
 }
 
-private const val AnchorTestTag = "Anchor"
 private const val ContainerTestTag = "Container"
-private const val TextTestTag = "Text"
\ No newline at end of file
+private const val TextTestTag = "Text"
+private const val SubheadTestTag = "Subhead"
+private const val ActionTestTag = "Action"
+private const val AnchorTestTag = "Anchor'"
\ No newline at end of file
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModel.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModel.android.kt
index ff4fb83..1c3aae9 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModel.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModel.android.kt
@@ -17,15 +17,59 @@
 package androidx.compose.material3
 
 import android.os.Build
+import android.text.format.DateFormat
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.core.os.ConfigurationCompat
+import java.util.Locale
 
 /**
- * Creates a [CalendarModel] to be used by the date picker.
+ * Returns a [CalendarModel] to be used by the date picker.
  */
 @ExperimentalMaterial3Api
-internal actual fun createCalendarModel(): CalendarModel {
+internal actual fun CalendarModel(): CalendarModel {
     return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
         CalendarModelImpl()
     } else {
         LegacyCalendarModelImpl()
     }
 }
+
+/**
+ * Formats a UTC timestamp into a string with a given date format skeleton.
+ *
+ * A skeleton is similar to, and uses the same format characters as described in
+ * [Unicode Technical Standard #35](https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)
+ *
+ * One difference is that order is irrelevant. For example, "MMMMd" will return "MMMM d" in the
+ * en_US locale, but "d. MMMM" in the de_CH locale.
+ *
+ * @param utcTimeMillis a UTC timestamp to format (milliseconds from epoch)
+ * @param skeleton a date format skeleton
+ * @param locale the [Locale] to use when formatting the given timestamp
+ */
+@ExperimentalMaterial3Api
+internal actual fun formatWithSkeleton(
+    utcTimeMillis: Long,
+    skeleton: String,
+    locale: Locale
+): String {
+    val pattern = DateFormat.getBestDateTimePattern(locale, skeleton)
+    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        CalendarModelImpl.formatWithPattern(utcTimeMillis, pattern, locale)
+    } else {
+        LegacyCalendarModelImpl.formatWithPattern(utcTimeMillis, pattern, locale)
+    }
+}
+
+/**
+ * A composable function that returns the default [Locale]. It will be recomposed when the
+ * `Configuration` gets updated.
+ */
+@Composable
+@ReadOnlyComposable
+@ExperimentalMaterial3Api
+internal actual fun defaultLocale(): Locale {
+    return ConfigurationCompat.getLocales(LocalConfiguration.current).get(0) ?: Locale.getDefault()
+}
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModelImpl.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModelImpl.android.kt
index 044d662..f9eed30 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModelImpl.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModelImpl.android.kt
@@ -69,15 +69,16 @@
             }
         }
 
-    override val dateInputFormat: DateInputFormat
-        get() = datePatternAsInputFormat(
+    override fun getDateInputFormat(locale: Locale): DateInputFormat {
+        return datePatternAsInputFormat(
             DateTimeFormatterBuilder.getLocalizedDateTimePattern(
                 /* dateStyle = */ FormatStyle.SHORT,
                 /* timeStyle = */ null,
-                /* chrono = */ Chronology.ofLocale(Locale.getDefault()),
-                /* locale = */ Locale.getDefault()
+                /* chrono = */ Chronology.ofLocale(locale),
+                /* locale = */ locale
             )
         )
+    }
 
     override fun getCanonicalDate(timeInMillis: Long): CalendarDate {
         val localDate =
@@ -128,17 +129,8 @@
         return getMonth(earlierMonth)
     }
 
-    override fun format(month: CalendarMonth, pattern: String): String {
-        val formatter: DateTimeFormatter =
-            DateTimeFormatter.ofPattern(pattern).withDecimalStyle(DecimalStyle.ofDefaultLocale())
-        return month.toLocalDate().format(formatter)
-    }
-
-    override fun format(date: CalendarDate, pattern: String): String {
-        val formatter: DateTimeFormatter =
-            DateTimeFormatter.ofPattern(pattern).withDecimalStyle(DecimalStyle.ofDefaultLocale())
-        return date.toLocalDate().format(formatter)
-    }
+    override fun formatWithPattern(utcTimeMillis: Long, pattern: String, locale: Locale): String =
+        CalendarModelImpl.formatWithPattern(utcTimeMillis, pattern, locale)
 
     override fun parse(date: String, pattern: String): CalendarDate? {
         // TODO: A DateTimeFormatter can be reused.
@@ -157,6 +149,32 @@
         }
     }
 
+    companion object {
+
+        /**
+         * Formats a UTC timestamp into a string with a given date format pattern.
+         *
+         * @param utcTimeMillis a UTC timestamp to format (milliseconds from epoch)
+         * @param pattern a date format pattern
+         * @param locale the [Locale] to use when formatting the given timestamp
+         */
+        fun formatWithPattern(utcTimeMillis: Long, pattern: String, locale: Locale): String {
+            val formatter: DateTimeFormatter =
+                DateTimeFormatter.ofPattern(pattern, locale)
+                    .withDecimalStyle(DecimalStyle.of(locale))
+            return Instant
+                .ofEpochMilli(utcTimeMillis)
+                .atZone(utcTimeZoneId)
+                .toLocalDate()
+                .format(formatter)
+        }
+
+        /**
+         * Holds a UTC [ZoneId].
+         */
+        internal val utcTimeZoneId: ZoneId = ZoneId.of("UTC")
+    }
+
     private fun getMonth(firstDayLocalDate: LocalDate): CalendarMonth {
         val difference = firstDayLocalDate.dayOfWeek.value - firstDayOfWeek
         val daysFromStartOfWeekToFirstOfMonth = if (difference < 0) {
@@ -187,6 +205,4 @@
             this.dayOfMonth
         )
     }
-
-    private val utcTimeZoneId = ZoneId.of("UTC")
 }
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DatePickerDialog.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DatePickerDialog.android.kt
index cee3ad1..f77adef 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DatePickerDialog.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DatePickerDialog.android.kt
@@ -25,6 +25,7 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.requiredWidth
 import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material3.tokens.DatePickerModalTokens
 import androidx.compose.material3.tokens.DialogTokens
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
@@ -78,11 +79,9 @@
         properties = properties
     ) {
         Surface(
-            // TODO: Use DatePickerModalTokens values for width and height after b/247694457 is
-            //  resolved.
             modifier = Modifier
-                .requiredWidth(ContainerWidth)
-                .heightIn(max = ContainerHeight),
+                .requiredWidth(DatePickerModalTokens.ContainerWidth)
+                .heightIn(max = DatePickerModalTokens.ContainerHeight),
             shape = shape,
             color = colors.containerColor,
             tonalElevation = tonalElevation,
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..0ca4a17 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
@@ -17,11 +17,15 @@
 package androidx.compose.material3
 
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.ui.R
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalContext
+import androidx.core.os.ConfigurationCompat
+import java.util.Locale
 
 @Composable
+@ReadOnlyComposable
 internal actual fun getString(string: Strings): String {
     LocalConfiguration.current
     val resources = LocalContext.current.resources
@@ -39,29 +43,113 @@
         Strings.SnackbarDismiss -> resources.getString(
             androidx.compose.material3.R.string.snackbar_dismiss
         )
+
         Strings.SearchBarSearch -> resources.getString(
             androidx.compose.material3.R.string.search_bar_search
         )
+
         Strings.SuggestionsAvailable ->
             resources.getString(androidx.compose.material3.R.string.suggestions_available)
+
         Strings.DatePickerTitle -> resources.getString(
             androidx.compose.material3.R.string.date_picker_title
         )
+
         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
         )
+
         Strings.DatePickerSwitchToDaySelection -> resources.getString(
             androidx.compose.material3.R.string.date_picker_switch_to_day_selection
         )
+
         Strings.DatePickerSwitchToNextMonth -> resources.getString(
             androidx.compose.material3.R.string.date_picker_switch_to_next_month
         )
+
         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
+        )
+        Strings.DatePickerTodayDescription -> resources.getString(
+            androidx.compose.material3.R.string.date_picker_today_description
+        )
+        Strings.DateInputTitle -> resources.getString(
+            androidx.compose.material3.R.string.date_input_title
+        )
+        Strings.DateInputHeadline -> resources.getString(
+            androidx.compose.material3.R.string.date_input_headline
+        )
+        Strings.DateInputLabel -> resources.getString(
+            androidx.compose.material3.R.string.date_input_label
+        )
+        Strings.DateInputHeadlineDescription -> resources.getString(
+            androidx.compose.material3.R.string.date_input_headline_description
+        )
+        Strings.DateInputNoInputDescription -> resources.getString(
+            androidx.compose.material3.R.string.date_input_no_input_description
+        )
+        Strings.DateInputInvalidNotAllowed -> resources.getString(
+            androidx.compose.material3.R.string.date_input_invalid_not_allowed
+        )
+        Strings.DateInputInvalidForPattern -> resources.getString(
+            androidx.compose.material3.R.string.date_input_invalid_for_pattern
+        )
+        Strings.DateInputInvalidYearRange -> resources.getString(
+            androidx.compose.material3.R.string.date_input_invalid_year_range
+        )
+        Strings.DatePickerSwitchToCalendarMode -> resources.getString(
+            androidx.compose.material3.R.string.date_picker_switch_to_calendar_mode
+        )
+        Strings.DatePickerSwitchToInputMode -> resources.getString(
+            androidx.compose.material3.R.string.date_picker_switch_to_input_mode
+        )
+        Strings.TooltipLongPressLabel -> resources.getString(
+            androidx.compose.material3.R.string.tooltip_long_press_label
+        )
+        Strings.TimePickerAM -> resources.getString(
+            androidx.compose.material3.R.string.time_picker_am)
+        Strings.TimePickerPM -> resources.getString(
+            androidx.compose.material3.R.string.time_picker_pm)
+        Strings.TimePickerPeriodToggle -> resources.getString(
+                androidx.compose.material3.R.string.time_picker_period_toggle_description)
+        Strings.TimePickerMinuteSelection -> resources.getString(
+            androidx.compose.material3.R.string.time_picker_minute_selection)
+        Strings.TimePickerHourSelection -> resources.getString(
+            androidx.compose.material3.R.string.time_picker_hour_selection)
+        Strings.TimePickerHourSuffix -> resources.getString(
+            androidx.compose.material3.R.string.time_picker_hour_suffix)
+        Strings.TimePickerMinuteSuffix -> resources.getString(
+            androidx.compose.material3.R.string.time_picker_minute_suffix)
+        Strings.TimePicker24HourSuffix -> resources.getString(
+            androidx.compose.material3.R.string.time_picker_hour_24h_suffix)
         else -> ""
     }
 }
+@Composable
+@ReadOnlyComposable
+internal actual fun getString(string: Strings, vararg formatArgs: Any): String {
+    val raw = getString(string)
+    val locale =
+        ConfigurationCompat.getLocales(LocalConfiguration.current).get(0) ?: Locale.getDefault()
+    return String.format(locale, raw, *formatArgs)
+}
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/TimeFormat.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/TimeFormat.android.kt
new file mode 100644
index 0000000..ea45cd6
--- /dev/null
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/TimeFormat.android.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import android.text.format.DateFormat.is24HourFormat
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.platform.LocalContext
+
+internal actual val is24HourFormat: Boolean
+    @Composable
+    @ReadOnlyComposable get() = is24HourFormat(LocalContext.current)
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-af/strings.xml b/compose/material3/material3/src/androidMain/res/values-af/strings.xml
index f17f7e2..e46c3dd 100644
--- a/compose/material3/material3/src/androidMain/res/values-af/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-af/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Dialoog"</string>
     <string name="expanded" msgid="5974471714631304645">"Uitgevou"</string>
     <string name="collapsed" msgid="5389587048670450460">"Ingevou"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Maak toe"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Soek"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Voorstelle hieronder"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Kies datum"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Geselekteerde datum"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Skakel oor na kies van ’n jaar"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Skakel oor na kies van ’n dag"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Swiep om ’n jaar te kies of tik om terug te skakel om ’n dag te kies"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Verander na volgende maand"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Verander na vorige maand"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Gaan na jaar %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Huidige keuse: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Geen"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Jaarkieser sigbaar"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-am/strings.xml b/compose/material3/material3/src/androidMain/res/values-am/strings.xml
index caebbc7..f8327988 100644
--- a/compose/material3/material3/src/androidMain/res/values-am/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-am/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"መገናኛ"</string>
     <string name="expanded" msgid="5974471714631304645">"ተዘርግቷል"</string>
     <string name="collapsed" msgid="5389587048670450460">"ተሰብስቧል"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"አሰናብት"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"ፍለጋ"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"የአስተያየት ጥቆማዎች ከታች"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"ቀን ይምረጡ"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"የተመረጠው ቀን"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"ወደ ዓመት መምረጥ ቀይር"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"ወደ ቀን መምረጥ ቀይር"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"ዓመት ለመምረጥ ያንሸራትቱ ወይም ወደ ቀንን መምረጥ መልሶ ለመቀየር መታ ያድርጉ"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"ወደ የሚቀጥለው ወር ቀይር"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"ወደ ቀዳሚው ወር ቀይር"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"ወደ ዓመት %1$s ያስሱ"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"የአሁን ምርጫ፦ %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"ምንም"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"ዓመት መራጭ ይታያል"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-ar/strings.xml b/compose/material3/material3/src/androidMain/res/values-ar/strings.xml
index 7d6ce36..f783a0f 100644
--- a/compose/material3/material3/src/androidMain/res/values-ar/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-ar/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"مربّع حوار"</string>
     <string name="expanded" msgid="5974471714631304645">"موسَّع"</string>
     <string name="collapsed" msgid="5389587048670450460">"مصغَّر"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"إغلاق"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"بحث"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"إليك الاقتراحات:"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"اختيار تاريخ"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"التاريخ المحدَّد"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"التبديل لاختيار سنة"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"التبديل لاختيار يوم"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"مرِّر سريعًا لتحديد عام، أو انقر للرجوع إلى تحديد يوم."</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"التغيير إلى الشهر التالي"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"التغيير إلى الشهر السابق"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"‏الانتقال إلى عام %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"‏التحديد الحالي: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"بدون تاريخ"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"أداة اختيار الأعوام مرئية"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-as/strings.xml b/compose/material3/material3/src/androidMain/res/values-as/strings.xml
index 05565c2..3615dbf 100644
--- a/compose/material3/material3/src/androidMain/res/values-as/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-as/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ডায়ল’গ"</string>
     <string name="expanded" msgid="5974471714631304645">"বিস্তাৰ কৰা আছে"</string>
     <string name="collapsed" msgid="5389587048670450460">"সংকোচন কৰা আছে"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"অগ্ৰাহ্য কৰক"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"সন্ধান কৰক"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"তলত পৰামৰ্শ দেখুওৱা হৈছে"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"তাৰিখ বাছনি কৰক"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"বাছনি কৰা তাৰিখ"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"বছৰ বাছনি কৰাৰ ছুইচ"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"দিন বাছনি কৰাৰ ছুইচ"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"এটা বছৰ বাছনি কৰিবলৈ ছোৱাইপ কৰক অথবা এটা দিন বাছনি কৰাৰ সুবিধাটোলৈ উভতি যাবলৈ টিপক"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"পৰৱৰ্তী মাহলৈ সলনি কৰক"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"পূৰ্বৱৰ্তী মাহলৈ সলনি কৰক"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"বৰ্ষ %1$sলৈ নেভিগে’ট কৰক"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"বৰ্তমানৰ বাছনি: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"একো নাই"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"বছৰ বাছনিকৰ্তা দৃশ্যমান"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-az/strings.xml b/compose/material3/material3/src/androidMain/res/values-az/strings.xml
index 6fe56b0..0473c2c 100644
--- a/compose/material3/material3/src/androidMain/res/values-az/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-az/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Dialoq"</string>
     <string name="expanded" msgid="5974471714631304645">"Genişləndirilib"</string>
     <string name="collapsed" msgid="5389587048670450460">"Yığcamlaşdırılıb"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Qapadın"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Axtarış"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Təkliflər aşağıdadır"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Tarix seçin"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Seçilmiş tarix"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"İl seçiminə keçin"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Gün seçiminə keçin"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"İl seçmək üçün sürüşdürün və ya gün seçiminə qayıtmaq üçün toxunun"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Növbəti aya dəyişin"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Əvvəlki aya dəyişin"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Bu ilə keçin: %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Cari seçim: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Heç bir"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"İl seçicisi görünür"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-b+sr+Latn/strings.xml b/compose/material3/material3/src/androidMain/res/values-b+sr+Latn/strings.xml
index c7adcd8..c9ac82a 100644
--- a/compose/material3/material3/src/androidMain/res/values-b+sr+Latn/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-b+sr+Latn/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Dijalog"</string>
     <string name="expanded" msgid="5974471714631304645">"Prošireno je"</string>
     <string name="collapsed" msgid="5389587048670450460">"Skupljeno je"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Odbacite"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Pretraga"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Predlozi su u nastavku"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Izaberite datum"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Izabrani datum"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Pređite na izbor godine"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Pređite na izbor dana"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Prevucite da biste izabrali godinu ili dodirnite da biste se vratili na izbor dana"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Pređite na sledeći mesec"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Pređite na prethodni mesec"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Idite na godinu: %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Aktuelni izbor: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Ništa"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Vidljiv birač godina"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-be/strings.xml b/compose/material3/material3/src/androidMain/res/values-be/strings.xml
index 576a74f..ce5f78d 100644
--- a/compose/material3/material3/src/androidMain/res/values-be/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-be/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Дыялогавае акно"</string>
     <string name="expanded" msgid="5974471714631304645">"Разгорнута"</string>
     <string name="collapsed" msgid="5389587048670450460">"Згорнута"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Закрыць"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Пошук"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Прапановы ўнізе"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Выберыце дату"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Выбраная дата"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Перайсці да выбару года"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Перайсці да выбару дня"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Правядзіце пальцам, каб выбраць год, або націсніце, каб вярнуцца да выбару даты"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Перайсці да наступнага месяца"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Перайсці да папярэдняга месяца"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Перайсці ў год %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Бягучы выбар: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Не выбрана"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Бачны інструмент выбару года"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-bg/strings.xml b/compose/material3/material3/src/androidMain/res/values-bg/strings.xml
index 54d365c..08e44f8 100644
--- a/compose/material3/material3/src/androidMain/res/values-bg/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-bg/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Диалогов прозорец"</string>
     <string name="expanded" msgid="5974471714631304645">"Разгънато"</string>
     <string name="collapsed" msgid="5389587048670450460">"Свито"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Отхвърляне"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Търсене"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Предложенията са по-долу"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Избиране на дата"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Избрана дата"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Превключване към избиране на година"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Превключване към избиране на ден"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Прекарайте пръст, за да изберете година, или докоснете, за да се върнете към избора на ден"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Преминаване към следващия месец"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Преминаване към предишния месец"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Навигиране до %1$s година"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Текущ избор: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Без"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Инструментът за избор на година е видим"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-bn/strings.xml b/compose/material3/material3/src/androidMain/res/values-bn/strings.xml
index acf1a6a..6d4ef6e 100644
--- a/compose/material3/material3/src/androidMain/res/values-bn/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-bn/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ডায়ালগ বক্স"</string>
     <string name="expanded" msgid="5974471714631304645">"বড় করা হয়েছে"</string>
     <string name="collapsed" msgid="5389587048670450460">"আড়াল করা হয়েছে"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"বাতিল করুন"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"সার্চ করুন"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"নিচে দেওয়া সাজেশন"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"তারিখ বেছে নিন"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"বেছে নেওয়া তারিখ"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"কোনও একটি বছর বেছে নিতে পাল্টে নিন"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"কোনও একটি দিন বেছে নিতে পাল্টে নিন"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"একটি বছর বেছে নিতে সোয়াইপ করুন অথবা কোনও একটি দিন বাছতে ফিরে গিয়ে সুইচে ট্যাপ করুন"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"আগামী মাসে পরিবর্তন করুন"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"আগের মাসে পরিবর্তন করুন"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"নেভিগেট করে %1$s বছরে যান"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"বর্তমানে বেছে নেওয়া হয়েছে: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"কোনওটিই নয়"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"বছর বেছে নেওয়ার তালিকা দেখা যাচ্ছে"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-bs/strings.xml b/compose/material3/material3/src/androidMain/res/values-bs/strings.xml
index 0d4a8ec..8c24d35 100644
--- a/compose/material3/material3/src/androidMain/res/values-bs/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-bs/strings.xml
@@ -20,11 +20,26 @@
     <string name="dialog" msgid="4057925834421392736">"Dijaloški okvir"</string>
     <string name="expanded" msgid="5974471714631304645">"Prošireno"</string>
     <string name="collapsed" msgid="5389587048670450460">"Suženo"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Odbacivanje"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Pretraživanje"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Prijedlozi su u nastavku"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Odabir datuma"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Odabrani datum"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Prebaci na odabir godine"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Prebaci na odabir dana"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Prevucite da odaberete godinu ili dodirnite da se vratite na odabir dana"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Promijeni na sljedeći mjesec"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Promijeni na prethodni mjesec"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Odlazak na %1$s. godinu"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Trenutni odabir: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Ništa"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Birač godine je vidljiv"</string>
+    <string name="date_input_title" msgid="3010396677286327048">"Odaberite datum"</string>
+    <string name="date_input_headline" msgid="3499643850558715142">"Datum unosa"</string>
+    <string name="date_input_label" msgid="5194825853981987218">"Datum"</string>
+    <string name="date_input_headline_description" msgid="8562356184193964298">"Datum unosa: %1$s"</string>
+    <string name="date_input_no_input_description" msgid="5722931102250207748">"Ništa"</string>
+    <string name="date_input_invalid_not_allowed" msgid="6114792992433444995">"Datum nije dopušten: %1$s"</string>
+    <string name="date_input_invalid_for_pattern" msgid="5281836720766682161">"Datum se ne podudara s očekivanim uzorkom: %1$s"</string>
+    <string name="date_input_invalid_year_range" msgid="8434112129235255568">"Datum je izvan očekivanog raspona godine %1$s – %2$s"</string>
+    <string name="tooltip_long_press_label" msgid="2732804537909054941">"Prikaži opis"</string>
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-ca/strings.xml b/compose/material3/material3/src/androidMain/res/values-ca/strings.xml
index 1db45f4..33269eb 100644
--- a/compose/material3/material3/src/androidMain/res/values-ca/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-ca/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Diàleg"</string>
     <string name="expanded" msgid="5974471714631304645">"S\'ha desplegat"</string>
     <string name="collapsed" msgid="5389587048670450460">"S\'ha replegat"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Ignora"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Cerca"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Suggeriments a continuació"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Selecciona la data"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Data seleccionada"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Canvia a la selecció de l\'any"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Canvia a la selecció del dia"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Llisca per seleccionar un any o toca per tornar a seleccionar un dia"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Canvia al mes següent"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Canvia al mes anterior"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Navega fins a l\'any %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Selecció actual: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Cap"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Selector d\'any visible"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-cs/strings.xml b/compose/material3/material3/src/androidMain/res/values-cs/strings.xml
index eaef71a..9f7ed3f 100644
--- a/compose/material3/material3/src/androidMain/res/values-cs/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-cs/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Dialogové okno"</string>
     <string name="expanded" msgid="5974471714631304645">"Rozbaleno"</string>
     <string name="collapsed" msgid="5389587048670450460">"Sbaleno"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Zavřít"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Hledat"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Návrh je níže"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Vybrat datum"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Vybrané datum"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Přepnout na výběr roku"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Přepnout na výběr dne"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Přejetím prstem vyberte rok nebo se klepnutím vraťte k výběru dne"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Přejít na další měsíc"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Přejít na předchozí měsíc"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Přejít na rok %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Aktuální výběr: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Žádné"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Je vidět výběr roku"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-da/strings.xml b/compose/material3/material3/src/androidMain/res/values-da/strings.xml
index 7f5f7bfa..7c05fee 100644
--- a/compose/material3/material3/src/androidMain/res/values-da/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-da/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Dialogboks"</string>
     <string name="expanded" msgid="5974471714631304645">"Udvidet"</string>
     <string name="collapsed" msgid="5389587048670450460">"Skjult"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Afvis"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Søg"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Forslag nedenfor"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Vælg dato"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Valgt dato"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Skift til valg af år"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Skift til valg af dag"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Stryg for at vælge et år, eller tryk for at skifte tilbage til datovælgeren"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Skift til næste måned"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Skift til forrige måned"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Naviger til år %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Aktuelt valg: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Ingen"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Årsvælgeren er synlig"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-de/strings.xml b/compose/material3/material3/src/androidMain/res/values-de/strings.xml
index e211bf1..ab55db2 100644
--- a/compose/material3/material3/src/androidMain/res/values-de/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-de/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Dialogfeld"</string>
     <string name="expanded" msgid="5974471714631304645">"Maximiert"</string>
     <string name="collapsed" msgid="5389587048670450460">"Minimiert"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Schließen"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Suchen"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Vorschläge unten"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Datum auswählen"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Ausgewähltes Datum"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Zur Jahresauswahl wechseln"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Zur Tagesauswahl wechseln"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Wischen, um ein Jahr auszuwählen, oder tippen, um zur Tagesauswahl zurückzukehren"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Zum nächsten Monat wechseln"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Zum vorherigen Monat wechseln"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Zum Jahr %1$s wechseln"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Aktuelle Auswahl: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Keine"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Jahresauswahl sichtbar"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-el/strings.xml b/compose/material3/material3/src/androidMain/res/values-el/strings.xml
index ce1c470..24ada47 100644
--- a/compose/material3/material3/src/androidMain/res/values-el/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-el/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Παράθυρο διαλόγου"</string>
     <string name="expanded" msgid="5974471714631304645">"Ανεπτυγμένο"</string>
     <string name="collapsed" msgid="5389587048670450460">"Συμπτυγμένο"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Παράβλεψη"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Αναζήτηση"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Προτάσεις παρακάτω"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Επιλογή ημερομηνίας"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Επιλεγμένη ημερομηνία"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Εναλλαγή σε επιλογή έτους"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Εναλλαγή σε επιλογή ημερομηνίας"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Σύρετε για να επιλέξετε ένα έτος ή πατήστε για να επιστρέψετε στην επιλογή ημέρας."</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Αλλαγή στον επόμενο μήνα"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Αλλαγή στον προηγούμενο μήνα"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Μετάβαση στο έτος %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Τρέχουσα επιλογή: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Καμία"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Το εργαλείο επιλογής έτους είναι ορατό"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-en-rAU/strings.xml b/compose/material3/material3/src/androidMain/res/values-en-rAU/strings.xml
index 596e8f24..b67749f 100644
--- a/compose/material3/material3/src/androidMain/res/values-en-rAU/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-en-rAU/strings.xml
@@ -20,11 +20,26 @@
     <string name="dialog" msgid="4057925834421392736">"Dialogue"</string>
     <string name="expanded" msgid="5974471714631304645">"Expanded"</string>
     <string name="collapsed" msgid="5389587048670450460">"Collapsed"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Dismiss"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Search"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Suggestions below"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Select date"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Selected date"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Switch to selecting a year"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Switch to selecting a day"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Swipe to select a year, or tap to switch back to selecting a day"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Change to next month"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Change to previous month"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Navigate to year %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Current selection: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"None"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Year picker visible"</string>
+    <string name="date_input_title" msgid="3010396677286327048">"Select date"</string>
+    <string name="date_input_headline" msgid="3499643850558715142">"Entered date"</string>
+    <string name="date_input_label" msgid="5194825853981987218">"Date"</string>
+    <string name="date_input_headline_description" msgid="8562356184193964298">"Entered date: %1$s"</string>
+    <string name="date_input_no_input_description" msgid="5722931102250207748">"None"</string>
+    <string name="date_input_invalid_not_allowed" msgid="6114792992433444995">"Date not allowed: %1$s"</string>
+    <string name="date_input_invalid_for_pattern" msgid="5281836720766682161">"Date does not match expected pattern: %1$s"</string>
+    <string name="date_input_invalid_year_range" msgid="8434112129235255568">"Date out of expected year range %1$s – %2$s"</string>
+    <string name="tooltip_long_press_label" msgid="2732804537909054941">"Show tooltip"</string>
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-en-rCA/strings.xml b/compose/material3/material3/src/androidMain/res/values-en-rCA/strings.xml
index 4ee174d..4f43a60 100644
--- a/compose/material3/material3/src/androidMain/res/values-en-rCA/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-en-rCA/strings.xml
@@ -20,11 +20,26 @@
     <string name="dialog" msgid="4057925834421392736">"Dialog"</string>
     <string name="expanded" msgid="5974471714631304645">"Expanded"</string>
     <string name="collapsed" msgid="5389587048670450460">"Collapsed"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Dismiss"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Search"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Suggestions below"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Select date"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Selected date"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Switch to selecting a year"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Switch to selecting a day"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Swipe to select a year, or tap to switch back to selecting a day"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Change to next month"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Change to previous month"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Navigate to year %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Current selection: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"None"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Year picker visible"</string>
+    <string name="date_input_title" msgid="3010396677286327048">"Select date"</string>
+    <string name="date_input_headline" msgid="3499643850558715142">"Entered date"</string>
+    <string name="date_input_label" msgid="5194825853981987218">"Date"</string>
+    <string name="date_input_headline_description" msgid="8562356184193964298">"Entered date: %1$s"</string>
+    <string name="date_input_no_input_description" msgid="5722931102250207748">"None"</string>
+    <string name="date_input_invalid_not_allowed" msgid="6114792992433444995">"Date not allowed: %1$s"</string>
+    <string name="date_input_invalid_for_pattern" msgid="5281836720766682161">"Date does not match expected pattern: %1$s"</string>
+    <string name="date_input_invalid_year_range" msgid="8434112129235255568">"Date out of expected year range %1$s - %2$s"</string>
+    <string name="tooltip_long_press_label" msgid="2732804537909054941">"Show tooltip"</string>
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-en-rGB/strings.xml b/compose/material3/material3/src/androidMain/res/values-en-rGB/strings.xml
index 596e8f24..b67749f 100644
--- a/compose/material3/material3/src/androidMain/res/values-en-rGB/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-en-rGB/strings.xml
@@ -20,11 +20,26 @@
     <string name="dialog" msgid="4057925834421392736">"Dialogue"</string>
     <string name="expanded" msgid="5974471714631304645">"Expanded"</string>
     <string name="collapsed" msgid="5389587048670450460">"Collapsed"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Dismiss"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Search"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Suggestions below"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Select date"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Selected date"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Switch to selecting a year"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Switch to selecting a day"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Swipe to select a year, or tap to switch back to selecting a day"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Change to next month"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Change to previous month"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Navigate to year %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Current selection: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"None"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Year picker visible"</string>
+    <string name="date_input_title" msgid="3010396677286327048">"Select date"</string>
+    <string name="date_input_headline" msgid="3499643850558715142">"Entered date"</string>
+    <string name="date_input_label" msgid="5194825853981987218">"Date"</string>
+    <string name="date_input_headline_description" msgid="8562356184193964298">"Entered date: %1$s"</string>
+    <string name="date_input_no_input_description" msgid="5722931102250207748">"None"</string>
+    <string name="date_input_invalid_not_allowed" msgid="6114792992433444995">"Date not allowed: %1$s"</string>
+    <string name="date_input_invalid_for_pattern" msgid="5281836720766682161">"Date does not match expected pattern: %1$s"</string>
+    <string name="date_input_invalid_year_range" msgid="8434112129235255568">"Date out of expected year range %1$s – %2$s"</string>
+    <string name="tooltip_long_press_label" msgid="2732804537909054941">"Show tooltip"</string>
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-en-rIN/strings.xml b/compose/material3/material3/src/androidMain/res/values-en-rIN/strings.xml
index 596e8f24..b67749f 100644
--- a/compose/material3/material3/src/androidMain/res/values-en-rIN/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-en-rIN/strings.xml
@@ -20,11 +20,26 @@
     <string name="dialog" msgid="4057925834421392736">"Dialogue"</string>
     <string name="expanded" msgid="5974471714631304645">"Expanded"</string>
     <string name="collapsed" msgid="5389587048670450460">"Collapsed"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Dismiss"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Search"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Suggestions below"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Select date"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Selected date"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Switch to selecting a year"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Switch to selecting a day"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Swipe to select a year, or tap to switch back to selecting a day"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Change to next month"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Change to previous month"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Navigate to year %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Current selection: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"None"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Year picker visible"</string>
+    <string name="date_input_title" msgid="3010396677286327048">"Select date"</string>
+    <string name="date_input_headline" msgid="3499643850558715142">"Entered date"</string>
+    <string name="date_input_label" msgid="5194825853981987218">"Date"</string>
+    <string name="date_input_headline_description" msgid="8562356184193964298">"Entered date: %1$s"</string>
+    <string name="date_input_no_input_description" msgid="5722931102250207748">"None"</string>
+    <string name="date_input_invalid_not_allowed" msgid="6114792992433444995">"Date not allowed: %1$s"</string>
+    <string name="date_input_invalid_for_pattern" msgid="5281836720766682161">"Date does not match expected pattern: %1$s"</string>
+    <string name="date_input_invalid_year_range" msgid="8434112129235255568">"Date out of expected year range %1$s – %2$s"</string>
+    <string name="tooltip_long_press_label" msgid="2732804537909054941">"Show tooltip"</string>
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-en-rXC/strings.xml b/compose/material3/material3/src/androidMain/res/values-en-rXC/strings.xml
index d691ec7..5d47ca6 100644
--- a/compose/material3/material3/src/androidMain/res/values-en-rXC/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-en-rXC/strings.xml
@@ -20,11 +20,26 @@
     <string name="dialog" msgid="4057925834421392736">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‎‎‎‏‎‏‎‎‎‎‏‎‏‎‎‏‏‎‎‎‎‎‏‎‏‏‏‏‏‎‏‏‎‏‏‎‎‏‎‎‎‏‎‏‎‏‎‏‎‏‎‏‏‎‎‎‎‎‎Dialog‎‏‎‎‏‎"</string>
     <string name="expanded" msgid="5974471714631304645">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‏‏‎‏‎‎‏‏‎‎‏‎‏‏‎‎‏‎‏‏‏‏‎‏‏‎‎‏‏‏‏‎‎‏‏‎‏‏‏‏‏‎‎‎‏‎‏‏‏‎‎‎‏‎‏‎Expanded‎‏‎‎‏‎"</string>
     <string name="collapsed" msgid="5389587048670450460">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‏‎‏‏‎‎‏‎‏‏‏‎‏‎‏‎‎‎‏‏‎‏‏‏‎‏‏‎‎‏‎‎‏‎‎‎‎‎‏‏‎‎‎‏‎‏‎‎‏‏‎‎‎‏‏‏‎‎‎Collapsed‎‏‎‎‏‎"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‏‎‏‏‏‏‏‏‏‏‎‎‏‎‏‎‏‏‎‏‏‎‎‎‏‏‏‏‎‏‏‏‏‎‏‏‏‎‏‏‎‎‏‏‎‏‎‎‏‎‏‏‏‏‏‏‎‏‎Dismiss‎‏‎‎‏‎"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‏‎‎‎‏‏‎‎‎‎‏‏‏‏‏‎‎‏‏‎‏‎‏‏‎‎‏‎‎‎‎‎‏‎‎‏‎‏‎‏‎‎‎‎‎‏‏‎‏‏‎‏‎‏‎‏‎‎Search‎‏‎‎‏‎"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‎‏‏‏‏‎‎‎‏‏‏‏‎‎‏‏‏‎‏‎‏‎‏‏‎‏‏‎‎‏‏‎‏‏‎‎‏‏‏‎‎‏‏‏‎‏‎‎‏‏‎‏‎‎‏‏‏‏‎‎Suggestions below‎‏‎‎‏‎"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‎‎‏‎‏‏‏‏‏‏‎‎‏‎‏‏‏‏‎‏‏‎‏‎‎‎‎‎‏‏‎‏‎‏‎‎‎‎‎‎‏‏‎‏‏‎‏‎‎‏‎‎‎‎‎Select date‎‏‎‎‏‎"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‏‏‏‎‎‎‎‎‎‏‏‏‎‎‏‏‏‏‎‎‎‎‎‏‏‎‎‏‎‏‏‏‏‎‎‏‏‎‏‎‏‎‎‏‎‎‎‎‏‏‏‎‏‎‎‎‎‏‎Selected date‎‏‎‎‏‎"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‏‏‏‎‏‎‏‏‎‏‏‎‎‏‎‏‏‎‎‎‏‏‏‎‏‎‎‎‎‎‏‏‎‏‏‎‎‎‏‎‎‎‎‏‎‎‏‏‎‎‏‏‏‎‏‏‏‎‏‎Switch to selecting a year‎‏‎‎‏‎"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‎‎‏‏‎‎‎‏‏‏‏‏‎‎‎‎‎‎‏‎‎‎‎‏‏‏‏‏‏‎‎‎‏‏‎‎‏‎‏‎‏‏‏‎‏‏‏‏‎‎‎‏‎‏‏‏‎‏‎Switch to selecting a day‎‏‎‎‏‎"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‎‏‎‎‎‎‎‎‎‏‏‎‏‏‏‎‏‏‎‎‎‎‎‎‎‎‎‏‎‏‏‎‎‎‎‏‏‎‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‏‎‏‏‎Swipe to select a year, or tap to switch back to selecting a day‎‏‎‎‏‎"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‏‏‎‏‏‎‎‎‎‎‎‏‏‏‏‏‎‏‏‏‎‎‎‎‏‎‏‏‎‏‎‏‏‎‎‏‏‏‏‎‏‎‎‏‏‏‎‎‏‏‎‎‎‎‎‏‏‎‎Change to next month‎‏‎‎‏‎"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‏‎‏‎‏‏‎‏‏‏‎‏‎‏‏‎‎‎‏‎‎‏‎‏‏‏‎‏‏‎‎‏‏‏‎‏‏‎‎‏‏‎‏‎‏‎‏‏‎‎‎‎‎‏‎Change to previous month‎‏‎‎‏‎"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‏‏‎‏‎‎‏‎‏‎‎‏‎‎‎‎‏‎‏‏‎‏‏‏‏‏‎‏‏‏‎‎‏‎‏‎‎‏‎‏‏‎‎‎Navigate to year %1$s‎‏‎‎‏‎"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‎‎‎‎‏‏‎‏‏‏‎‏‏‏‏‏‏‏‎‎‎‏‎‎‏‏‎‎‏‏‏‎‏‎‏‎‏‏‏‏‏‎‎‏‏‎‎‏‏‏‏‎‏‏‏‏‎‏‎Current selection: %1$s‎‏‎‎‏‎"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‏‎‏‏‏‎‎‎‏‎‎‎‏‎‎‏‎‏‎‏‎‏‎‎‎‎‎‎‎‏‎‎‏‏‎‏‎‎‎‎‏‎‏‎‎‏‎‎‏‏‏‎‎‏‎‏‏‎None‎‏‎‎‏‎"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‏‏‏‎‎‎‎‎‏‏‏‏‏‎‎‎‏‏‎‎‏‎‏‎‎‎‏‏‎‏‎‏‎‎‏‎‎‎‏‎‎‏‏‏‎‏‎‏‎‎‎‎‎‎‎Year picker visible‎‏‎‎‏‎"</string>
+    <string name="date_input_title" msgid="3010396677286327048">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‎‏‏‏‎‎‎‏‏‏‎‎‎‏‎‎‏‏‏‏‎‏‎‏‎‎‎‎‎‎‎‏‏‎‏‎‏‎‏‎‏‎‏‏‏‏‎‎‏‏‎‎‎‎‏‎‎‎‎Select date‎‏‎‎‏‎"</string>
+    <string name="date_input_headline" msgid="3499643850558715142">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‎‎‏‎‎‏‎‎‎‏‎‎‏‏‏‎‏‏‏‎‎‎‏‎‏‎‎‏‎‎‏‎‎‎‏‏‏‎‏‏‏‎‏‎‏‏‎‏‎‏‎‎‎‎‎‏‏‎‎Entered date‎‏‎‎‏‎"</string>
+    <string name="date_input_label" msgid="5194825853981987218">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‎‎‎‎‏‎‏‏‏‏‎‏‏‏‎‏‎‏‎‎‏‎‏‏‏‎‏‎‎‎‎‎‎‏‎‎‏‏‏‎‎‏‎‎‏‎‎‎‏‏‎‎‏‎‎‏‎‎Date‎‏‎‎‏‎"</string>
+    <string name="date_input_headline_description" msgid="8562356184193964298">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‏‎‏‏‎‏‎‎‏‏‏‎‎‏‏‎‎‏‏‎‎‏‎‏‎‏‏‎‎‎‏‏‏‏‎‏‎‎‎‎‎‎‎‎‎‏‏‎‎‏‎‎‎‎‏‎‏‎‎Entered date: %1$s‎‏‎‎‏‎"</string>
+    <string name="date_input_no_input_description" msgid="5722931102250207748">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‏‎‏‏‎‏‎‏‏‏‏‏‎‏‏‏‏‏‎‎‎‎‏‎‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‎‎‏‎‎‎‏‎‏‎‎‎‎‎‎‏‎‎‎None‎‏‎‎‏‎"</string>
+    <string name="date_input_invalid_not_allowed" msgid="6114792992433444995">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‎‎‏‏‎‏‏‏‎‎‎‎‎‏‏‎‏‏‏‏‎‏‎‏‎‎‎‎‏‏‏‏‎‏‎‎‎‎‏‎‏‏‎‎‎‏‏‎‎‎‏‎‎‎‎‎‏‏‎Date not allowed: %1$s‎‏‎‎‏‎"</string>
+    <string name="date_input_invalid_for_pattern" msgid="5281836720766682161">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‏‎‏‎‎‏‏‎‎‏‏‎‏‏‎‏‎‏‎‎‎‎‎‏‏‎‏‎‏‏‏‏‎‏‏‏‎‏‎‎‏‏‎‎‎‎‏‎‎‎‎‏‏‎‎‎‏‎Date does not match expected pattern: %1$s‎‏‎‎‏‎"</string>
+    <string name="date_input_invalid_year_range" msgid="8434112129235255568">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‏‎‎‎‎‏‎‏‏‏‏‏‏‏‏‎‎‎‏‎‎‏‎‏‏‏‎‏‏‎‎‏‎‏‏‏‎‎‎‎‏‏‎‎‎‎‎‎‏‎‎‎‏‎‎‎‎‎Date out of expected year range %1$s - %2$s‎‏‎‎‏‎"</string>
+    <string name="tooltip_long_press_label" msgid="2732804537909054941">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‎‏‏‏‏‎‏‏‎‎‏‏‎‏‏‏‏‏‎‎‏‏‏‏‏‏‏‎‎‎‏‎‏‎‏‎‏‎‎‏‎‏‎‎‎‎‏‏‎‏‏‏‎‏‏‏‎‏‎Show tooltip‎‏‎‎‏‎"</string>
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-es-rUS/strings.xml b/compose/material3/material3/src/androidMain/res/values-es-rUS/strings.xml
index f9ea7a7..816a701 100644
--- a/compose/material3/material3/src/androidMain/res/values-es-rUS/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-es-rUS/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Diálogo"</string>
     <string name="expanded" msgid="5974471714631304645">"Expandido"</string>
     <string name="collapsed" msgid="5389587048670450460">"Contraído"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Descartar"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Buscar"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Sugerencias a continuación"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Seleccionar fecha"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Fecha seleccionada"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Cambiar a seleccionar un año"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Cambiar a seleccionar un día"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Desliza el dedo para elegir un año o presiona para volver a seleccionar un día"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Cambiar al mes siguiente"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Cambiar al mes anterior"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Navegar al año %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Selección actual: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Nada"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Selector de año visible"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-es/strings.xml b/compose/material3/material3/src/androidMain/res/values-es/strings.xml
index 60e5df3..f3e7902 100644
--- a/compose/material3/material3/src/androidMain/res/values-es/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-es/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Cuadro de diálogo"</string>
     <string name="expanded" msgid="5974471714631304645">"Desplegado"</string>
     <string name="collapsed" msgid="5389587048670450460">"Contraído"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Cerrar"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Buscar"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Sugerencias a continuación"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Seleccionar fecha"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Fecha seleccionada"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Cambiar para seleccionar un año"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Cambiar para seleccionar un día"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Desliza el dedo para seleccionar un año o toca para volver a seleccionar un día"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Cambiar al mes siguiente"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Cambiar al mes anterior"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Ir al año %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Selección: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Ninguno"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Selector de año visible"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-et/strings.xml b/compose/material3/material3/src/androidMain/res/values-et/strings.xml
index c420378..834e1de 100644
--- a/compose/material3/material3/src/androidMain/res/values-et/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-et/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Dialoog"</string>
     <string name="expanded" msgid="5974471714631304645">"Laiendatud"</string>
     <string name="collapsed" msgid="5389587048670450460">"Ahendatud"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Loobu"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Otsing"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Soovitused on allpool"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Valige kuupäev"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Valitud kuupäev"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Lülitu aasta valimisele"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Lülitu päeva valimisele"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Pühkige aasta valimiseks või puudutage, et minna tagasi päeva valimise juurde"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Vaheta järgmisele kuule"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Vaheta eelmisele kuule"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Liigu aasta %1$s juurde"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Praegune valik: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Pole"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Aasta valija on nähtav"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-eu/strings.xml b/compose/material3/material3/src/androidMain/res/values-eu/strings.xml
index e3b168c..f879ef1 100644
--- a/compose/material3/material3/src/androidMain/res/values-eu/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-eu/strings.xml
@@ -20,11 +20,26 @@
     <string name="dialog" msgid="4057925834421392736">"Leihoa"</string>
     <string name="expanded" msgid="5974471714631304645">"Zabalduta"</string>
     <string name="collapsed" msgid="5389587048670450460">"Tolestuta"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Baztertu"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Bilaketa"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Iradokizunak daude behean"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Hautatu data bat"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Hautatutako data"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Joan urte-hautatzailera"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Joan egun-hautatzailera"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Pasatu hatza urte bat hautatzeko. Bestela, sakatu hau eguna hautatzeko pantailara itzultzeko."</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Aldatu hurrengo hilabetera"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Aldatu aurreko hilabetera"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Joan %1$s urtera"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Oraingo hautapena: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Bat ere ez"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Urte-hautatzailea ikusgai dago"</string>
+    <string name="date_input_title" msgid="3010396677286327048">"Hautatu data bat"</string>
+    <string name="date_input_headline" msgid="3499643850558715142">"Idatzitako data"</string>
+    <string name="date_input_label" msgid="5194825853981987218">"Data"</string>
+    <string name="date_input_headline_description" msgid="8562356184193964298">"Idatzitako data: %1$s"</string>
+    <string name="date_input_no_input_description" msgid="5722931102250207748">"Bat ere ez"</string>
+    <string name="date_input_invalid_not_allowed" msgid="6114792992433444995">"Ez da onartzen data: %1$s"</string>
+    <string name="date_input_invalid_for_pattern" msgid="5281836720766682161">"Data ez dator bat espero den ereduarekin: %1$s"</string>
+    <string name="date_input_invalid_year_range" msgid="8434112129235255568">"Espero den urte tartetik (%1$s-%2$s) kanpo dago data"</string>
+    <string name="tooltip_long_press_label" msgid="2732804537909054941">"Erakutsi aholkua"</string>
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-fa/strings.xml b/compose/material3/material3/src/androidMain/res/values-fa/strings.xml
index c2742d5..e31b5a8 100644
--- a/compose/material3/material3/src/androidMain/res/values-fa/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-fa/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"کادر گفتگو"</string>
     <string name="expanded" msgid="5974471714631304645">"ازهم بازشده"</string>
     <string name="collapsed" msgid="5389587048670450460">"جمع‌شده"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"بستن"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"جستجو"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"پیشنهادهای زیر"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"انتخاب تاریخ"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"تاریخ انتخابی"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"رفتن به انتخاب سال"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"رفتن به انتخاب روز"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"برای انتخاب سال، تند بکشید یا برای برگشتن به انتخاب روز، ضربه بزنید"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"تغییر به ماه بعدی"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"تغییر به ماه قبلی"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"‏پیمایش به سال %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"‏انتخاب فعلی: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"هیچ‌کدام"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"انتخابگر سال نمایان است"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-fi/strings.xml b/compose/material3/material3/src/androidMain/res/values-fi/strings.xml
index 54abb11..0a5817a 100644
--- a/compose/material3/material3/src/androidMain/res/values-fi/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-fi/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Valintaikkuna"</string>
     <string name="expanded" msgid="5974471714631304645">"Laajennettu"</string>
     <string name="collapsed" msgid="5389587048670450460">"Tiivistetty"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Hylkää"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Hae"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Ehdotuksia alla"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Valitse päivämäärä"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Valittu päivämäärä"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Vaihda vuoden valintaan"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Vaihda päivämäärän valintaan"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Valitse vuosi pyyhkäisemällä tai palaa päivän valintaan napauttamalla"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Vaihda seuraavaan kuukauteen"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Vaihda edelliseen kuukauteen"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Siirry vuoteen %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Nykyinen valinta: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"–"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Vuosivalitsin näkyvillä"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-fr-rCA/strings.xml b/compose/material3/material3/src/androidMain/res/values-fr-rCA/strings.xml
index 34105f8..0ec8d9b 100644
--- a/compose/material3/material3/src/androidMain/res/values-fr-rCA/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-fr-rCA/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Dialogue"</string>
     <string name="expanded" msgid="5974471714631304645">"Développé"</string>
     <string name="collapsed" msgid="5389587048670450460">"Réduit"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Fermer"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Recherche"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Suggestions ci-dessous"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Sélectionnez une date"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Date sélectionnée"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Passer à la sélection d\'une année"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Passer à la sélection d\'un jour"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Balayez l\'écran pour sélectionner une année, ou touchez pour revenir en arrière et sélectionner un jour"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Passer au mois suivant"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Passer au mois précédent"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Naviguez jusqu\'à l\'année %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Sélection actuelle : %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Aucune"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Sélecteur d\'année visible"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-fr/strings.xml b/compose/material3/material3/src/androidMain/res/values-fr/strings.xml
index 6703adf..ab3346f 100644
--- a/compose/material3/material3/src/androidMain/res/values-fr/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-fr/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Boîte de dialogue"</string>
     <string name="expanded" msgid="5974471714631304645">"Développé"</string>
     <string name="collapsed" msgid="5389587048670450460">"Réduit"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Ignorer"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Rechercher"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Suggestions ci-dessous"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Sélectionner une date"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Date sélectionnée"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Passer à la sélection d\'une année"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Passer à la sélection d\'un jour"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Balayez l\'écran pour sélectionner une année ou appuyez pour revenir à la sélection d\'un jour"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Passer au mois suivant"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Passer au mois précédent"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Accéder à l\'année %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Sélection actuelle : %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Aucune"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Sélecteur d\'année visible"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-gl/strings.xml b/compose/material3/material3/src/androidMain/res/values-gl/strings.xml
index 61ccee8..111bc92 100644
--- a/compose/material3/material3/src/androidMain/res/values-gl/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-gl/strings.xml
@@ -20,11 +20,40 @@
     <string name="dialog" msgid="4057925834421392736">"Cadro de diálogo"</string>
     <string name="expanded" msgid="5974471714631304645">"Despregado"</string>
     <string name="collapsed" msgid="5389587048670450460">"Contraído"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Pechar"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Busca"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Hai suxestións abaixo"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Selecciona a data"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Data seleccionada"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Cambiar a seleccionar un ano"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Cambiar a seleccionar un día"</string>
+    <!-- no translation found for date_picker_switch_to_day_selection (145089358343568971) -->
+    <skip />
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Cambiar ao mes seguinte"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Cambiar ao mes anterior"</string>
+    <!-- no translation found for date_picker_navigate_to_year_description (5152441868029453612) -->
+    <skip />
+    <!-- no translation found for date_picker_headline_description (4627306862713137085) -->
+    <skip />
+    <!-- no translation found for date_picker_no_selection_description (5724377114289981899) -->
+    <skip />
+    <!-- no translation found for date_picker_year_picker_pane_title (8140324713311804736) -->
+    <skip />
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-gu/strings.xml b/compose/material3/material3/src/androidMain/res/values-gu/strings.xml
index 23098b9..dd3faef 100644
--- a/compose/material3/material3/src/androidMain/res/values-gu/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-gu/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"સંવાદ બૉક્સ"</string>
     <string name="expanded" msgid="5974471714631304645">"મોટી કરેલી"</string>
     <string name="collapsed" msgid="5389587048670450460">"નાની કરેલી"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"છોડી દો"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"શોધો"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"સૂચનો નીચે છે"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"તારીખ પસંદ કરો"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"પસંદ કરેલી તારીખ"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"વર્ષ પસંદ કરવાના વિકલ્પ પર સ્વિચ કરો"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"દિવસ પસંદ કરવાના વિકલ્પ પર સ્વિચ કરો"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"વર્ષ પસંદ કરવા માટે સ્વાઇપ કરો અથવા દિવસની પસંદગી પર પાછા સ્વિચ કરવા માટે ટૅપ કરો"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"બદલીને આગલો મહિનો પસંદ કરો"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"બદલીને પાછલો મહિનો પસંદ કરો"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"વર્ષ %1$s પર નૅવિગેટ કરો"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"હાલની પસંદગી: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"એકપણ નહીં"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"વર્ષ માટેનું પિકર દૃશ્યમાન છે"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-hi/strings.xml b/compose/material3/material3/src/androidMain/res/values-hi/strings.xml
index ea69e79..f699414 100644
--- a/compose/material3/material3/src/androidMain/res/values-hi/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-hi/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"डायलॉग"</string>
     <string name="expanded" msgid="5974471714631304645">"बड़ा किया गया"</string>
     <string name="collapsed" msgid="5389587048670450460">"छोटा किया गया"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"खारिज करें"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"खोजें"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"सुझाव यहां मौजूद हैं"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"तारीख चुनें"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"चुनी गई तारीख"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"साल चुनने के लिए स्विच करें"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"दिन चुनने के लिए स्विच करें"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"साल चुनने के लिए स्वाइप करें या दिन चुनने पर वापस स्विच करने लिए टैप करें"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"अगले महीने पर जाएं"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"पिछले महीने पर जाएं"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"साल %1$s पर जाएं"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"फ़िलहाल, यह चुना गया है: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"कोई नहीं"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"साल चुनने का विकल्प दिख रहा है"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-hr/strings.xml b/compose/material3/material3/src/androidMain/res/values-hr/strings.xml
index 038c64a..3b70225 100644
--- a/compose/material3/material3/src/androidMain/res/values-hr/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-hr/strings.xml
@@ -20,11 +20,26 @@
     <string name="dialog" msgid="4057925834421392736">"Dijaloški okvir"</string>
     <string name="expanded" msgid="5974471714631304645">"Prošireno"</string>
     <string name="collapsed" msgid="5389587048670450460">"Sažeto"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Odbaci"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Pretraživanje"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Prijedlozi su u nastavku"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Odaberite datum"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Odabrani datum"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Prijelaz na odabir godine"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Prijelaz na odabir dana"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Pomaknite se za odabir godine ili dodirnite za povratak na odabir dana"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Pomicanje na sljedeći mjesec"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Pomicanje na prethodni mjesec"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Prelazak u godinu %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Trenutačni odabir: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Ništa"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Vidljiv je alat za odabir godine"</string>
+    <string name="date_input_title" msgid="3010396677286327048">"Odaberite datum"</string>
+    <string name="date_input_headline" msgid="3499643850558715142">"Datum unosa"</string>
+    <string name="date_input_label" msgid="5194825853981987218">"Datum"</string>
+    <string name="date_input_headline_description" msgid="8562356184193964298">"Datum unosa: %1$s"</string>
+    <string name="date_input_no_input_description" msgid="5722931102250207748">"Ništa"</string>
+    <string name="date_input_invalid_not_allowed" msgid="6114792992433444995">"Datum nije dopušten: %1$s"</string>
+    <string name="date_input_invalid_for_pattern" msgid="5281836720766682161">"Datum se ne podudara s očekivanim uzorkom: %1$s"</string>
+    <string name="date_input_invalid_year_range" msgid="8434112129235255568">"Datum je izvan očekivanog raspona godine %1$s – %2$s"</string>
+    <string name="tooltip_long_press_label" msgid="2732804537909054941">"Prikaži opis"</string>
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-hu/strings.xml b/compose/material3/material3/src/androidMain/res/values-hu/strings.xml
index 8f033c5..55ef980b 100644
--- a/compose/material3/material3/src/androidMain/res/values-hu/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-hu/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Párbeszédablak"</string>
     <string name="expanded" msgid="5974471714631304645">"Kibontva"</string>
     <string name="collapsed" msgid="5389587048670450460">"Összecsukva"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Elvetés"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Keresés"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Javaslatok alább"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Dátum kiválasztása"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Kiválasztott dátum"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Váltson a kívánt év kiválasztásához"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Váltson a kívánt nap kiválasztásához"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Csúsztatással kiválaszthatja a kívánt évet, vagy koppintással visszaválthat a nap kiválasztásához."</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Váltás a következő hónapra"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Váltás az előző hónapra"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Navigálás a következő évhez: %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Jelenleg kiválasztva: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Nincs"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Látható az évválasztó"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-hy/strings.xml b/compose/material3/material3/src/androidMain/res/values-hy/strings.xml
index eea216f..8f81997 100644
--- a/compose/material3/material3/src/androidMain/res/values-hy/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-hy/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Երկխոսության պատուհան"</string>
     <string name="expanded" msgid="5974471714631304645">"Ծավալված է"</string>
     <string name="collapsed" msgid="5389587048670450460">"Ծալված է"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Փակել"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Որոնում"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Առաջարկները հասանելի են ստորև"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Ընտրեք ամսաթիվը"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Ընտրված ամսաթիվ"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Անցնել տարվա ընտրությանը"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Անցնել օրվա ընտրությանը"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Սահեցրեք՝ տարեթիվ ընտրելու համար, կամ հպեք՝ օրվա ընտրությանը վերադառնալու համար"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Անցնել հաջորդ ամսվան"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Անցնել նախորդ ամսվան"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Անցնել %1$s թվական"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Ընթացիկ ընտրությունը՝ %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Ոչ մեկը"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Տարեթվի ցուցադրվող ընտրիչ"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-in/strings.xml b/compose/material3/material3/src/androidMain/res/values-in/strings.xml
index 2361441..7f7eae1 100644
--- a/compose/material3/material3/src/androidMain/res/values-in/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-in/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Dialog"</string>
     <string name="expanded" msgid="5974471714631304645">"Diluaskan"</string>
     <string name="collapsed" msgid="5389587048670450460">"Diciutkan"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Tutup"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Telusuri"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Saran di bawah"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Pilih tanggal"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Tanggal yang dipilih"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Beralih ke memilih tahun"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Beralih ke memilih hari"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Geser untuk memilih tahun, atau ketuk untuk beralih kembali ke pemilihan tanggal"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Ubah ke bulan berikutnya"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Ubah ke bulan sebelumnya"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Pilih tahun %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Pilihan saat ini: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Tidak ada"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Pemilih tahun terlihat"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-is/strings.xml b/compose/material3/material3/src/androidMain/res/values-is/strings.xml
index d5917da..0e63211 100644
--- a/compose/material3/material3/src/androidMain/res/values-is/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-is/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Gluggi"</string>
     <string name="expanded" msgid="5974471714631304645">"Stækkað"</string>
     <string name="collapsed" msgid="5389587048670450460">"Minnkað"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Hunsa"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Leit"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Tillögur hér fyrir neðan"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Velja dagsetningu"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Valin dagsetning"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Skipta yfir í val á ári"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Skipta yfir í val á dagsetningu"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Strjúktu til að velja ár eða ýttu til að skipta aftur yfir í að velja dag"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Breyta í næsta mánuð"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Breyta í fyrri mánuð"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Fletta til ársins %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Núverandi val: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Ekkert"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Ársval birt"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-it/strings.xml b/compose/material3/material3/src/androidMain/res/values-it/strings.xml
index d24ce76..0ecf89f 100644
--- a/compose/material3/material3/src/androidMain/res/values-it/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-it/strings.xml
@@ -20,11 +20,26 @@
     <string name="dialog" msgid="4057925834421392736">"Finestra di dialogo"</string>
     <string name="expanded" msgid="5974471714631304645">"Controllo espanso"</string>
     <string name="collapsed" msgid="5389587048670450460">"Controllo compresso"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Chiudi"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Cerca"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Suggerimenti sotto"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Seleziona data"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Data selezionata"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Passa alla selezione di un anno"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Passa alla selezione di un giorno"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Scorri per selezionare un anno o tocca per tornare alla selezione di un giorno"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Passa al mese successivo"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Passa al mese precedente"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Vai all\'anno %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Selezione attuale: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Nessuna selezione"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Selettore dell\'anno visibile"</string>
+    <string name="date_input_title" msgid="3010396677286327048">"Seleziona data"</string>
+    <string name="date_input_headline" msgid="3499643850558715142">"Data inserita"</string>
+    <string name="date_input_label" msgid="5194825853981987218">"Data"</string>
+    <string name="date_input_headline_description" msgid="8562356184193964298">"Data inserita: %1$s"</string>
+    <string name="date_input_no_input_description" msgid="5722931102250207748">"Nessuna"</string>
+    <string name="date_input_invalid_not_allowed" msgid="6114792992433444995">"Data non consentita: %1$s"</string>
+    <string name="date_input_invalid_for_pattern" msgid="5281836720766682161">"La data non corrisponde al pattern previsto: %1$s"</string>
+    <string name="date_input_invalid_year_range" msgid="8434112129235255568">"La data non rientra nell\'intervallo di anni previsto (%1$s-%2$s)"</string>
+    <string name="tooltip_long_press_label" msgid="2732804537909054941">"Mostra descrizione comando"</string>
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-iw/strings.xml b/compose/material3/material3/src/androidMain/res/values-iw/strings.xml
index ab2ba8f..3224130 100644
--- a/compose/material3/material3/src/androidMain/res/values-iw/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-iw/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"תיבת דו-שיח"</string>
     <string name="expanded" msgid="5974471714631304645">"מורחב"</string>
     <string name="collapsed" msgid="5389587048670450460">"מכווץ"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"סגירה"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"חיפוש"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"הצעות מופיעות למטה"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"בחירת תאריך"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"התאריך הנבחר"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"החלפה לבחירה של שנה"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"החלפה לבחירה של יום"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"יש להחליק כדי לבחור שנה, או להקיש כדי לחזור לבחירת היום"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"מעבר לחודש הבא"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"מעבר לחודש הקודם"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"‏ניווט לשנת %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"‏הבחירה הנוכחית: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"ללא"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"בורר השנה גלוי"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-ja/strings.xml b/compose/material3/material3/src/androidMain/res/values-ja/strings.xml
index b7f6f60..c28cc19 100644
--- a/compose/material3/material3/src/androidMain/res/values-ja/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-ja/strings.xml
@@ -20,11 +20,26 @@
     <string name="dialog" msgid="4057925834421392736">"ダイアログ"</string>
     <string name="expanded" msgid="5974471714631304645">"開いています"</string>
     <string name="collapsed" msgid="5389587048670450460">"閉じています"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"閉じる"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"検索"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"検索候補は次のとおりです"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"日付を選択します"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"選択した日付"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"年の選択に移行"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"日付の選択に移行"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"スワイプして年を選択するか、タップして日付の選択に戻ります"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"翌月に変更"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"前月に変更"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"年に移動 %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"現在の選択: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"なし"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"年の選択ツールの表示"</string>
+    <string name="date_input_title" msgid="3010396677286327048">"日付を選択"</string>
+    <string name="date_input_headline" msgid="3499643850558715142">"入力された日付"</string>
+    <string name="date_input_label" msgid="5194825853981987218">"日付"</string>
+    <string name="date_input_headline_description" msgid="8562356184193964298">"入力された日付: %1$s"</string>
+    <string name="date_input_no_input_description" msgid="5722931102250207748">"なし"</string>
+    <string name="date_input_invalid_not_allowed" msgid="6114792992433444995">"許可されていない日付です: %1$s"</string>
+    <string name="date_input_invalid_for_pattern" msgid="5281836720766682161">"想定されるパターンと日付が一致しません: %1$s"</string>
+    <string name="date_input_invalid_year_range" msgid="8434112129235255568">"想定される年の範囲(%1$s~%2$s)から日付が外れています"</string>
+    <string name="tooltip_long_press_label" msgid="2732804537909054941">"ツールチップを表示します"</string>
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-ka/strings.xml b/compose/material3/material3/src/androidMain/res/values-ka/strings.xml
index 53154d3..dc3fa1f 100644
--- a/compose/material3/material3/src/androidMain/res/values-ka/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-ka/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"დიალოგი"</string>
     <string name="expanded" msgid="5974471714631304645">"გაფართოებულია"</string>
     <string name="collapsed" msgid="5389587048670450460">"ჩაკეცილი"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"დახურვა"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"ძიება"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"შემოთავაზებები იხილეთ ქვემოთ"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"აირჩიეთ თარიღი"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"არჩეული თარიღი"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"წლის არჩევაზე გადასვლა"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"დღის არჩევაზე გადასვლა"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"გადაფურცლეთ წლის ასარჩევად, ან შეხებით აირჩიეთ ისევ დღის არჩევაზე გადართვა"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"შემდეგ თვეზე გადასვლა"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"წინა თვეზე გადასვლა"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"%1$s-ზე გადასვლა"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"ამჟამინდელი არჩევანი: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"არცერთი"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"არჩეული წელი ხილულია"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-kk/strings.xml b/compose/material3/material3/src/androidMain/res/values-kk/strings.xml
index 2d22d8d..3c86ac5 100644
--- a/compose/material3/material3/src/androidMain/res/values-kk/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-kk/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Диалогтік терезе"</string>
     <string name="expanded" msgid="5974471714631304645">"Жайылды"</string>
     <string name="collapsed" msgid="5389587048670450460">"Жиылды"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Жабу"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Іздеу"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Төмендегі ұсыныстар"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Күн таңдау"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Таңдалған күн"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Жыл таңдауға өту"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Күн таңдауға өту"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Жыл таңдау үшін сырғытыңыз. Күн таңдауға ауысу үшін түртіңіз."</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Келесі айға өзгерту"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Алдыңғы айға өзгерту"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Мына жылға өту: %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Қазіргі таңдау: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Ешқандай"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Көрсетілген жыл таңдағышы"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-km/strings.xml b/compose/material3/material3/src/androidMain/res/values-km/strings.xml
index 5b84c88..af6061a 100644
--- a/compose/material3/material3/src/androidMain/res/values-km/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-km/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ប្រអប់"</string>
     <string name="expanded" msgid="5974471714631304645">"បាន​ពង្រីក"</string>
     <string name="collapsed" msgid="5389587048670450460">"បាន​បង្រួម"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"ច្រានចោល"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"ស្វែងរក"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"ការណែនាំខាងក្រោម"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"ជ្រើសរើស​កាលបរិច្ឆេទ"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"កាលបរិច្ឆេទដែលបាន​ជ្រើសរើស"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"ប្ដូរទៅ​ការជ្រើសរើសឆ្នាំ"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"ប្ដូរទៅ​ការជ្រើសរើសថ្ងៃ"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"អូសដើម្បីជ្រើសរើសឆ្នាំ ឬចុចដើម្បីប្ដូរត្រឡប់ទៅការជ្រើសរើសថ្ងៃវិញ"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"ប្ដូរ​ទៅ​ខែបន្ទាប់"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"ប្ដូរ​ទៅ​ខែមុន"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"រុករកទៅកាន់ឆ្នាំ %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"ការជ្រើសរើសបច្ចុប្បន្ន៖ %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"គ្មាន"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"អាចមើលឃើញផ្ទាំងជ្រើសរើសឆ្នាំ"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-kn/strings.xml b/compose/material3/material3/src/androidMain/res/values-kn/strings.xml
index c19db91..a146b0f 100644
--- a/compose/material3/material3/src/androidMain/res/values-kn/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-kn/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ಡೈಲಾಗ್"</string>
     <string name="expanded" msgid="5974471714631304645">"ವಿಸ್ತರಿಸಲಾಗಿದೆ"</string>
     <string name="collapsed" msgid="5389587048670450460">"ಕುಗ್ಗಿಸಲಾಗಿದೆ"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"ವಜಾಗೊಳಿಸಿ"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"ಹುಡುಕಿ"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"ಸಲಹೆಗಳನ್ನು ಕೆಳಗೆ ನೀಡಲಾಗಿದೆ"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"ದಿನಾಂಕವನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"ದಿನಾಂಕವನ್ನು ಆಯ್ಕೆಮಾಡಲಾಗಿದೆ"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"ವರ್ಷವನ್ನು ಆಯ್ಕೆ ಮಾಡಲು ಬದಲಿಸಿ"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"ದಿನವನ್ನು ಆಯ್ಕೆ ಮಾಡಲು ಬದಲಿಸಿ"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"ಒಂದು ವರ್ಷವನ್ನು ಆಯ್ಕೆಮಾಡಲು ಸ್ವೈಪ್ ಮಾಡಿ ಅಥವಾ ಒಂದು ದಿನವನ್ನು ಆಯ್ಕೆಮಾಡಲು ಹಿಂತಿರುಗಲು ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"ಮುಂದಿನ ತಿಂಗಳಿಗೆ ಬದಲಿಸಿ"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"ಹಿಂದಿನ ತಿಂಗಳಿಗೆ ಬದಲಿಸಿ"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"%1$s ವರ್ಷಕ್ಕೆ ನ್ಯಾವಿಗೇಟ್ ಮಾಡಿ"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"ಪ್ರಸ್ತುತ ಆಯ್ಕೆ: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"ಯಾವುದೂ ಅಲ್ಲ"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"ವರ್ಷದ ಪಿಕರ್ ಗೋಚರಿಸುತ್ತದೆ"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-ko/strings.xml b/compose/material3/material3/src/androidMain/res/values-ko/strings.xml
index 3fe0e18..4058800 100644
--- a/compose/material3/material3/src/androidMain/res/values-ko/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-ko/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"대화상자"</string>
     <string name="expanded" msgid="5974471714631304645">"펼침"</string>
     <string name="collapsed" msgid="5389587048670450460">"접힘"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"닫기"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"검색"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"아래의 추천 검색어"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"날짜 선택"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"선택한 날짜"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"연도 선택으로 전환"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"일자 선택으로 전환"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"스와이프하여 연도를 선택하거나 탭하여 날짜 선택으로 돌아가세요."</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"다음 달로 변경"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"이전 달로 변경"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"%1$s년으로 이동"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"현재 선택사항: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"없음"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"연도 선택 도구 표시"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-ky/strings.xml b/compose/material3/material3/src/androidMain/res/values-ky/strings.xml
index bdc280d..2a2be27 100644
--- a/compose/material3/material3/src/androidMain/res/values-ky/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-ky/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Диалог"</string>
     <string name="expanded" msgid="5974471714631304645">"Жайылып көрсөтүлдү"</string>
     <string name="collapsed" msgid="5389587048670450460">"Жыйыштырылды"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Жабуу"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Издөө"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Сунуштар төмөндө келтирилди"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Күндү тандоо"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Тандалган күн"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Жыл тандоого которулуу"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Күн тандоого которулуу"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Жылды тандоо үчүн экранды сүрүңүз же күндү тандоого кайтуу үчүн таптап коюңуз"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Кийинки айга өзгөртүү"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Мурунку айга өзгөртүү"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"%1$s-жылга өтүү"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Учурда %1$s тандалды"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Жок"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Көрсөтүлгөн жыл тандагыч"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-lo/strings.xml b/compose/material3/material3/src/androidMain/res/values-lo/strings.xml
index a30b4b37..ad23c6d 100644
--- a/compose/material3/material3/src/androidMain/res/values-lo/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-lo/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ກ່ອງໂຕ້ຕອບ"</string>
     <string name="expanded" msgid="5974471714631304645">"ຂະຫຍາຍແລ້ວ"</string>
     <string name="collapsed" msgid="5389587048670450460">"ຫຍໍ້ແລ້ວ"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"ປິດໄວ້"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"ຊອກຫາ"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"ການແນະນຳຢູ່ຂ້າງລຸ່ມ"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"ເລືອກວັນທີ"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"ວັນທີທີ່ເລືອກໄວ້"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"ປ່ຽນໄປເລືອກປີ"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"ປ່ຽນໄປເລືອກມື້"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"ປັດເພື່ອເລືອກປີ ຫຼື ແຕະເພື່ອສະຫຼັບກັບໄປຫາການເລືອກວັນ"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"ປ່ຽນເປັນເດືອນຕໍ່ໄປ"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"ປ່ຽນເປັນເດືອນຜ່ານມາ"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"ນຳທາງໄປຫາປີ %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"ການເລືອກປັດຈຸບັນ: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"ບໍ່ມີ"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"ສະແດງຕົວເລືອກປີ"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-lt/strings.xml b/compose/material3/material3/src/androidMain/res/values-lt/strings.xml
index 7da76b7..1d88730 100644
--- a/compose/material3/material3/src/androidMain/res/values-lt/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-lt/strings.xml
@@ -20,11 +20,26 @@
     <string name="dialog" msgid="4057925834421392736">"Dialogo langas"</string>
     <string name="expanded" msgid="5974471714631304645">"Išskleista"</string>
     <string name="collapsed" msgid="5389587048670450460">"Sutraukta"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Atsisakyti"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Paieška"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Pasiūlymai pateikti toliau"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Pasirinkite datą"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Pasirinkta data"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Perjungti į metų pasirinkimą"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Perjungti į dienos pasirinkimą"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Perbraukite, kad pasirinktumėte metus, arba palieskite, kad grįžtumėte ir vėl pasirinktumėte dieną"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Pakeisti į kitą mėnesį"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Pakeisti į ankstesnį mėnesį"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Eiti į %1$s m."</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Dabartinis pasirinkimas: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Nėra"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Rodomas metų parinkiklis"</string>
+    <string name="date_input_title" msgid="3010396677286327048">"Pasirinkite datą"</string>
+    <string name="date_input_headline" msgid="3499643850558715142">"Įvesta data"</string>
+    <string name="date_input_label" msgid="5194825853981987218">"Data"</string>
+    <string name="date_input_headline_description" msgid="8562356184193964298">"Įvesta data: %1$s"</string>
+    <string name="date_input_no_input_description" msgid="5722931102250207748">"Nėra"</string>
+    <string name="date_input_invalid_not_allowed" msgid="6114792992433444995">"Data neleidžiama: %1$s"</string>
+    <string name="date_input_invalid_for_pattern" msgid="5281836720766682161">"Data neatitinka numatyto šablono: %1$s"</string>
+    <string name="date_input_invalid_year_range" msgid="8434112129235255568">"Data nepatenka į numatytų metų diapazoną: %1$s–%2$s"</string>
+    <string name="tooltip_long_press_label" msgid="2732804537909054941">"Rodyti patarimą"</string>
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-lv/strings.xml b/compose/material3/material3/src/androidMain/res/values-lv/strings.xml
index 9d54644..34e073d 100644
--- a/compose/material3/material3/src/androidMain/res/values-lv/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-lv/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Dialoglodziņš"</string>
     <string name="expanded" msgid="5974471714631304645">"Izvērsts"</string>
     <string name="collapsed" msgid="5389587048670450460">"Sakļauts"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Noraidīt"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Meklēšana"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Tālāk ir sniegti ieteikumi"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Atlasīt datumu"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Atlasītais datums"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Pāriet uz gada atlasi"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Pāriet uz dienas atlasi"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Velciet, lai atlasītu gadu, vai pieskarieties, lai pārietu atpakaļ pie dienas atlases"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Mainīt uz nākamo mēnesi"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Mainīt uz iepriekšējo mēnesi"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Pāriet uz %1$s. gadu"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Pašreizējā atlase: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Nav"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Redzams gada atlasītājs"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-mk/strings.xml b/compose/material3/material3/src/androidMain/res/values-mk/strings.xml
index 43b137d..5ade032 100644
--- a/compose/material3/material3/src/androidMain/res/values-mk/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-mk/strings.xml
@@ -20,11 +20,26 @@
     <string name="dialog" msgid="4057925834421392736">"Дијалог"</string>
     <string name="expanded" msgid="5974471714631304645">"Проширено"</string>
     <string name="collapsed" msgid="5389587048670450460">"Собрано"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Отфрли"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Пребарување"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Предлозите се наведени подолу"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Изберете датум"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Избран датум"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Префрли на бирање година"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Префрли на бирање ден"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Повлечете за да изберете година или допрете за да се вратите на бирање ден"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Промени на следниот месец"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Промени на претходниот месец"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Одете на годината %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Тековен избор: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Нема"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Избирачот на година е видлив"</string>
+    <string name="date_input_title" msgid="3010396677286327048">"Изберете датум"</string>
+    <string name="date_input_headline" msgid="3499643850558715142">"Внесен датум"</string>
+    <string name="date_input_label" msgid="5194825853981987218">"Датум"</string>
+    <string name="date_input_headline_description" msgid="8562356184193964298">"Внесен датум: %1$s"</string>
+    <string name="date_input_no_input_description" msgid="5722931102250207748">"Нема"</string>
+    <string name="date_input_invalid_not_allowed" msgid="6114792992433444995">"Датумот не е дозволен: %1$s"</string>
+    <string name="date_input_invalid_for_pattern" msgid="5281836720766682161">"Не се совпаѓа со очекуваната шема: %1$s"</string>
+    <string name="date_input_invalid_year_range" msgid="8434112129235255568">"Датумот не е во очекуваниот опсег на години %1$s - %2$s"</string>
+    <string name="tooltip_long_press_label" msgid="2732804537909054941">"Прикажи совет за алатка"</string>
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-ml/strings.xml b/compose/material3/material3/src/androidMain/res/values-ml/strings.xml
index 09b2554b..9923a3b 100644
--- a/compose/material3/material3/src/androidMain/res/values-ml/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-ml/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ഡയലോഗ്"</string>
     <string name="expanded" msgid="5974471714631304645">"വിപുലീകരിച്ചത്"</string>
     <string name="collapsed" msgid="5389587048670450460">"ചുരുക്കിയത്"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"ഡിസ്‌മിസ് ചെയ്യുക"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"തിരയുക"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"നിദ്ദേശങ്ങൾ ചുവടെയുണ്ട്"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"തീയതി തിരഞ്ഞെടുക്കുക"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"തിരഞ്ഞെടുത്ത തീയതി"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"വർഷം തിരഞ്ഞെടുക്കുന്നതിലേക്ക് മാറുക"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"ദിവസം തിരഞ്ഞെടുക്കുന്നതിലേക്ക് മാറുക"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"വർഷം തിരഞ്ഞെടുക്കാൻ സ്വൈപ്പ് ചെയ്യുക അല്ലെങ്കിൽ ദിവസം തിരഞ്ഞെടുക്കുന്നതിലേക്ക് തിരികെ പോകാൻ ടാപ്പ് ചെയ്യുക"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"അടുത്ത മാസത്തിലേക്ക് മാറ്റുക"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"മുമ്പത്തെ മാസത്തിലേക്ക് മാറ്റുക"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"%1$s എന്ന വർഷത്തിലേക്ക് പോകുക"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"നിലവിലെ തിരഞ്ഞെടുപ്പ്: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"ഒന്നുമില്ല"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"വർഷ പിക്കർ ദൃശ്യമാണ്"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-mn/strings.xml b/compose/material3/material3/src/androidMain/res/values-mn/strings.xml
index 0270772..1cc6860 100644
--- a/compose/material3/material3/src/androidMain/res/values-mn/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-mn/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Харилцах цонх"</string>
     <string name="expanded" msgid="5974471714631304645">"Дэлгэсэн"</string>
     <string name="collapsed" msgid="5389587048670450460">"Хураасан"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Үл хэрэгсэх"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Хайх"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Доорх зөвлөмжүүд"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Огноо сонгох"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Сонгосон огноо"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Жил сонгох руу сэлгэх"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Өдөр сонгох руу сэлгэх"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Он сонгохын тулд шудрах эсвэл өдөр сонгох руу буцааж сэлгэхийн тулд товшино уу"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Дараагийн сар луу өөрчлөх"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Өмнөх сар луу өөрчлөх"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"%1$s он руу шилжих"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Одоогийн сонголт: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Байхгүй"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Он сонгогч харагдаж байна"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-mr/strings.xml b/compose/material3/material3/src/androidMain/res/values-mr/strings.xml
index 980c5e8..575a698 100644
--- a/compose/material3/material3/src/androidMain/res/values-mr/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-mr/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"डायलॉग"</string>
     <string name="expanded" msgid="5974471714631304645">"विस्तारित केला"</string>
     <string name="collapsed" msgid="5389587048670450460">"कोलॅप्स केला"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"डिसमिस करा"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"शोधा"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"सूचना खाली आहेत"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"तारीख निवडा"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"निवडलेली तारीख"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"वर्ष निवडणे वर स्विच करा"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"दिवस निवडणे वर स्विच करा"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"वर्ष निवडण्यासाठी स्‍वाइप करा, किंवा दिवस निवडण्यावर परत स्विच करण्यासाठी टॅप करा"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"पुढील महिन्यावर बदला"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"मागील महिन्यावर बदला"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"%1$s वर्षावर नेव्हिगेट करा"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"सद्य निवड: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"काहीही नाही"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"वर्ष पिकर दृश्यमान आहे"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-ms/strings.xml b/compose/material3/material3/src/androidMain/res/values-ms/strings.xml
index 0b7fbbf..e588581 100644
--- a/compose/material3/material3/src/androidMain/res/values-ms/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-ms/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Dialog"</string>
     <string name="expanded" msgid="5974471714631304645">"Dikembangkan"</string>
     <string name="collapsed" msgid="5389587048670450460">"Dikuncupkan"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Ketepikan"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Carian"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Cadangan di bawah"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Pilih tarikh"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Tarikh dipilih"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Beralih kepada pemilihan tahun"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Beralih kepada pemilihan hari"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Leret untuk memilih tahun atau ketik untuk bertukar kembali kepada pemilihan hari"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Tukar kepada bulan seterusnya"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Tukar kepada bulan sebelumnya"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Navigasi ke tahun %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Pilihan semasa: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Tiada"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Pemilih tahun kelihatan"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-my/strings.xml b/compose/material3/material3/src/androidMain/res/values-my/strings.xml
index 48c1e81..3204728 100644
--- a/compose/material3/material3/src/androidMain/res/values-my/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-my/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ဒိုင်ယာလော့"</string>
     <string name="expanded" msgid="5974471714631304645">"ချဲ့ထားသည်"</string>
     <string name="collapsed" msgid="5389587048670450460">"ခေါက်ထားသည်"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"ပယ်ရန်"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"ရှာဖွေရန်"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"အကြံပြုချက်များ အောက်တွင်ရှိသည်"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"ရက်စွဲရွေးရန်"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"ရွေးထားသည့် ရက်စွဲ"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"နှစ်ရွေးခြင်းသို့ ပြောင်းရန်"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"ရက်ရွေးခြင်းသို့ ပြောင်းရန်"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"ခုနှစ်ရွေးချယ်ရန် ပွတ်ဆွဲပါ (သို့) ရက်ရွေးချယ်ခြင်းသို့ ပြန်ရန် တို့ပါ"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"နောက်လသို့ ပြောင်းရန်"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"ယခင်လသို့ ပြောင်းရန်"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"%1$s ခုနှစ်သို့ သွားရန်"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"လက်ရှိ ရွေးချယ်မှု- %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"မရှိ"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"ခုနှစ်ရွေးချယ်ရေးစနစ်ကို မြင်ရသည်"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-nb/strings.xml b/compose/material3/material3/src/androidMain/res/values-nb/strings.xml
index 8bec049..eebe6bc 100644
--- a/compose/material3/material3/src/androidMain/res/values-nb/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-nb/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Dialogboks"</string>
     <string name="expanded" msgid="5974471714631304645">"Vises"</string>
     <string name="collapsed" msgid="5389587048670450460">"Skjult"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Lukk"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Søk"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Du finner forslag nedenfor"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Velg dato"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Valgt dato"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Bytt til å velge et år"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Bytt til å velge en dag"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Sveip for å velge år, eller trykk for å bytte tilbake til valg av dag"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Endre til neste måned"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Endre til forrige måned"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Gå til år %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Valgt: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Ingen"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Årsvelgeren er synlig"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-ne/strings.xml b/compose/material3/material3/src/androidMain/res/values-ne/strings.xml
index a53e08a..4b950f9 100644
--- a/compose/material3/material3/src/androidMain/res/values-ne/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-ne/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"डायलग"</string>
     <string name="expanded" msgid="5974471714631304645">"एक्स्पान्ड गरियो"</string>
     <string name="collapsed" msgid="5389587048670450460">"कोल्याप्स गरियो"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"हटाउनुहोस्"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"खोज्नुहोस्"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"सुझावहरू तल दिइएका छन्"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"मिति चयन गर्नुहोस्"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"चयन गरिएको मिति"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"साल चयन गर्ने फिल्डमा जानुहोस्"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"दिन चयन गर्ने फिल्डमा जानुहोस्"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"कुनै साल छनौट गर्न स्वाइप गर्नुहोस् वा दिन चयन गर्न ट्याप गर्नुहोस्"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"हाल चयन गरिएको महिना परिवर्तन गरी आगामी महिना बनाउनुहोस्"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"हाल चयन गरिएको महिना परिवर्तन गरी अघिल्लो महिना बनाउनुहोस्"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"साल %1$s मा जानुहोस्"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"हालको छनौट: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"कुनै पनि होइन"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"साल पिकर देखिएको छ"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-nl/strings.xml b/compose/material3/material3/src/androidMain/res/values-nl/strings.xml
index e390a5b..2ff3515 100644
--- a/compose/material3/material3/src/androidMain/res/values-nl/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-nl/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Dialoogvenster"</string>
     <string name="expanded" msgid="5974471714631304645">"Uitgevouwen"</string>
     <string name="collapsed" msgid="5389587048670450460">"Samengevouwen"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Sluiten"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Zoeken"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Suggesties hieronder"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Datum selecteren"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Geselecteerde datum"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Schakelaar om een jaar te selecteren"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Schakelaar om een dag te selecteren"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Swipe om een jaar te selecteren of tik om terug te gaan en een dag te selecteren"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Naar volgende maand gaan"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Naar vorige maand gaan"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Ga naar jaar %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Huidige selectie: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Geen"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Jaarselectie zichtbaar"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-or/strings.xml b/compose/material3/material3/src/androidMain/res/values-or/strings.xml
index 76a9baa..912bdbb 100644
--- a/compose/material3/material3/src/androidMain/res/values-or/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-or/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ଡାଏଲଗ"</string>
     <string name="expanded" msgid="5974471714631304645">"ବିସ୍ତାର କରାଯାଇଛି"</string>
     <string name="collapsed" msgid="5389587048670450460">"ସଙ୍କୁଚିତ କରାଯାଇଛି"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"ଖାରଜ କରନ୍ତୁ"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"ସର୍ଚ୍ଚ କରନ୍ତୁ"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"ପରାମର୍ଶ ତଳେ ଦିଆଯାଇଛି"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"ତାରିଖ ଚୟନ କରନ୍ତୁ"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"ଚୟନିତ ତାରିଖ"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"ବର୍ଷ ଚୟନ କରିବାକୁ ସ୍ୱିଚ କରନ୍ତୁ"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"ଦିନ ଚୟନ କରିବାକୁ ସ୍ୱିଚ କରନ୍ତୁ"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"ଏକ ବର୍ଷ ଚୟନ କରିବା ପାଇଁ ସ୍ୱାଇପ କରନ୍ତୁ କିମ୍ବା ଏକ ଦିନ ଚୟନ କରିବା ପାଇଁ ପୁଣି ସ୍ୱିଚ କରିବାକୁ ଟାପ କରନ୍ତୁ"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"ପରବର୍ତ୍ତୀ ମାସକୁ ପରିବର୍ତ୍ତନ କରନ୍ତୁ"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"ପୂର୍ବବର୍ତ୍ତୀ ମାସକୁ ପରିବର୍ତ୍ତନ କରନ୍ତୁ"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"%1$s ବର୍ଷକୁ ନାଭିଗେଟ କରନ୍ତୁ"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"ବର୍ତ୍ତମାନର ଚୟନ: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"କିଛି ନାହିଁ"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"ବର୍ଷ ପିକର ଦେଖାଯାଉଛି"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-pa/strings.xml b/compose/material3/material3/src/androidMain/res/values-pa/strings.xml
index d61ea52..6d49ad8 100644
--- a/compose/material3/material3/src/androidMain/res/values-pa/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-pa/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ਵਿੰਡੋ"</string>
     <string name="expanded" msgid="5974471714631304645">"ਵਿਸਤਾਰ ਕੀਤਾ ਗਿਆ"</string>
     <string name="collapsed" msgid="5389587048670450460">"ਸਮੇਟਿਆ ਗਿਆ"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"ਖਾਰਜ ਕਰੋ"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"ਖੋਜੋ"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"ਸੁਝਾਅ ਹੇਠਾਂ ਹਨ"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"ਤਾਰੀਖ ਚੁਣੋ"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"ਚੁਣੀ ਗਈ ਤਾਰੀਖ"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"ਸਾਲ ਚੁਣਨ ਲਈ ਸਵਿੱਚ ਕਰੋ"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"ਦਿਨ ਚੁਣਨ ਲਈ ਸਵਿੱਚ ਕਰੋ"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"ਕੋਈ ਸਾਲ ਚੁਣਨ ਲਈ ਸਵਾਈਪ ਕਰੋ ਜਾਂ ਕੋਈ ਦਿਨ ਚੁਣਨ ਲਈ ਵਾਪਸ ਜਾਣ ਵਾਸਤੇ ਟੈਪ ਕਰੋ"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"ਅਗਲੇ ਮਹੀਨੇ \'ਤੇ ਜਾਓ"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"ਪਿਛਲੇ ਮਹੀਨੇ \'ਤੇ ਜਾਓ"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"ਸਾਲ %1$s \'ਤੇ ਜਾਓ"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"ਮੌਜੂਦਾ ਚੋਣ: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"ਕੋਈ ਨਹੀਂ"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"ਸਾਲ ਚੋਣਕਾਰ ਦਿਖਣਯੋਗ ਹੈ"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-pl/strings.xml b/compose/material3/material3/src/androidMain/res/values-pl/strings.xml
index 7930b29..2daa8a8 100644
--- a/compose/material3/material3/src/androidMain/res/values-pl/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-pl/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Dialog"</string>
     <string name="expanded" msgid="5974471714631304645">"Rozwinięte"</string>
     <string name="collapsed" msgid="5389587048670450460">"Zwinięte"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Zamknij"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Wyszukiwanie"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Propozycje znajdziesz poniżej"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Wybierz datę"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Wybrana data"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Przełącz na wybór roku"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Przełącz na wybór dnia"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Przesuń, aby wybrać rok, lub wróć do poprzedniej sekcji i wybierz dzień"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Zmień na następny miesiąc"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Zmień na poprzedni miesiąc"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Przejdź do roku %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Obecnie wybrane: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Brak"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Widoczny selektor roku"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-pt-rBR/strings.xml b/compose/material3/material3/src/androidMain/res/values-pt-rBR/strings.xml
index 1b57ef0..6b6c723 100644
--- a/compose/material3/material3/src/androidMain/res/values-pt-rBR/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-pt-rBR/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Caixa de diálogo"</string>
     <string name="expanded" msgid="5974471714631304645">"Aberto"</string>
     <string name="collapsed" msgid="5389587048670450460">"Fechado"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Dispensar"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Pesquisa"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Sugestões abaixo"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Selecionar data"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Data selecionada"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Trocar para a seleção de ano"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Trocar para a seleção de dia"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Deslize para selecionar um ano ou toque para voltar à seleção de dia"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Mudar para o próximo mês"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Mudar para o mês anterior"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Navegar para o ano de %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Seleção atual: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Nenhuma"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Seletor de dia visível"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-pt-rPT/strings.xml b/compose/material3/material3/src/androidMain/res/values-pt-rPT/strings.xml
index 9fb9c1b..86e9c17 100644
--- a/compose/material3/material3/src/androidMain/res/values-pt-rPT/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-pt-rPT/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Caixa de diálogo"</string>
     <string name="expanded" msgid="5974471714631304645">"Expandido"</string>
     <string name="collapsed" msgid="5389587048670450460">"Reduzido"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Ignorar"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Pesquisar"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Sugestões abaixo"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Selecionar data"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Data selecionada"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Mude para a seleção do ano"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Mude para a seleção do dia"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Deslize rapidamente para selecionar um ano ou toque para mudar novamente para a seleção do dia"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Mudar para o mês seguinte"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Mudar para o mês anterior"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Navegar para o ano %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Seleção atual: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Nenhuma"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Selecionador de ano visível"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-pt/strings.xml b/compose/material3/material3/src/androidMain/res/values-pt/strings.xml
index 1b57ef0..6b6c723 100644
--- a/compose/material3/material3/src/androidMain/res/values-pt/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-pt/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Caixa de diálogo"</string>
     <string name="expanded" msgid="5974471714631304645">"Aberto"</string>
     <string name="collapsed" msgid="5389587048670450460">"Fechado"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Dispensar"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Pesquisa"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Sugestões abaixo"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Selecionar data"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Data selecionada"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Trocar para a seleção de ano"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Trocar para a seleção de dia"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Deslize para selecionar um ano ou toque para voltar à seleção de dia"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Mudar para o próximo mês"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Mudar para o mês anterior"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Navegar para o ano de %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Seleção atual: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Nenhuma"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Seletor de dia visível"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-ro/strings.xml b/compose/material3/material3/src/androidMain/res/values-ro/strings.xml
index 0afaa0d..75def7c 100644
--- a/compose/material3/material3/src/androidMain/res/values-ro/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-ro/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Dialog"</string>
     <string name="expanded" msgid="5974471714631304645">"Extins"</string>
     <string name="collapsed" msgid="5389587048670450460">"Restrâns"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Respinge"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Caută"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Sugestii mai jos"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Selectează data"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Data selectată"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Comută la selectarea anului"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Comută la selectarea zilei"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Glisează pentru a selecta un an sau atinge pentru a reveni la selectarea zilei"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Treci la luna următoare"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Treci la luna anterioară"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Navighează la anul %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Opțiunea selectată: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Fără"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Selectorul de an este vizibil"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-ru/strings.xml b/compose/material3/material3/src/androidMain/res/values-ru/strings.xml
index 0a3e165..b6296a1 100644
--- a/compose/material3/material3/src/androidMain/res/values-ru/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-ru/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Диалоговое окно"</string>
     <string name="expanded" msgid="5974471714631304645">"Развернуто"</string>
     <string name="collapsed" msgid="5389587048670450460">"Свернуто"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Закрыть"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Строка поиска"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Подсказки показаны ниже"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Выбор даты"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Выбранная дата"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Перейти к выбору года"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Перейти к выбору дня"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Проведите по экрану, чтобы выбрать год, или нажмите, чтобы вернуться к выбору дня."</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Перейти к следующему месяцу"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Перейти к предыдущему месяцу"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Переход к %1$s году"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Текущий выбор: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Не выбрано"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Отображаемый выбор года"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-si/strings.xml b/compose/material3/material3/src/androidMain/res/values-si/strings.xml
index bcb0565..e6acdba 100644
--- a/compose/material3/material3/src/androidMain/res/values-si/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-si/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"සංවාදය"</string>
     <string name="expanded" msgid="5974471714631304645">"දිග හරින ලදි"</string>
     <string name="collapsed" msgid="5389587048670450460">"හකුළන ලදි"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"අස් කරන්න"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"සෙවීම"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"පහත යෝජනා"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"දිනය තෝරන්න"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"තේරූ දිනය"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"වසරක් තේරීමට මාරු වන්න"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"දිනක් තේරීමට මාරු වන්න"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"වසරක් තේරීමට ස්වයිප් කරන්න, නැතහොත් දිනක් තේරීමට ආපසු මාරු වීමට තට්ටු කරන්න"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"ලබන මාසයට වෙනස් කරන්න"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"කලින් මාසයට වෙනස් කරන්න"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"%1$s වසර වෙත සංචලන කරන්න"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"වත්මන් තේරීම: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"කිසිවක් නැත"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"වසර තෝරකය දෘශ්‍යමානයි"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-sk/strings.xml b/compose/material3/material3/src/androidMain/res/values-sk/strings.xml
index 8c85f48..f1dc227 100644
--- a/compose/material3/material3/src/androidMain/res/values-sk/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-sk/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Dialógové okno"</string>
     <string name="expanded" msgid="5974471714631304645">"Rozbalené"</string>
     <string name="collapsed" msgid="5389587048670450460">"Zbalené"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Zavrieť"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Vyhľadávanie"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Návrhy sú nižšie"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Vybrať dátum"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Vybraný dátum"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Prepnúť na výber roka"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Prepnúť na výber dňa"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Potiahnutím vyberte rok alebo klepnutím prepnite späť na výber dňa"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Zmeniť na nasledujúci mesiac"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Zmeniť na predchádzajúci mesiac"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Prechod na rok %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Aktuálny výber: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Žiadne"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Výber roka je viditeľný"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-sl/strings.xml b/compose/material3/material3/src/androidMain/res/values-sl/strings.xml
index 91f34b8..6a6b270 100644
--- a/compose/material3/material3/src/androidMain/res/values-sl/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-sl/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Pogovorno okno"</string>
     <string name="expanded" msgid="5974471714631304645">"Razširjeno"</string>
     <string name="collapsed" msgid="5389587048670450460">"Strnjeno"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Opusti"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Iskanje"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Predlogi so spodaj"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Izbira datuma"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Izbrani datum"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Preklopi na izbiro leta"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Preklopi na izbiro dneva"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Povlecite, da izberete leto, ali se dotaknite, da preklopite nazaj na izbiranje dneva."</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Spremeni na naslednji mesec"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Spremeni na prejšnji mesec"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Pomik na leto %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Trenutna izbira: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Brez"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Izbirnik leta je viden"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-sq/strings.xml b/compose/material3/material3/src/androidMain/res/values-sq/strings.xml
index 747e375..b7c198e 100644
--- a/compose/material3/material3/src/androidMain/res/values-sq/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-sq/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Dialogu"</string>
     <string name="expanded" msgid="5974471714631304645">"U zgjerua"</string>
     <string name="collapsed" msgid="5389587048670450460">"U palos"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Hiq"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Kërko"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Sugjerimet më poshtë"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Zgjidh datën"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Data e zgjedhur"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Kalo te zgjedhja e një viti"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Kalo te zgjedhja e një dite"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Rrëshqit shpejt për të zgjedhur një vit ose trokit për të kaluar sërish te zgjedhja e ditës"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Ndrysho te muaji i ardhshëm"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Ndrysho te muaji i kaluar"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Navigo në vitin %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Zgjedhja aktuale: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Asnjë"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Zgjedhësi i vitit i dukshëm"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-sr/strings.xml b/compose/material3/material3/src/androidMain/res/values-sr/strings.xml
index dade0a9..aff6c1a 100644
--- a/compose/material3/material3/src/androidMain/res/values-sr/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-sr/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Дијалог"</string>
     <string name="expanded" msgid="5974471714631304645">"Проширено је"</string>
     <string name="collapsed" msgid="5389587048670450460">"Скупљено је"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Одбаците"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Претрага"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Предлози су у наставку"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Изаберите датум"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Изабрани датум"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Пређите на избор године"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Пређите на избор дана"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Превуците да бисте изабрали годину или додирните да бисте се вратили на избор дана"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Пређите на следећи месец"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Пређите на претходни месец"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Идите на годину: %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Актуелни избор: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Ништа"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Видљив бирач година"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-sv/strings.xml b/compose/material3/material3/src/androidMain/res/values-sv/strings.xml
index f783a1c..57638de 100644
--- a/compose/material3/material3/src/androidMain/res/values-sv/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-sv/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Dialogruta"</string>
     <string name="expanded" msgid="5974471714631304645">"Utökad"</string>
     <string name="collapsed" msgid="5389587048670450460">"Komprimerad"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Stäng"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Sök"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Se förslag nedan"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Välj datum"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Valt datum"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Byt till att välja år"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Byt till att välja dag"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Svep för att välja ett år eller tryck för att återgå till att välja en dag"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Ändra till nästa månad"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Ändra till föregående månad"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Navigera till %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Aktuellt val: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Inget"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Årväljaren är synlig"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-sw/strings.xml b/compose/material3/material3/src/androidMain/res/values-sw/strings.xml
index 83be738..1495a76 100644
--- a/compose/material3/material3/src/androidMain/res/values-sw/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-sw/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Mazungumzo"</string>
     <string name="expanded" msgid="5974471714631304645">"Imepanuliwa"</string>
     <string name="collapsed" msgid="5389587048670450460">"Imekunjwa"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Ondoa"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Tafuta"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Mapendekezo yaliyo hapa chini"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Chagua tarehe"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Tarehe uliyochagua"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Nenda kwenye sehemu ya kuchagua mwaka"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Nenda kwenye sehemu ya kuchagua siku"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Telezesha ili uchague mwaka au gusa ili urejee kwenye kuchagua siku"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Nenda kwenye mwezi unaofuata"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Rudi kwenye mwezi uliotangulia"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Nenda kwenye mwaka %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Iliyochaguliwa sasa: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Hamna"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Kiteua mwaka kinaonekana"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-ta/strings.xml b/compose/material3/material3/src/androidMain/res/values-ta/strings.xml
index 53cba20..c0c83ef 100644
--- a/compose/material3/material3/src/androidMain/res/values-ta/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-ta/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"உரையாடல்"</string>
     <string name="expanded" msgid="5974471714631304645">"விரிக்கப்பட்டது"</string>
     <string name="collapsed" msgid="5389587048670450460">"சுருக்கப்பட்டது"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"மூடும்"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"தேடலாம்"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"பரிந்துரைகள் கீழே கிடைக்கும்"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"தேதியைத் தேர்ந்தெடுக்கவும்"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"தேர்ந்தெடுக்கப்பட்ட தேதி"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"ஆண்டைத் தேர்ந்தெடுக்கும் விருப்பத்திற்கு மாற்று"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"தேதியைத் தேர்ந்தெடுக்கும் விருப்பத்திற்கு மாற்று"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"ஆண்டைத் தேர்வுசெய்ய ஸ்வைப் செய்யுங்கள் அல்லது தேதியைத் தேர்வுசெய்யும் பக்கத்திற்கு மீண்டும் செல்ல தட்டுங்கள்"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"அடுத்த மாதத்திற்கு மாற்று"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"முந்தைய மாதத்திற்கு மாற்று"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"%1$sக்குச் செல்லும்"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"தற்போது %1$s தேர்வுசெய்யப்பட்டுள்ளது"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"ஏதுமில்லை"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"ஆண்டைத் தேர்வுசெய்யும் பக்கம் காட்டப்படுகிறது"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-te/strings.xml b/compose/material3/material3/src/androidMain/res/values-te/strings.xml
index 1a79c25..5dd0adb 100644
--- a/compose/material3/material3/src/androidMain/res/values-te/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-te/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"డైలాగ్"</string>
     <string name="expanded" msgid="5974471714631304645">"విస్తరించబడింది"</string>
     <string name="collapsed" msgid="5389587048670450460">"కుదించబడింది"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"విస్మరించండి"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"సెర్చ్ చేయండి"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"సూచనలు దిగువున ఉన్నాయి"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"తేదీని ఎంచుకోండి"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"ఎంచుకున్న తేదీ"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"ఒక సంవత్సరాన్ని ఎంచుకునే ఆప్షన్‌కు మారండి"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"ఒక రోజును ఎంచుకునే ఆప్షన్‌కు మారండి"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"సంవత్సరాన్ని ఎంచుకోవడానికి స్వైప్ చేయండి, లేదా రోజును ఎంచుకునేందుకు తిరిగి మారడానికి ట్యాప్ చేయండి"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"వచ్చే నెలకు మార్చండి"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"మునుపటి నెలకు మార్చండి"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"%1$s సంవత్సరానికి వెళ్లండి"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"ప్రస్తుత ఎంపిక: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"ఏదీ లేదు"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"సంవత్సరం పికర్ కనిపిస్తుంది"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-th/strings.xml b/compose/material3/material3/src/androidMain/res/values-th/strings.xml
index 0c74caa..3033d65 100644
--- a/compose/material3/material3/src/androidMain/res/values-th/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-th/strings.xml
@@ -20,11 +20,26 @@
     <string name="dialog" msgid="4057925834421392736">"กล่องโต้ตอบ"</string>
     <string name="expanded" msgid="5974471714631304645">"ขยาย"</string>
     <string name="collapsed" msgid="5389587048670450460">"ยุบ"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"ปิด"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"ค้นหา"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"มีคำแนะนำที่ด้านล่าง"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"เลือกวันที่"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"วันที่ที่เลือก"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"เปลี่ยนไปที่การเลือกปี"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"เปลี่ยนไปที่การเลือกวัน"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"ปัดเพื่อเลือกปีหรือแตะเพื่อเปลี่ยนกลับไปยังการเลือกวัน"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"เปลี่ยนไปที่เดือนถัดไป"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"เปลี่ยนไปที่เดือนก่อนหน้า"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"ไปยังปี %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"การเลือกปัจจุบัน: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"ไม่มี"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"แสดงตัวเลือกปี"</string>
+    <string name="date_input_title" msgid="3010396677286327048">"เลือกวันที่"</string>
+    <string name="date_input_headline" msgid="3499643850558715142">"วันที่ป้อน"</string>
+    <string name="date_input_label" msgid="5194825853981987218">"วันที่"</string>
+    <string name="date_input_headline_description" msgid="8562356184193964298">"วันที่ป้อน: %1$s"</string>
+    <string name="date_input_no_input_description" msgid="5722931102250207748">"ไม่มี"</string>
+    <string name="date_input_invalid_not_allowed" msgid="6114792992433444995">"ไม่อนุญาตให้ใช้วันที่นี้: %1$s"</string>
+    <string name="date_input_invalid_for_pattern" msgid="5281836720766682161">"วันที่ไม่ตรงกับรูปแบบที่คาดไว้: %1$s"</string>
+    <string name="date_input_invalid_year_range" msgid="8434112129235255568">"วันที่อยู่นอกเหนือจากช่วงปีที่คาดไว้ %1$s - %2$s"</string>
+    <string name="tooltip_long_press_label" msgid="2732804537909054941">"แสดงเคล็ดลับเครื่องมือ"</string>
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-tl/strings.xml b/compose/material3/material3/src/androidMain/res/values-tl/strings.xml
index fcf09bd..dfdc9b2 100644
--- a/compose/material3/material3/src/androidMain/res/values-tl/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-tl/strings.xml
@@ -20,11 +20,26 @@
     <string name="dialog" msgid="4057925834421392736">"Dialog"</string>
     <string name="expanded" msgid="5974471714631304645">"Naka-expand"</string>
     <string name="collapsed" msgid="5389587048670450460">"Naka-collapse"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"I-dismiss"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Maghanap"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Mga suhestyon sa ibaba"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Pumili ng petsa"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Piniling petsa"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Lumipat sa pagpili ng taon"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Lumipat sa pagpili ng araw"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Mag-swipe para pumili ng taon, o mag-tap para bumalik sa pagpili ng araw"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Lumipat sa susunod na buwan"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Lumipat sa nakaraang buwan"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Mag-navigate papunta sa taong %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Kasalukuyang napili: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Wala"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Nakikita ang picker ng taon"</string>
+    <string name="date_input_title" msgid="3010396677286327048">"Pumili ng petsa"</string>
+    <string name="date_input_headline" msgid="3499643850558715142">"Inilagay na petsa"</string>
+    <string name="date_input_label" msgid="5194825853981987218">"Petsa"</string>
+    <string name="date_input_headline_description" msgid="8562356184193964298">"Inilagay na petsa: %1$s"</string>
+    <string name="date_input_no_input_description" msgid="5722931102250207748">"Wala"</string>
+    <string name="date_input_invalid_not_allowed" msgid="6114792992433444995">"Hindi pinapayagan ang petsa: %1$s"</string>
+    <string name="date_input_invalid_for_pattern" msgid="5281836720766682161">"Hindi tumutugma ang petsa sa inaasahang pattern: %1$s"</string>
+    <string name="date_input_invalid_year_range" msgid="8434112129235255568">"Wala ang petsa sa inaasahang hanay ng taon na %1$s - %2$s"</string>
+    <string name="tooltip_long_press_label" msgid="2732804537909054941">"Ipakita ang tooltip"</string>
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-tr/strings.xml b/compose/material3/material3/src/androidMain/res/values-tr/strings.xml
index 1485d39..b8de10e 100644
--- a/compose/material3/material3/src/androidMain/res/values-tr/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-tr/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"İletişim kutusu"</string>
     <string name="expanded" msgid="5974471714631304645">"Genişletilmiş"</string>
     <string name="collapsed" msgid="5389587048670450460">"Daraltılmış"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Kapat"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Arama"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Önerileri aşağıda bulabilirsiniz"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Tarih seç"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Seçilen tarih"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Yıl seçimine geç"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Gün seçimine geç"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Kaydırarak bir yıl seçin veya gün seçme bölümüne geri dönmek için dokunun"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Sonraki aya değiştir"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Önceki aya değiştir"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"%1$s yılına gidin"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Geçerli seçim: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Yok"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Yıl seçici görünür durumda"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-uk/strings.xml b/compose/material3/material3/src/androidMain/res/values-uk/strings.xml
index d7c398b8..cb30959 100644
--- a/compose/material3/material3/src/androidMain/res/values-uk/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-uk/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Вікно"</string>
     <string name="expanded" msgid="5974471714631304645">"Розгорнуто"</string>
     <string name="collapsed" msgid="5389587048670450460">"Згорнуто"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Закрити"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Пошук"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Підказки внизу"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Вибрати дату"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Вибрана дата"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Перейти до вибору року"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Перейти до вибору дня"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Проведіть пальцем по екрану, щоб вибрати рік, або торкніться, щоб повернутися до вибору дня"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Перейти до наступного місяця"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Перейти до попереднього місяця"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Перейти до %1$s року"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Поточний вибір: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Немає"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Показувати засіб вибору року"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-ur/strings.xml b/compose/material3/material3/src/androidMain/res/values-ur/strings.xml
index 2bc5202..dd566cd 100644
--- a/compose/material3/material3/src/androidMain/res/values-ur/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-ur/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ڈائلاگ"</string>
     <string name="expanded" msgid="5974471714631304645">"پھیلایا گیا"</string>
     <string name="collapsed" msgid="5389587048670450460">"سکیڑا گیا"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"برخاست کریں"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"تلاش کریں"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"تلاش کی تجاویز نیچے دستیاب ہیں"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"تاریخ منتخب کریں"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"منتخب کردہ تاریخ"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"ایک سال کا انتخاب کرنے کے لیے سوئچ کریں"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"دن منتخب کرنے کے لیے سوئچ کریں"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"ایک سال منتخب کرنے کے لیے سوائپ کریں یا ایک دن منتخب کرنے کی خاطر دوبارہ سوئچ کرنے کے لیے تھپتھپائیں"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"اگلے ماہ میں تبدیل کریں"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"گزشتہ ماہ میں منتقل کریں"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"‏سال ‎%1$s پر نیویگیٹ کریں"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"‏موجودہ انتخاب: ‎%1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"کوئی نہیں"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"سال کا منتخب کنندہ مرئی ہے"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-uz/strings.xml b/compose/material3/material3/src/androidMain/res/values-uz/strings.xml
index 51cb568..c93a3ac 100644
--- a/compose/material3/material3/src/androidMain/res/values-uz/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-uz/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Muloqot oynasi"</string>
     <string name="expanded" msgid="5974471714631304645">"Yoyilgan"</string>
     <string name="collapsed" msgid="5389587048670450460">"Yigʻilgan"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Yopish"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Qidiruv"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Takliflar quyida"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Sanani tanlang"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Tanlangan sana"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Tanlangan yilga oʻtish"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Tanlangan kunga oʻtish"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Yilni tanlash uchun suring yoki kunni tanlashga qaytish uchun tegining"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Keyingi oyga oʻzgartirish"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Avvalgi oyga oʻzgartirish"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"%1$s-yilga oʻtish"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Joriy tanlov: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Hech biri"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Yil tanlagich ochiq"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-vi/strings.xml b/compose/material3/material3/src/androidMain/res/values-vi/strings.xml
index c39a5bc..0108f77 100644
--- a/compose/material3/material3/src/androidMain/res/values-vi/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-vi/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Hộp thoại"</string>
     <string name="expanded" msgid="5974471714631304645">"Đã mở rộng"</string>
     <string name="collapsed" msgid="5389587048670450460">"Đã thu gọn"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Đóng"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Tìm kiếm"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Các đề xuất ở bên dưới"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Chọn ngày"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Ngày đã chọn"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Chuyển sang chọn năm"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Chuyển sang chọn ngày"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Vuốt để chọn một năm hoặc nhấn để chuyển lại về chọn một ngày"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Chuyển sang tháng tiếp theo"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Chuyển về tháng trước"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Chuyển đến năm %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Lựa chọn hiện tại: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Không có"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Bộ chọn năm hiển thị"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-zh-rCN/strings.xml b/compose/material3/material3/src/androidMain/res/values-zh-rCN/strings.xml
index e33dea1..a6362df 100644
--- a/compose/material3/material3/src/androidMain/res/values-zh-rCN/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-zh-rCN/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"对话框"</string>
     <string name="expanded" msgid="5974471714631304645">"已展开"</string>
     <string name="collapsed" msgid="5389587048670450460">"已收起"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"关闭"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"搜索"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"以下是搜索建议"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"选择日期"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"选定的日期"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"切换以选择年份"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"切换以选择日期"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"滑动可选择年份,点按可切换回选择日期"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"转到下个月"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"转到上个月"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"切换到年份:%1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"当前的选择:%1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"无"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"年份选择器可见"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-zh-rHK/strings.xml b/compose/material3/material3/src/androidMain/res/values-zh-rHK/strings.xml
index 033e623..c41edfe 100644
--- a/compose/material3/material3/src/androidMain/res/values-zh-rHK/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-zh-rHK/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"對話框"</string>
     <string name="expanded" msgid="5974471714631304645">"展開咗"</string>
     <string name="collapsed" msgid="5389587048670450460">"合埋咗"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"閂"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"搵"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"建議如下"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"選取日期"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"所選日期"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"切換為選取年份"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"切換為選取日期"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"滑動可選取年分,或可輕按返回選取日期"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"變更至下個月"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"變更至上個月"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"前往 %1$s 年"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"目前選項:%1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"無"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"顯示年分挑選器"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-zh-rTW/strings.xml b/compose/material3/material3/src/androidMain/res/values-zh-rTW/strings.xml
index ca1ce24..6ea7600 100644
--- a/compose/material3/material3/src/androidMain/res/values-zh-rTW/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-zh-rTW/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"對話方塊"</string>
     <string name="expanded" msgid="5974471714631304645">"已展開"</string>
     <string name="collapsed" msgid="5389587048670450460">"已收合"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"關閉"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"搜尋"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"建議如下"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"選取日期"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"所選日期"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"改為選取年份"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"改為選取星期幾"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"滑動可選取年分,也可輕觸返回選取日期"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"改成下個月"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"改成上個月"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"前往 %1$s 年"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"目前選項:%1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"無"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"顯示年份挑選器"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values-zu/strings.xml b/compose/material3/material3/src/androidMain/res/values-zu/strings.xml
index 68ea064..093f8fb 100644
--- a/compose/material3/material3/src/androidMain/res/values-zu/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values-zu/strings.xml
@@ -20,11 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Ibhokisi"</string>
     <string name="expanded" msgid="5974471714631304645">"Kunwetshiwe"</string>
     <string name="collapsed" msgid="5389587048670450460">"Kugoqiwe"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"Chitha"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"Sesha"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"Iziphakamiso ngezansi"</string>
     <string name="date_picker_title" msgid="9208721003668059792">"Khetha usuku"</string>
     <string name="date_picker_headline" msgid="2846784065735639969">"Khetha usuku"</string>
     <string name="date_picker_switch_to_year_selection" msgid="3412370019845183965">"Shintshela ekukhetheni unyaka"</string>
-    <string name="date_picker_switch_to_day_selection" msgid="5820832733264067677">"Shintshela ekukhetheni usuku"</string>
+    <string name="date_picker_switch_to_day_selection" msgid="145089358343568971">"Swayipha ukuze ukhethe unyaka, noma thepha ukuze ubuyele ekukhetheni usuku"</string>
     <string name="date_picker_switch_to_next_month" msgid="8313783187901412102">"Shintshela kunyanga elandelayo"</string>
     <string name="date_picker_switch_to_previous_month" msgid="7596294429748914881">"Shintshela kunyanga edlule"</string>
+    <string name="date_picker_navigate_to_year_description" msgid="5152441868029453612">"Funa kunyaka %1$s"</string>
+    <string name="date_picker_headline_description" msgid="4627306862713137085">"Ukukhetha kwamanje: %1$s"</string>
+    <string name="date_picker_no_selection_description" msgid="5724377114289981899">"Lutho"</string>
+    <string name="date_picker_year_picker_pane_title" msgid="8140324713311804736">"Isikhethi sonyaka siyabonakala"</string>
+    <!-- no translation found for date_input_title (3010396677286327048) -->
+    <skip />
+    <!-- no translation found for date_input_headline (3499643850558715142) -->
+    <skip />
+    <!-- no translation found for date_input_label (5194825853981987218) -->
+    <skip />
+    <!-- no translation found for date_input_headline_description (8562356184193964298) -->
+    <skip />
+    <!-- no translation found for date_input_no_input_description (5722931102250207748) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_not_allowed (6114792992433444995) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_for_pattern (5281836720766682161) -->
+    <skip />
+    <!-- no translation found for date_input_invalid_year_range (8434112129235255568) -->
+    <skip />
+    <!-- no translation found for tooltip_long_press_label (2732804537909054941) -->
+    <skip />
 </resources>
diff --git a/compose/material3/material3/src/androidMain/res/values/strings.xml b/compose/material3/material3/src/androidMain/res/values/strings.xml
index 9284a12..cdbd37b 100644
--- a/compose/material3/material3/src/androidMain/res/values/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values/strings.xml
@@ -32,7 +32,44 @@
     <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_today_description">Today</string>
+    <string name="date_picker_year_picker_pane_title">Year picker visible</string>
+    <string name="date_input_title">Select date</string>
+    <string name="date_input_headline">Entered date</string>
+    <string name="date_input_label">Date</string>
+    <string name="date_input_headline_description">Entered date: %1$s</string>
+    <string name="date_input_no_input_description">None</string>
+    <string name="date_input_invalid_not_allowed">Date not allowed: %1$s</string>
+    <string name="date_input_invalid_for_pattern">Date does not match expected pattern: %1$s</string>
+    <string name="date_input_invalid_year_range">
+        Date out of expected year range %1$s - %2$s
+    </string>
+    <string name="date_picker_switch_to_calendar_mode">Switch to calendar input mode</string>
+    <string name="date_picker_switch_to_input_mode">Switch to text input mode</string>
+    <!-- Spoken description of a tooltip -->
+    <string name="tooltip_long_press_label">Show tooltip</string>
+    <!-- Suffix for time in 12-hour standard, after noon. [CHAR_LIMIT=2]" -->
+    <string name="time_picker_pm">PM</string>
+    <!-- Suffix for time in 12-hour standard, before noon. [CHAR_LIMIT=2]" -->
+    <string name="time_picker_am">AM</string>
+    <!-- Description for the toggle to choose between AM and PM [CHAR_LIMIT=NONE] -->
+    <string name="time_picker_period_toggle_description">Select AM or PM</string>
+    <!-- Description for button to switch to select the hour [CHAR_LIMIT=NONE] -->
+    <string name="time_picker_hour_selection">Select hour</string>
+    <!-- Description for button to switch to select the minute [CHAR_LIMIT=NONE] -->
+    <string name="time_picker_minute_selection">Select minutes</string>
+    <!-- Spoken suffix for an hour in the clock [CHAR_LIMIT=10] -->
+    <string name="time_picker_hour_suffix">%1$d o\'clock</string>
+    <!-- Spoken suffix for an hour in the 24-hour clock [CHAR_LIMIT=10] -->
+    <string name="time_picker_hour_24h_suffix">%1$d hours</string>
+    <!-- Spoken suffix for an amount of minutes in the clock [CHAR_LIMIT=16] -->
+    <string name="time_picker_minute_suffix">%1$d minutes</string>
 </resources>
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/CalendarModel.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/CalendarModel.kt
index d72d719..d513a40 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/CalendarModel.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/CalendarModel.kt
@@ -16,13 +16,45 @@
 
 package androidx.compose.material3
 
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
 import java.util.Locale
 
 /**
  * Creates a [CalendarModel] to be used by the date picker.
  */
 @ExperimentalMaterial3Api
-internal expect fun createCalendarModel(): CalendarModel
+internal expect fun CalendarModel(): CalendarModel
+
+/**
+ * Formats a UTC timestamp into a string with a given date format skeleton.
+ *
+ * A skeleton is similar to, and uses the same format characters as described in
+ * [Unicode Technical Standard #35](https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)
+ *
+ * One difference is that order is irrelevant. For example, "MMMMd" will return "MMMM d" in the
+ * en_US locale, but "d. MMMM" in the de_CH locale.
+ *
+ * @param utcTimeMillis a UTC timestamp to format (milliseconds from epoch)
+ * @param skeleton a date format skeleton
+ * @param locale the [Locale] to use when formatting the given timestamp
+ */
+@ExperimentalMaterial3Api
+internal expect fun formatWithSkeleton(
+    utcTimeMillis: Long,
+    skeleton: String,
+    locale: Locale = Locale.getDefault()
+): String
+
+/**
+ * A composable function that returns the default [Locale].
+ *
+ * When running on an Android platform, it will be recomposed when the `Configuration` gets updated.
+ */
+@Composable
+@ReadOnlyComposable
+@ExperimentalMaterial3Api
+internal expect fun defaultLocale(): Locale
 
 @ExperimentalMaterial3Api
 internal interface CalendarModel {
@@ -51,7 +83,7 @@
     val weekdayNames: List<Pair<String, String>>
 
     /**
-     * Holds a [DateInputFormat] for the current [Locale].
+     * Returns a [DateInputFormat] for the given [Locale].
      *
      * The input format represents the date with two digits for the day and the month, and
      * four digits for the year.
@@ -66,7 +98,7 @@
      *  - dd.MM.yyyy
      *  - MM/dd/yyyy
      */
-    val dateInputFormat: DateInputFormat
+    fun getDateInputFormat(locale: Locale = Locale.getDefault()): DateInputFormat
 
     /**
      * Returns a [CalendarDate] from a given _UTC_ time in milliseconds.
@@ -129,20 +161,40 @@
     fun minusMonths(from: CalendarMonth, subtractedMonthsCount: Int): CalendarMonth
 
     /**
-     * Formats a [CalendarMonth] into a string with a given date format pattern.
+     * Formats a [CalendarMonth] into a string with a given date format skeleton.
      *
      * @param month a [CalendarMonth] to format
-     * @param pattern a date format pattern
+     * @param skeleton a date format skeleton
+     * @param locale the [Locale] to use when formatting the given month
      */
-    fun format(month: CalendarMonth, pattern: String): String
+    fun formatWithSkeleton(
+        month: CalendarMonth,
+        skeleton: String,
+        locale: Locale = Locale.getDefault()
+    ): String =
+        formatWithSkeleton(month.startUtcTimeMillis, skeleton, locale)
 
     /**
-     * Formats a [CalendarDate] into a string with a given date format pattern.
+     * Formats a [CalendarDate] into a string with a given date format skeleton.
      *
      * @param date a [CalendarDate] to format
-     * @param pattern a date format pattern
+     * @param skeleton a date format skeleton
+     * @param locale the [Locale] to use when formatting the given date
      */
-    fun format(date: CalendarDate, pattern: String): String
+    fun formatWithSkeleton(
+        date: CalendarDate,
+        skeleton: String,
+        locale: Locale = Locale.getDefault()
+    ): String = formatWithSkeleton(date.utcTimeMillis, skeleton, locale)
+
+    /**
+     * Formats a UTC timestamp into a string with a given date format pattern.
+     *
+     * @param utcTimeMillis a UTC timestamp to format (milliseconds from epoch)
+     * @param pattern a date format pattern
+     * @param locale the [Locale] to use when formatting the given timestamp
+     */
+    fun formatWithPattern(utcTimeMillis: Long, pattern: String, locale: Locale): String
 
     /**
      * Parses a date string into a [CalendarDate].
@@ -171,6 +223,16 @@
 ) : Comparable<CalendarDate> {
     override operator fun compareTo(other: CalendarDate): Int =
         this.utcTimeMillis.compareTo(other.utcTimeMillis)
+
+    /**
+     * Formats the date into a string with the given skeleton format and a [Locale].
+     */
+    fun format(
+        calendarModel: CalendarModel,
+        skeleton: String,
+        locale: Locale = Locale.getDefault()
+    ): String =
+        calendarModel.formatWithSkeleton(this, skeleton, locale)
 }
 
 /**
@@ -201,9 +263,19 @@
     /**
      * Returns the position of a [CalendarMonth] within given years range.
      */
-    internal fun indexIn(years: IntRange): Int {
+    fun indexIn(years: IntRange): Int {
         return (year - years.first) * 12 + month - 1
     }
+
+    /**
+     * Formats the month into a string with the given skeleton format and a [Locale].
+     */
+    fun format(
+        calendarModel: CalendarModel,
+        skeleton: String,
+        locale: Locale = Locale.getDefault()
+    ): String =
+        calendarModel.formatWithSkeleton(this, skeleton, locale)
 }
 
 /**
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateInput.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateInput.kt
new file mode 100644
index 0000000..129ac14
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateInput.kt
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.error
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.input.TransformedText
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.unit.dp
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun DateInputContent(
+    stateData: StateData,
+    dateFormatter: DatePickerFormatter,
+    dateValidator: (Long) -> Boolean,
+) {
+    DateInputTextField(
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(InputTextFieldPadding),
+        stateData = stateData,
+        dateFormatter = dateFormatter,
+        dateValidator = dateValidator
+    )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun DateInputTextField(
+    modifier: Modifier,
+    stateData: StateData,
+    dateFormatter: DatePickerFormatter,
+    dateValidator: (Long) -> Boolean
+) {
+    // Obtain the DateInputFormat for the default Locale.
+    val defaultLocale = defaultLocale()
+    val dateInputFormat = remember(defaultLocale) {
+        stateData.calendarModel.getDateInputFormat(defaultLocale)
+    }
+    var errorText by rememberSaveable { mutableStateOf("") }
+    var text by rememberSaveable(stateSaver = TextFieldValue.Saver) {
+        mutableStateOf(
+            TextFieldValue(
+                text = with(stateData) {
+                    selectedStartDate?.let {
+                        calendarModel.formatWithPattern(
+                            it.utcTimeMillis,
+                            dateInputFormat.patternWithoutDelimiters,
+                            defaultLocale
+                        )
+                    } ?: ""
+                },
+                TextRange(0, 0)
+            )
+        )
+    }
+
+    // Holds a string for displaying an error message when an input does not match the expected date
+    // pattern. The string expects a date pattern string as an argument to be formatted into it.
+    val errorDatePattern = getString(Strings.DateInputInvalidForPattern)
+    // Holds a string for displaying an error message when an input date exceeds the year-range
+    // defined at the DateInput's state. The string expects a start and end year as arguments to
+    // be formatted into it.
+    val errorDateOutOfYearRange = getString(Strings.DateInputInvalidYearRange)
+    // Holds a string for displaying an error message when an input date does not pass the
+    // DateInput's validator check. The string expects a date argument to be formatted into it.
+    val errorInvalidNotAllowed = getString(Strings.DateInputInvalidNotAllowed)
+
+    // Validates the input. Sets an error message at the errorText, or return a non-null
+    // CalendarDate that represents a validated date.
+    fun validate(input: TextFieldValue): CalendarDate? {
+        val dateInputText = input.text.trim()
+        if (dateInputText.isEmpty() ||
+            dateInputText.length < dateInputFormat.patternWithoutDelimiters.length
+        ) {
+            errorText = ""
+            return null
+        }
+        val parsedDate = stateData.calendarModel.parse(
+            dateInputText,
+            dateInputFormat.patternWithoutDelimiters
+        )
+        if (parsedDate == null) {
+            errorText = errorDatePattern.format(dateInputFormat.patternWithDelimiters.uppercase())
+            return null
+        }
+        // Check that the date is within the valid range of years.
+        if (!stateData.yearRange.contains(parsedDate.year)) {
+            errorText = errorDateOutOfYearRange.format(
+                stateData.yearRange.first,
+                stateData.yearRange.last
+            )
+            return null
+        }
+        // Check that the provided date validator allows this date to be selected.
+        if (!dateValidator.invoke(parsedDate.utcTimeMillis)) {
+            errorText = errorInvalidNotAllowed.format(
+                dateFormatter.formatDate(
+                    date = parsedDate,
+                    calendarModel = stateData.calendarModel,
+                    locale = defaultLocale
+                )
+            )
+            return null
+        }
+        return parsedDate
+    }
+
+    OutlinedTextField(
+        value = text,
+        onValueChange = { input ->
+            if (input.text.length <= dateInputFormat.patternWithoutDelimiters.length &&
+                input.text.all { it.isDigit() }
+            ) {
+                text = input
+                stateData.selectedStartDate = validate(input)
+            }
+        },
+        modifier = modifier
+            // Add bottom padding when there is no error. Otherwise, remove it as the error text
+            // will take additional height.
+            .padding(
+                bottom = if (errorText.isNotBlank()) {
+                    0.dp
+                } else {
+                    InputTextNonErroneousBottomPadding
+                }
+            )
+            .semantics {
+                if (errorText.isNotBlank()) error(errorText)
+            },
+        label = { Text(getString(string = Strings.DateInputLabel)) },
+        placeholder = { Text(dateInputFormat.patternWithDelimiters.uppercase()) },
+        supportingText = { if (errorText.isNotBlank()) Text(errorText) },
+        isError = errorText.isNotBlank(),
+        visualTransformation = DateVisualTransformation(dateInputFormat),
+        keyboardOptions = KeyboardOptions(
+            autoCorrect = false,
+            keyboardType = KeyboardType.Number,
+            imeAction = ImeAction.Done
+        ),
+        singleLine = true
+    )
+}
+
+/**
+ * A [VisualTransformation] for date input. The transformation will automatically display the date
+ * delimiters provided by the [DateInputFormat] as the date is being entered into the text field.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+private class DateVisualTransformation(private val dateInputFormat: DateInputFormat) :
+    VisualTransformation {
+
+    private val firstDelimiterOffset: Int =
+        dateInputFormat.patternWithDelimiters.indexOf(dateInputFormat.delimiter)
+    private val secondDelimiterOffset: Int =
+        dateInputFormat.patternWithDelimiters.lastIndexOf(dateInputFormat.delimiter)
+    private val dateFormatLength: Int = dateInputFormat.patternWithoutDelimiters.length
+
+    private val dateOffsetTranslator = object : OffsetMapping {
+
+        override fun originalToTransformed(offset: Int): Int {
+            return when {
+                offset < firstDelimiterOffset -> offset
+                offset < secondDelimiterOffset -> offset + 1
+                offset <= dateFormatLength -> offset + 2
+                else -> dateFormatLength + 2 // 10
+            }
+        }
+
+        override fun transformedToOriginal(offset: Int): Int {
+            return when {
+                offset <= firstDelimiterOffset - 1 -> offset
+                offset <= secondDelimiterOffset - 1 -> offset - 1
+                offset <= dateFormatLength + 1 -> offset - 2
+                else -> dateFormatLength // 8
+            }
+        }
+    }
+
+    override fun filter(text: AnnotatedString): TransformedText {
+        val trimmedText =
+            if (text.text.length > dateFormatLength) {
+                text.text.substring(0 until dateFormatLength)
+            } else {
+                text.text
+            }
+        var transformedText = ""
+        trimmedText.forEachIndexed { index, char ->
+            transformedText += char
+            if (index + 1 == firstDelimiterOffset || index + 2 == secondDelimiterOffset) {
+                transformedText += dateInputFormat.delimiter
+            }
+        }
+        return TransformedText(AnnotatedString(transformedText), dateOffsetTranslator)
+    }
+}
+
+private val InputTextFieldPadding = PaddingValues(
+    start = 12.dp,
+    end = 12.dp,
+    top = 10.dp
+)
+
+// An optional padding that will only be added to the bottom of the date input text field when it's
+// not showing an error message.
+private val InputTextNonErroneousBottomPadding = 16.dp
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 7393f4d..ce5a473 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
@@ -16,6 +16,7 @@
 
 package androidx.compose.material3
 
+import androidx.compose.animation.Crossfade
 import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.core.DecayAnimationSpec
 import androidx.compose.animation.core.Spring
@@ -52,11 +53,12 @@
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.ArrowDropDown
+import androidx.compose.material.icons.filled.DateRange
+import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material.icons.filled.KeyboardArrowLeft
 import androidx.compose.material.icons.filled.KeyboardArrowRight
 import androidx.compose.material3.tokens.DatePickerModalTokens
 import androidx.compose.material3.tokens.MotionTokens
-import androidx.compose.material3.tokens.TypographyKeyTokens
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.Immutable
@@ -70,6 +72,7 @@
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
@@ -82,14 +85,24 @@
 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
 import androidx.compose.ui.unit.dp
 import java.lang.Integer.max
 import java.text.NumberFormat
+import java.util.Locale
 import kotlinx.coroutines.launch
 
 // TODO: External preview image.
@@ -99,49 +112,69 @@
  * Date pickers let people select a date and preferably should be embedded into Dialogs.
  * See [DatePickerDialog].
  *
+ * By default, a date picker lets you pick a date via a calendar UI. However, it also allows
+ * switching into a date input mode for a manual entry of dates using the numbers on a keyboard.
+ *
  * A simple DatePicker looks like:
  * @sample androidx.compose.material3.samples.DatePickerSample
  *
+ * A DatePicker with an initial UI of a date input mode looks like:
+ * @sample androidx.compose.material3.samples.DateInputSample
+ *
  * A DatePicker with validation that blocks certain days from being selected looks like:
  * @sample androidx.compose.material3.samples.DatePickerWithDateValidatorSample
  *
- * @param datePickerState state of the date picker. See [rememberDatePickerState].
+ * @param state state of the date picker. See [rememberDatePickerState].
  * @param modifier the [Modifier] to be applied to this date picker
- * @param dateFormatter a [DatePickerFormatter] that provides formatting patterns for dates display
+ * @param dateFormatter a [DatePickerFormatter] that provides formatting skeletons for dates display
  * @param dateValidator a lambda that takes a date timestamp and return true if the date is a valid
  * one for selection. Invalid dates will appear disabled in the UI.
  * @param title the title to be displayed in the date picker
  * @param headline the headline to be displayed in the date picker
+ * @param showModeToggle indicates if this DatePicker should show a mode toggle action that
+ * transforms it into a date input
  * @param colors [DatePickerColors] that will be used to resolve the colors used for this date
  * picker in different states. See [DatePickerDefaults.colors].
  */
 @ExperimentalMaterial3Api
 @Composable
 fun DatePicker(
-    datePickerState: DatePickerState,
+    state: DatePickerState,
     modifier: Modifier = Modifier,
     dateFormatter: DatePickerFormatter = remember { DatePickerFormatter() },
     dateValidator: (Long) -> Boolean = { true },
-    title: (@Composable () -> Unit)? = { DatePickerDefaults.DatePickerTitle() },
+    title: (@Composable () -> Unit)? = { DatePickerDefaults.DatePickerTitle(state) },
     headline: @Composable () -> Unit = {
         DatePickerDefaults.DatePickerHeadline(
-            datePickerState,
+            state,
             dateFormatter
         )
     },
+    showModeToggle: Boolean = true,
     colors: DatePickerColors = DatePickerDefaults.colors()
-) = DatePickerImpl(
-    modifier = modifier,
-    datePickerState = datePickerState,
-    dateFormatter = dateFormatter,
-    dateValidator = dateValidator,
-    title = title,
-    headline = headline,
-    colors = colors
-)
+) {
+    DateEntryContainer(
+        modifier = modifier,
+        title = title,
+        headline = headline,
+        modeToggleButton = if (showModeToggle) {
+            { DateEntryModeToggleButton(stateData = state.stateData) }
+        } else {
+            null
+        },
+        colors = colors
+    ) {
+        SwitchableDateEntryContent(
+            state = state,
+            dateFormatter = dateFormatter,
+            dateValidator = dateValidator,
+            colors = colors
+        )
+    }
+}
 
 /**
- * Creates a [DatePickerState] for a date picker that is remembered across compositions.
+ * Creates a [DatePickerState] for a [DatePicker] that is remembered across compositions.
  *
  * @param initialSelectedDateMillis timestamp in _UTC_ milliseconds from the epoch that represents
  * an initial selection of a date. Provide a `null` to indicate no selection.
@@ -151,20 +184,23 @@
  * selected date. Otherwise, in case `null` is provided, the displayed month would be the
  * current one.
  * @param yearRange an [IntRange] that holds the year range that the date picker will be limited to
+ * @param initialDisplayMode an initial [DisplayMode] that this state will hold
  */
 @Composable
 @ExperimentalMaterial3Api
 fun rememberDatePickerState(
     @Suppress("AutoBoxing") initialSelectedDateMillis: Long? = null,
     @Suppress("AutoBoxing") initialDisplayedMonthMillis: Long? = initialSelectedDateMillis,
-    yearRange: IntRange = DatePickerDefaults.YearRange
+    yearRange: IntRange = DatePickerDefaults.YearRange,
+    initialDisplayMode: DisplayMode = DisplayMode.Picker
 ): DatePickerState = rememberSaveable(
     saver = DatePickerState.Saver()
 ) {
     DatePickerState(
         initialSelectedDateMillis = initialSelectedDateMillis,
         initialDisplayedMonthMillis = initialDisplayedMonthMillis,
-        yearRange = yearRange
+        yearRange = yearRange,
+        initialDisplayMode = initialDisplayMode
     )
 }
 
@@ -173,27 +209,43 @@
  * [rememberDatePickerState].
  *
  * The state's [selectedDateMillis] will provide a timestamp that represents the _start_ of the day.
- *
- * @param initialSelectedDateMillis timestamp in _UTC_ milliseconds from the epoch that represents
- * an initial selection of a date. Provide a `null` to indicate no selection. Note that the state's
- * [selectedDateMillis] will provide a timestamp that represents the _start_ of the day, which may
- * be different than the provided initialSelectedDateMillis.
- * @param initialDisplayedMonthMillis timestamp in _UTC_ milliseconds from the epoch that represents
- * an initial selection of a month to be displayed to the user. In case `null` is provided, the
- * displayed month would be the current one.
- * @param yearRange an [IntRange] that holds the year range that the date picker will be limited to
- * @see rememberDatePickerState
- * @throws [IllegalArgumentException] if the initial selected date or displayed month represent a
- * year that is out of the year range.
  */
 @ExperimentalMaterial3Api
 @Stable
-class DatePickerState constructor(
-    @Suppress("AutoBoxing") initialSelectedDateMillis: Long?,
-    @Suppress("AutoBoxing") initialDisplayedMonthMillis: Long?,
-    val yearRange: IntRange
-) {
-    internal val calendarModel: CalendarModel = createCalendarModel()
+class DatePickerState private constructor(internal val stateData: StateData) {
+
+    /**
+     * Constructs a DatePickerState.
+     *
+     * @param initialSelectedDateMillis timestamp in _UTC_ milliseconds from the epoch that
+     * represents an initial selection of a date. Provide a `null` to indicate no selection. Note
+     * that the state's
+     * [selectedDateMillis] will provide a timestamp that represents the _start_ of the day, which
+     * may be different than the provided initialSelectedDateMillis.
+     * @param initialDisplayedMonthMillis timestamp in _UTC_ milliseconds from the epoch that
+     * represents an initial selection of a month to be displayed to the user. In case `null` is
+     * provided, the displayed month would be the current one.
+     * @param yearRange an [IntRange] that holds the year range that the date picker will be limited
+     * to
+     * @param initialDisplayMode an initial [DisplayMode] that this state will hold
+     * @see rememberDatePickerState
+     * @throws [IllegalArgumentException] if the initial selected date or displayed month represent
+     * a year that is out of the year range.
+     */
+    constructor(
+        @Suppress("AutoBoxing") initialSelectedDateMillis: Long?,
+        @Suppress("AutoBoxing") initialDisplayedMonthMillis: Long?,
+        yearRange: IntRange,
+        initialDisplayMode: DisplayMode
+    ) : this(
+        StateData(
+            initialSelectedStartDateMillis = initialSelectedDateMillis,
+            initialSelectedEndDateMillis = null,
+            initialDisplayedMonthMillis = initialDisplayedMonthMillis,
+            yearRange = yearRange,
+            initialDisplayMode = initialDisplayMode,
+        )
+    )
 
     /**
      * A timestamp that represents the _start_ of the day of the selected date in _UTC_ milliseconds
@@ -203,87 +255,22 @@
      */
     @get:Suppress("AutoBoxing")
     val selectedDateMillis by derivedStateOf {
-        selectedDate?.utcTimeMillis
+        stateData.selectedStartDate?.utcTimeMillis
     }
 
     /**
-     * A mutable state of [CalendarDate] that represents the selected date.
+     * A mutable state of [DisplayMode] that represents the current display mode of the UI
+     * (i.e. picker or input).
      */
-    internal var selectedDate by mutableStateOf(
-        if (initialSelectedDateMillis != null) {
-            val date = calendarModel.getCanonicalDate(
-                initialSelectedDateMillis
-            )
-            require(yearRange.contains(date.year)) {
-                "The initial selected date's year (${date.year}) is out of the years range of " +
-                    "$yearRange."
-            }
-            date
-        } else {
-            null
-        }
-    )
-
-    /**
-     * A mutable state for the month that is displayed to the user. In case an initial month was not
-     * provided, the current month will be the one to be displayed.
-     */
-    internal var displayedMonth by mutableStateOf(
-        if (initialDisplayedMonthMillis != null) {
-            val month = calendarModel.getMonth(initialDisplayedMonthMillis)
-            require(yearRange.contains(month.year)) {
-                "The initial display month's year (${month.year}) is out of the years range of " +
-                    "$yearRange."
-            }
-            month
-        } else {
-            currentMonth
-        }
-    )
-
-    /**
-     * The current [CalendarMonth] that represents the present's day month.
-     */
-    internal val currentMonth: CalendarMonth
-        get() = calendarModel.getMonth(calendarModel.today)
-
-    /**
-     * The displayed month index within the total months at the defined years range.
-     *
-     * @see [displayedMonth]
-     * @see [yearRange]
-     */
-    internal val displayedMonthIndex: Int
-        get() = displayedMonth.indexIn(yearRange)
-
-    /**
-     * The total month count for the defined years range.
-     *
-     * @see [yearRange]
-     */
-    internal val totalMonthsInRange: Int
-        get() = (yearRange.last - yearRange.first + 1) * 12
+    var displayMode by stateData.displayMode
 
     companion object {
         /**
          * The default [Saver] implementation for [DatePickerState].
          */
         fun Saver(): Saver<DatePickerState, *> = Saver(
-            save = {
-                listOf(
-                    it.selectedDateMillis,
-                    it.displayedMonth.startUtcTimeMillis,
-                    it.yearRange.first,
-                    it.yearRange.last
-                )
-            },
-            restore = { value ->
-                DatePickerState(
-                    initialSelectedDateMillis = value[0] as Long?,
-                    initialDisplayedMonthMillis = value[1] as Long?,
-                    yearRange = IntRange(value[2] as Int, value[3] as Int)
-                )
-            }
+            save = { with(StateData.Saver()) { save(it.stateData) } },
+            restore = { value -> DatePickerState(with(StateData.Saver()) { restore(value)!! }) }
         )
     }
 }
@@ -292,6 +279,7 @@
  * Contains default values used by the date pickers.
  */
 @ExperimentalMaterial3Api
+@Stable
 object DatePickerDefaults {
 
     /**
@@ -370,25 +358,29 @@
             todayDateBorderColor = todayDateBorderColor
         )
 
-    /** A default date picker title composable. */
+    /**
+     * A default date picker title composable.
+     *
+     * @param state a [DatePickerState] that will help determine the title's content
+     */
     @Composable
-    fun DatePickerTitle() = Text(getString(string = Strings.DatePickerTitle))
+    fun DatePickerTitle(state: DatePickerState) {
+        when (state.displayMode) {
+            DisplayMode.Picker -> Text(getString(string = Strings.DatePickerTitle))
+            DisplayMode.Input -> Text(getString(string = Strings.DateInputTitle))
+        }
+    }
 
     /**
      * A default date picker headline composable lambda that displays a default headline text when
      * there is no date selection, and an actual date string when there is.
+     *
+     * @param state a [DatePickerState] that will help determine the title's headline
+     * @param dateFormatter a [DatePickerFormatter]
      */
     @Composable
     fun DatePickerHeadline(state: DatePickerState, dateFormatter: DatePickerFormatter) {
-        val formattedDate = dateFormatter.formatDate(
-            date = state.selectedDate,
-            calendarModel = state.calendarModel
-        )
-        if (formattedDate == null) {
-            Text(getString(string = Strings.DatePickerHeadline), maxLines = 1)
-        } else {
-            Text(formattedDate, maxLines = 1)
-        }
+        DateEntryHeadline(stateData = state.stateData, dateFormatter = dateFormatter)
     }
 
     /**
@@ -422,6 +414,23 @@
 
     /** The default shape for date picker dialogs. */
     val shape: Shape @Composable get() = DatePickerModalTokens.ContainerShape.toShape()
+
+    /**
+     * A date format skeleton used to format the date picker's year selection menu button (e.g.
+     * "March 2021")
+     */
+    const val YearMonthSkeleton: String = "yMMMM"
+
+    /**
+     * A date format skeleton used to format a selected date (e.g. "Mar 27, 2021")
+     */
+    const val YearAbbrMonthDaySkeleton: String = "yMMMd"
+
+    /**
+     * A date format skeleton used to format a selected date to be used as content description for
+     * screen readers (e.g. "Saturday, March 27, 2021")
+     */
+    const val YearMonthWeekdayDaySkeleton: String = "yMMMMEEEEd"
 }
 
 /**
@@ -592,71 +601,260 @@
 /**
  * A date formatter used by [DatePicker].
  *
- * @param shortFormat date format for displaying a date in a short length string, and is used
- * when a selected date is at the current year (e.g. Mon, Aug 17)
- * @param mediumFormat date format for displaying a date in a medium length string, and is used
- * when a selected date is at a different year (e.g. Sep 17, 1999)
- * @param monthYearFormat date format for displaying a date as a month and a year (e.g.
- * September 2022)
+ * The date formatter will apply the best possible localized form of the given skeleton and Locale.
+ * A skeleton is similar to, and uses the same format characters as, a Unicode
+ * <a href="http://www.unicode.org/reports/tr35/#Date_Format_Patterns">UTS #35</a> pattern.
+ *
+ * One difference is that order is irrelevant. For example, "MMMMd" will return "MMMM d" in the
+ * `en_US` locale, but "d. MMMM" in the `de_CH` locale.
+ *
+ * @param yearSelectionSkeleton a date format skeleton used to format the date picker's year
+ * selection menu button (e.g. "March 2021").
+ * @param selectedDateSkeleton a date format skeleton used to format a selected date (e.g.
+ * "Mar 27, 2021")
+ * @param selectedDateDescriptionSkeleton a date format skeleton used to format a selected date to
+ * be used as content description for screen readers (e.g. "Saturday, March 27, 2021")
  */
 @ExperimentalMaterial3Api
 @Immutable
 class DatePickerFormatter constructor(
-    internal val shortFormat: String = "E, MMM d", // e.g. Mon, Aug 17
-    internal val mediumFormat: String = "MMM d, yyyy", // e.g. Aug 17, 2022,
-    internal val monthYearFormat: String = "MMMM yyyy", // e.g. September 2022
+    internal val yearSelectionSkeleton: String = DatePickerDefaults.YearMonthSkeleton,
+    internal val selectedDateSkeleton: String = DatePickerDefaults.YearAbbrMonthDaySkeleton,
+    internal val selectedDateDescriptionSkeleton: String =
+        DatePickerDefaults.YearMonthWeekdayDaySkeleton
 ) {
 
-    internal fun formatMonthYear(month: CalendarMonth?, calendarModel: CalendarModel): String? {
+    internal fun formatMonthYear(
+        month: CalendarMonth?,
+        calendarModel: CalendarModel,
+        locale: Locale
+    ): String? {
         if (month == null) return null
-        return calendarModel.format(month, monthYearFormat)
+        return calendarModel.formatWithSkeleton(month, yearSelectionSkeleton, locale)
     }
 
-    internal fun formatDate(date: CalendarDate?, calendarModel: CalendarModel): String? {
+    internal fun formatDate(
+        date: CalendarDate?,
+        calendarModel: CalendarModel,
+        locale: Locale,
+        forContentDescription: Boolean = false
+    ): String? {
         if (date == null) return null
-        val pattern = if (calendarModel.today.year == date.year) {
-            shortFormat
-        } else {
-            mediumFormat
-        }
-        return calendarModel.format(date, pattern)
+        return calendarModel.formatWithSkeleton(
+            date, if (forContentDescription) {
+                selectedDateDescriptionSkeleton
+            } else {
+                selectedDateSkeleton
+            },
+            locale
+        )
     }
 
     override fun equals(other: Any?): Boolean {
         if (other !is DatePickerFormatter) return false
 
-        if (monthYearFormat != other.monthYearFormat) return false
-        if (shortFormat != other.shortFormat) return false
-        if (mediumFormat != other.mediumFormat) return false
+        if (yearSelectionSkeleton != other.yearSelectionSkeleton) return false
+        if (selectedDateSkeleton != other.selectedDateSkeleton) return false
+        if (selectedDateDescriptionSkeleton != other.selectedDateDescriptionSkeleton) return false
 
         return true
     }
 
     override fun hashCode(): Int {
-        var result = monthYearFormat.hashCode()
-        result = 31 * result + shortFormat.hashCode()
-        result = 31 * result + mediumFormat.hashCode()
+        var result = yearSelectionSkeleton.hashCode()
+        result = 31 * result + selectedDateSkeleton.hashCode()
+        result = 31 * result + selectedDateDescriptionSkeleton.hashCode()
         return result
     }
 }
 
+/**
+ * Represents the different modes that a date picker can be at.
+ */
+@Immutable
+@JvmInline
 @ExperimentalMaterial3Api
+value class DisplayMode internal constructor(internal val value: Int) {
+
+    companion object {
+        /** Date picker mode */
+        val Picker = DisplayMode(0)
+
+        /** Date text input mode */
+        val Input = DisplayMode(1)
+    }
+
+    override fun toString() = when (this) {
+        Picker -> "Picker"
+        Input -> "Input"
+        else -> "Unknown"
+    }
+}
+
+/**
+ * Holds the state's data for the date picker.
+ *
+ * Note that the internal representation is capable of holding a start and end date. However, the
+ * the [DatePickerState] and the [DateRangePickerState] that use this class will only expose
+ * publicly the relevant functionality for their purpose.
+ *
+ * @param initialSelectedStartDateMillis timestamp in _UTC_ milliseconds from the epoch that
+ * represents an initial selection of a start date. Provide a `null` to indicate no selection.
+ * @param initialSelectedEndDateMillis timestamp in _UTC_ milliseconds from the epoch that
+ * represents an initial selection of an end date. Provide a `null` to indicate no selection. This
+ * value will be ignored in case it's smaller or equals to the initial start value.
+ * @param initialDisplayedMonthMillis timestamp in _UTC_ milliseconds from the epoch that represents
+ * an initial selection of a month to be displayed to the user. In case `null` is provided, the
+ * displayed month would be the current one.
+ * @param yearRange an [IntRange] that holds the year range that the date picker will be limited to
+ * @param initialDisplayMode an initial [DisplayMode] that this state will hold
+ * @see rememberDatePickerState
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Stable
+internal class StateData constructor(
+    initialSelectedStartDateMillis: Long?,
+    initialSelectedEndDateMillis: Long?,
+    initialDisplayedMonthMillis: Long?,
+    val yearRange: IntRange,
+    initialDisplayMode: DisplayMode,
+) {
+
+    val calendarModel: CalendarModel = CalendarModel()
+
+    /**
+     * A mutable state of [CalendarDate] that represents the start date for a selection.
+     */
+    internal var selectedStartDate by mutableStateOf(
+        if (initialSelectedStartDateMillis != null) {
+            val date = calendarModel.getCanonicalDate(
+                initialSelectedStartDateMillis
+            )
+            require(yearRange.contains(date.year)) {
+                "The initial selected start date's year (${date.year}) is out of the years range " +
+                    "of $yearRange."
+            }
+            date
+        } else {
+            null
+        }
+    )
+
+    /**
+     * A mutable state of [CalendarDate] that represents the end date for a selection.
+     *
+     * Single date selection states that use this [StateData] should always have this as `null`.
+     */
+    internal var selectedEndDate by mutableStateOf(
+        // Set to null in case the provided value is "undefined" or <= than the start date.
+        if (initialSelectedEndDateMillis != null &&
+            initialSelectedStartDateMillis != null &&
+            initialSelectedEndDateMillis > initialSelectedStartDateMillis
+        ) {
+            val date = calendarModel.getCanonicalDate(
+                initialSelectedEndDateMillis
+            )
+            require(yearRange.contains(date.year)) {
+                "The initial selected end date's year (${date.year}) is out of the years range " +
+                    "of $yearRange."
+            }
+            date
+        } else {
+            null
+        }
+    )
+
+    /**
+     * A mutable state for the month that is displayed to the user. In case an initial month was not
+     * provided, the current month will be the one to be displayed.
+     */
+    internal var displayedMonth by mutableStateOf(
+        if (initialDisplayedMonthMillis != null) {
+            val month = calendarModel.getMonth(initialDisplayedMonthMillis)
+            require(yearRange.contains(month.year)) {
+                "The initial display month's year (${month.year}) is out of the years range of " +
+                    "$yearRange."
+            }
+            month
+        } else {
+            currentMonth
+        }
+    )
+
+    /**
+     * The current [CalendarMonth] that represents the present's day month.
+     */
+    internal val currentMonth: CalendarMonth
+        get() = calendarModel.getMonth(calendarModel.today)
+
+    /**
+     * A mutable state of [DisplayMode] that represents the current display mode of the UI
+     * (i.e. picker or input).
+     */
+    internal var displayMode = mutableStateOf(initialDisplayMode)
+
+    /**
+     * The displayed month index within the total months at the defined years range.
+     *
+     * @see [displayedMonth]
+     * @see [yearRange]
+     */
+    internal val displayedMonthIndex: Int
+        get() = displayedMonth.indexIn(yearRange)
+
+    /**
+     * The total month count for the defined years range.
+     *
+     * @see [yearRange]
+     */
+    internal val totalMonthsInRange: Int
+        get() = (yearRange.last - yearRange.first + 1) * 12
+
+    companion object {
+        /**
+         * A [Saver] implementation for [StateData].
+         */
+        fun Saver(): Saver<StateData, Any> = listSaver(
+            save = {
+                listOf(
+                    it.selectedStartDate?.utcTimeMillis,
+                    it.selectedEndDate?.utcTimeMillis,
+                    it.displayedMonth.startUtcTimeMillis,
+                    it.yearRange.first,
+                    it.yearRange.last,
+                    it.displayMode.value.value
+                )
+            },
+            restore = { value ->
+                StateData(
+                    initialSelectedStartDateMillis = value[0] as Long?,
+                    initialSelectedEndDateMillis = value[1] as Long?,
+                    initialDisplayedMonthMillis = value[2] as Long?,
+                    yearRange = IntRange(value[3] as Int, value[4] as Int),
+                    initialDisplayMode = DisplayMode(value[5] as Int)
+                )
+            }
+        )
+    }
+}
+
+/**
+ * A base container for the date picker and the date input. This container composes the top common
+ * area of the UI, and accepts [content] for the actual calendar picker or text field input.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
-private fun DatePickerImpl(
+internal fun DateEntryContainer(
     modifier: Modifier,
-    datePickerState: DatePickerState,
-    dateFormatter: DatePickerFormatter,
-    dateValidator: (Long) -> Boolean,
     title: (@Composable () -> Unit)?,
     headline: @Composable () -> Unit,
-    colors: DatePickerColors
+    modeToggleButton: (@Composable () -> Unit)?,
+    colors: DatePickerColors,
+    content: @Composable () -> Unit
 ) {
-    val monthsListState =
-        rememberLazyListState(initialFirstVisibleItemIndex = datePickerState.displayedMonthIndex)
-    val coroutineScope = rememberCoroutineScope()
     Column(
         modifier = modifier
-            .sizeIn(minWidth = ContainerWidth)
+            .sizeIn(minWidth = DatePickerModalTokens.ContainerWidth)
             .padding(DatePickerHorizontalPadding)
     ) {
         DatePickerHeader(
@@ -666,23 +864,147 @@
             headlineContentColor = colors.headlineContentColor
         ) {
             headline()
+            modeToggleButton?.invoke()
         }
-
         Divider()
+        content()
+    }
+}
 
-        val onDateSelected = { dateInMillis: Long ->
-            datePickerState.selectedDate =
-                datePickerState.calendarModel.getCanonicalDate(dateInMillis)
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun DateEntryModeToggleButton(stateData: StateData) {
+    with(stateData) {
+        if (displayMode.value == DisplayMode.Picker) {
+            IconButton(onClick = {
+                displayMode.value = DisplayMode.Input
+            }) {
+                Icon(
+                    imageVector = Icons.Filled.Edit,
+                    contentDescription = getString(Strings.DatePickerSwitchToInputMode)
+                )
+            }
+        } else {
+            IconButton(
+                onClick = {
+                    // Update the displayed month, if needed, and change the mode to a
+                    // date-picker.
+                    selectedStartDate?.let { displayedMonth = calendarModel.getMonth(it) }
+                    displayMode.value = DisplayMode.Picker
+                }
+            ) {
+                Icon(
+                    imageVector = Icons.Filled.DateRange,
+                    contentDescription = getString(Strings.DatePickerSwitchToCalendarMode)
+                )
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun DateEntryHeadline(stateData: StateData, dateFormatter: DatePickerFormatter) {
+    with(stateData) {
+        val defaultLocale = defaultLocale()
+        val formattedDate = dateFormatter.formatDate(
+            date = selectedStartDate,
+            calendarModel = calendarModel,
+            locale = defaultLocale
+        )
+        val verboseDateDescription = dateFormatter.formatDate(
+            date = selectedStartDate,
+            calendarModel = calendarModel,
+            locale = defaultLocale,
+            forContentDescription = true
+        ) ?: when (displayMode.value) {
+            DisplayMode.Picker -> getString(Strings.DatePickerNoSelectionDescription)
+            DisplayMode.Input -> getString(Strings.DateInputNoInputDescription)
+            else -> ""
         }
 
-        var yearPickerVisible by rememberSaveable { mutableStateOf(false) }
+        val headlineText = formattedDate ?: when (displayMode.value) {
+            DisplayMode.Picker -> getString(Strings.DatePickerHeadline)
+            DisplayMode.Input -> getString(Strings.DateInputHeadline)
+            else -> ""
+        }
+
+        val headlineDescription = when (displayMode.value) {
+            DisplayMode.Picker -> getString(Strings.DatePickerHeadlineDescription)
+            DisplayMode.Input -> getString(Strings.DateInputHeadlineDescription)
+            else -> ""
+        }.format(verboseDateDescription)
+        Text(
+            text = headlineText,
+            modifier = Modifier.semantics {
+                liveRegion = LiveRegionMode.Polite
+                contentDescription = headlineDescription
+            },
+            maxLines = 1
+        )
+    }
+}
+
+/**
+ * Date entry content that displays a [DatePickerContent] or a [DateInputContent] according to the
+ * state's display mode.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun SwitchableDateEntryContent(
+    state: DatePickerState,
+    dateFormatter: DatePickerFormatter,
+    dateValidator: (Long) -> Boolean,
+    colors: DatePickerColors
+) {
+    // TODO(b/266480386): Apply the motion spec for this once we have it. Consider replacing this
+    //  with AnimatedContent when it's out of experimental.
+    Crossfade(targetState = state.displayMode, animationSpec = spring()) { mode ->
+        when (mode) {
+            DisplayMode.Picker -> DatePickerContent(
+                stateData = state.stateData,
+                dateFormatter = dateFormatter,
+                dateValidator = dateValidator,
+                colors = colors
+            )
+
+            DisplayMode.Input -> DateInputContent(
+                stateData = state.stateData,
+                dateFormatter = dateFormatter,
+                dateValidator = dateValidator,
+            )
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun DatePickerContent(
+    stateData: StateData,
+    dateFormatter: DatePickerFormatter,
+    dateValidator: (Long) -> Boolean,
+    colors: DatePickerColors
+) {
+    val monthsListState =
+        rememberLazyListState(initialFirstVisibleItemIndex = stateData.displayedMonthIndex)
+    val coroutineScope = rememberCoroutineScope()
+
+    val onDateSelected = { dateInMillis: Long ->
+        stateData.selectedStartDate =
+            stateData.calendarModel.getCanonicalDate(dateInMillis)
+    }
+
+    var yearPickerVisible by rememberSaveable { mutableStateOf(false) }
+    val defaultLocale = defaultLocale()
+    Column {
         MonthsNavigation(
             nextAvailable = monthsListState.canScrollForward,
             previousAvailable = monthsListState.canScrollBackward,
             yearPickerVisible = yearPickerVisible,
             yearPickerText = dateFormatter.formatMonthYear(
-                datePickerState.displayedMonth,
-                datePickerState.calendarModel
+                month = stateData.displayedMonth,
+                calendarModel = stateData.calendarModel,
+                locale = defaultLocale
             ) ?: "-",
             onNextClicked = {
                 coroutineScope.launch {
@@ -703,11 +1025,12 @@
 
         Box {
             Column {
-                WeekDays(colors, datePickerState.calendarModel)
+                WeekDays(colors, stateData.calendarModel)
                 HorizontalMonthsList(
                     onDateSelected = onDateSelected,
-                    datePickerState = datePickerState,
+                    stateData = stateData,
                     lazyListState = monthsListState,
+                    dateFormatter = dateFormatter,
                     dateValidator = dateValidator,
                     colors = colors
                 )
@@ -718,7 +1041,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
@@ -734,7 +1062,7 @@
                                 // Scroll to the selected year (maintaining the month of year).
                                 // A LaunchEffect at the MonthsList will take care of rest and will
                                 // update the state's displayedMonth to the month we scrolled to.
-                                with(datePickerState) {
+                                with(stateData) {
                                     monthsListState.scrollToItem(
                                         (year - yearRange.first) * 12 + displayedMonth.month - 1
                                     )
@@ -742,7 +1070,7 @@
                             }
                         },
                         colors = colors,
-                        datePickerState = datePickerState
+                        stateData = stateData
                     )
                     Divider()
                 }
@@ -752,7 +1080,7 @@
 }
 
 @Composable
-private fun DatePickerHeader(
+internal fun DatePickerHeader(
     modifier: Modifier,
     title: (@Composable () -> Unit)?,
     titleContentColor: Color,
@@ -768,8 +1096,10 @@
     ) {
         if (title != null) {
             CompositionLocalProvider(LocalContentColor provides titleContentColor) {
-                // TODO: Use the value from the tokens, once updated (b/251240936).
-                val textStyle = MaterialTheme.typography.fromToken(HeaderSupportingTextFont)
+                val textStyle =
+                    MaterialTheme.typography.fromToken(
+                        DatePickerModalTokens.HeaderSupportingTextFont
+                    )
                 ProvideTextStyle(textStyle) {
                     Box(contentAlignment = Alignment.BottomStart) {
                         title()
@@ -783,7 +1113,7 @@
             ProvideTextStyle(textStyle) {
                 Row(
                     modifier = Modifier.fillMaxWidth(),
-                    horizontalArrangement = Arrangement.Start,
+                    horizontalArrangement = Arrangement.SpaceBetween,
                     verticalAlignment = Alignment.CenterVertically,
                     content = content
                 )
@@ -799,27 +1129,34 @@
 @Composable
 private fun HorizontalMonthsList(
     onDateSelected: (dateInMillis: Long) -> Unit,
-    datePickerState: DatePickerState,
+    stateData: StateData,
     lazyListState: LazyListState,
+    dateFormatter: DatePickerFormatter,
     dateValidator: (Long) -> Boolean,
     colors: DatePickerColors,
 ) {
-    val today = datePickerState.calendarModel.today
-    val firstMonth = remember(datePickerState.yearRange) {
-        datePickerState.calendarModel.getMonth(
-            year = datePickerState.yearRange.first,
+    val today = stateData.calendarModel.today
+    val firstMonth = remember(stateData.yearRange) {
+        stateData.calendarModel.getMonth(
+            year = stateData.yearRange.first,
             month = 1 // January
         )
     }
     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
         flingBehavior = DatePickerDefaults.rememberSnapFlingBehavior(lazyListState)
     ) {
-        items(datePickerState.totalMonthsInRange) {
+        items(stateData.totalMonthsInRange) {
             val month =
-                datePickerState.calendarModel.plusMonths(
+                stateData.calendarModel.plusMonths(
                     from = firstMonth,
                     addedMonthsCount = it
                 )
@@ -830,7 +1167,8 @@
                     month = month,
                     onDateSelected = onDateSelected,
                     today = today,
-                    selectedDate = datePickerState.selectedDate,
+                    selectedDate = stateData.selectedStartDate,
+                    dateFormatter = dateFormatter,
                     dateValidator = dateValidator,
                     colors = colors
                 )
@@ -842,7 +1180,7 @@
         snapshotFlow { lazyListState.firstVisibleItemIndex }.collect {
             val yearOffset = lazyListState.firstVisibleItemIndex / 12
             val month = lazyListState.firstVisibleItemIndex % 12 + 1
-            with(datePickerState) {
+            with(stateData) {
                 if (displayedMonth.month != month ||
                     displayedMonth.year != yearRange.first + yearOffset
                 ) {
@@ -873,8 +1211,8 @@
         dayNames.add(weekdays[i])
     }
     CompositionLocalProvider(LocalContentColor provides colors.weekdayContentColor) {
-        // TODO: Use the value from the tokens, once updated (b/251240936).
-        val textStyle = MaterialTheme.typography.fromToken(WeekdaysLabelTextFont)
+        val textStyle =
+            MaterialTheme.typography.fromToken(DatePickerModalTokens.WeekdaysLabelTextFont)
         ProvideTextStyle(value = textStyle) {
             Row(
                 modifier = Modifier
@@ -917,13 +1255,14 @@
     today: CalendarDate,
     selectedDate: CalendarDate?,
     dateValidator: (Long) -> Boolean,
+    dateFormatter: DatePickerFormatter,
     colors: DatePickerColors
 ) {
+    val todayDescription = getString(string = Strings.DatePickerTodayDescription)
     ProvideTextStyle(
-        // TODO: Use the value from the tokens, once updated (b/251240936).
-        MaterialTheme.typography.fromToken(DateLabelTextFont)
+        MaterialTheme.typography.fromToken(DatePickerModalTokens.DateLabelTextFont)
     ) {
-        var cellsCount = 0
+        var cellIndex = 0
         Column(
             modifier = Modifier
                 .requiredHeight(RecommendedSizeForAccessibility * MaxCalendarRows),
@@ -936,35 +1275,54 @@
                     verticalAlignment = Alignment.CenterVertically
                 ) {
                     repeat(DaysInWeek) {
-                        if (cellsCount < month.daysFromStartOfWeekToFirstOfMonth ||
-                            cellsCount >=
+                        if (cellIndex < month.daysFromStartOfWeekToFirstOfMonth ||
+                            cellIndex >=
                             (month.daysFromStartOfWeekToFirstOfMonth + month.numberOfDays)
                         ) {
                             // Empty cell
-                            Box(
+                            Spacer(
                                 modifier = Modifier.requiredSize(
                                     width = RecommendedSizeForAccessibility,
                                     height = RecommendedSizeForAccessibility
                                 )
                             )
                         } else {
-                            // TODO a11y should announce the day and whether it's selected or not.
-                            val dayNumber = cellsCount - month.daysFromStartOfWeekToFirstOfMonth
+                            val dayNumber = cellIndex - month.daysFromStartOfWeekToFirstOfMonth
                             val dateInMillis = month.startUtcTimeMillis +
                                 (dayNumber * MillisecondsIn24Hours)
+                            val isToday = dateInMillis == today.utcTimeMillis
                             Day(
-                                checked = dateInMillis == selectedDate?.utcTimeMillis,
-                                onCheckedChange = { onDateSelected(dateInMillis) },
+                                modifier = Modifier.semantics {
+                                    role = Role.Button
+                                    if (isToday) {
+                                        contentDescription = todayDescription
+                                    }
+                                },
+                                selected = dateInMillis == selectedDate?.utcTimeMillis,
+                                onClick = { onDateSelected(dateInMillis) },
                                 animateChecked = true,
                                 enabled = remember(dateInMillis) {
                                     dateValidator.invoke(dateInMillis)
                                 },
-                                today = dateInMillis == today.utcTimeMillis,
-                                text = (dayNumber + 1).toLocalString(),
+                                today = isToday,
                                 colors = colors
-                            )
+                            ) {
+                                val defaultLocale = defaultLocale()
+                                Text(
+                                    text = (dayNumber + 1).toLocalString(),
+                                    modifier = Modifier.semantics {
+                                        contentDescription =
+                                            formatWithSkeleton(
+                                                dateInMillis,
+                                                dateFormatter.selectedDateDescriptionSkeleton,
+                                                defaultLocale
+                                            )
+                                    },
+                                    textAlign = TextAlign.Center
+                                )
+                            }
                         }
-                        cellsCount++
+                        cellIndex++
                     }
                 }
             }
@@ -975,18 +1333,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,
@@ -995,16 +1354,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
@@ -1014,10 +1373,7 @@
         }
     ) {
         Box(contentAlignment = Alignment.Center) {
-            Text(
-                text = text,
-                textAlign = TextAlign.Center
-            )
+            content()
         }
     }
 }
@@ -1028,19 +1384,19 @@
     modifier: Modifier,
     onYearSelected: (year: Int) -> Unit,
     colors: DatePickerColors,
-    datePickerState: DatePickerState
+    stateData: StateData
 ) {
     ProvideTextStyle(
         value = MaterialTheme.typography.fromToken(DatePickerModalTokens.SelectionYearLabelTextFont)
     ) {
-        val currentYear = datePickerState.currentMonth.year
-        val displayedYear = datePickerState.displayedMonth.year
+        val currentYear = stateData.currentMonth.year
+        val displayedYear = stateData.displayedMonth.year
         val lazyGridState =
             rememberLazyGridState(
                 // Set the initial index to a few years before the current year to allow quicker
                 // selection of previous years.
                 initialFirstVisibleItemIndex = max(
-                    0, displayedYear - datePickerState.yearRange.first - YearsInRow
+                    0, displayedYear - stateData.yearRange.first - YearsInRow
                 )
             )
         // Match the years container color to any elevated surface color that is composed under it.
@@ -1051,29 +1407,41 @@
         }
         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)
         ) {
-            items(datePickerState.yearRange.count()) {
-                val selectedYear = it + datePickerState.yearRange.first
+            items(stateData.yearRange.count()) {
+                val selectedYear = it + stateData.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
                     )
                 }
@@ -1085,15 +1453,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,
@@ -1104,14 +1472,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
     ) {
@@ -1151,7 +1519,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) {
@@ -1223,10 +1597,6 @@
     return formatter.format(this)
 }
 
-// TODO: Remove after b/247694457 for updating the tokens is resolved.
-internal val ContainerWidth = 360.dp
-internal val ContainerHeight = 568.dp
-
 internal val MonthYearHeight = 56.dp
 internal val DatePickerHorizontalPadding = PaddingValues(horizontal = 12.dp)
 internal val HeaderPadding = PaddingValues(
@@ -1240,9 +1610,4 @@
 private const val MaxCalendarRows = 6
 private const val YearsInRow: Int = 3
 
-// TODO: Remove after b/251240936 for updating the typography is resolved.
-private val WeekdaysLabelTextFont = TypographyKeyTokens.BodyLarge
-private val DateLabelTextFont = TypographyKeyTokens.BodyLarge
-private val HeaderSupportingTextFont = TypographyKeyTokens.LabelLarge
-
 private val RecommendedSizeForAccessibility = 48.dp
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateRangePicker.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateRangePicker.kt
new file mode 100644
index 0000000..b6145ab
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateRangePicker.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+
+/**
+ * Creates a [DateRangePickerState] for a [DateRangePicker] that is remembered across compositions.
+ *
+ * @param initialSelectedStartDateMillis timestamp in _UTC_ milliseconds from the epoch that
+ * represents an initial selection of a start date. Provide a `null` to indicate no selection.
+ * @param initialSelectedEndDateMillis timestamp in _UTC_ milliseconds from the epoch that
+ * represents an initial selection of an end date. Provide a `null` to indicate no selection.
+ * @param initialDisplayedMonthMillis timestamp in _UTC_ milliseconds from the epoch that represents
+ * an initial selection of a month to be displayed to the user. By default, in case an
+ * `initialSelectedStartDateMillis` is provided, the initial displayed month would be the month of
+ * the selected date. Otherwise, in case `null` is provided, the displayed month would be the
+ * current one.
+ * @param yearRange an [IntRange] that holds the year range that the date picker will be limited to
+ * @param initialDisplayMode an initial [DisplayMode] that this state will hold
+ */
+@Composable
+@ExperimentalMaterial3Api
+internal fun rememberDateRangePickerState(
+    @Suppress("AutoBoxing") initialSelectedStartDateMillis: Long? = null,
+    @Suppress("AutoBoxing") initialSelectedEndDateMillis: Long? = null,
+    @Suppress("AutoBoxing") initialDisplayedMonthMillis: Long? =
+        initialSelectedStartDateMillis,
+    yearRange: IntRange = DatePickerDefaults.YearRange,
+    initialDisplayMode: DisplayMode = DisplayMode.Picker
+): DateRangePickerState = rememberSaveable(
+    saver = DateRangePickerState.Saver()
+) {
+    DateRangePickerState(
+        initialSelectedStartDateMillis = initialSelectedStartDateMillis,
+        initialSelectedEndDateMillis = initialSelectedEndDateMillis,
+        initialDisplayedMonthMillis = initialDisplayedMonthMillis,
+        yearRange = yearRange,
+        initialDisplayMode = initialDisplayMode
+    )
+}
+
+/**
+ * A state object that can be hoisted to observe the date picker state. See
+ * [rememberDateRangePickerState].
+ *
+ * The state's [selectedStartDateMillis] and [selectedEndDateMillis] will provide timestamps for the
+ * _beginning_ of the selected days (i.e. midnight in _UTC_ milliseconds from the epoch).
+ */
+@ExperimentalMaterial3Api
+@Stable
+internal class DateRangePickerState private constructor(internal val stateData: StateData) {
+
+    /**
+     * Constructs a DateRangePickerState.
+     *
+     * @param initialSelectedStartDateMillis timestamp in _UTC_ milliseconds from the epoch that
+     * represents an initial selection of a start date. Provide a `null` to indicate no selection.
+     * @param initialSelectedEndDateMillis timestamp in _UTC_ milliseconds from the epoch that
+     * represents an initial selection of an end date. Provide a `null` to indicate no selection.
+     * @param initialDisplayedMonthMillis timestamp in _UTC_ milliseconds from the epoch that
+     * represents an initial selection of a month to be displayed to the user. By default, in case
+     * an `initialSelectedStartDateMillis` is provided, the initial displayed month would be the
+     * month of the selected date. Otherwise, in case `null` is provided, the displayed month would
+     * be the current one.
+     * @param yearRange an [IntRange] that holds the year range that the date picker will be limited
+     * to
+     * @param initialDisplayMode an initial [DisplayMode] that this state will hold
+     * @see rememberDatePickerState
+     * @throws [IllegalArgumentException] if the initial selected date or displayed month represent
+     * a year that is out of the year range.
+     */
+    constructor(
+        @Suppress("AutoBoxing") initialSelectedStartDateMillis: Long?,
+        @Suppress("AutoBoxing") initialSelectedEndDateMillis: Long?,
+        @Suppress("AutoBoxing") initialDisplayedMonthMillis: Long?,
+        yearRange: IntRange,
+        initialDisplayMode: DisplayMode
+    ) : this(
+        StateData(
+            initialSelectedStartDateMillis = initialSelectedStartDateMillis,
+            initialSelectedEndDateMillis = initialSelectedEndDateMillis,
+            initialDisplayedMonthMillis = initialDisplayedMonthMillis,
+            yearRange = yearRange,
+            initialDisplayMode = initialDisplayMode,
+        )
+    )
+
+    /**
+     * A timestamp that represents the selected range start date.
+     *
+     * The timestamp would hold a value for the _start_ of the day in _UTC_ milliseconds from the
+     * epoch.
+     *
+     * In case a start date was not selected or provided, the state will hold a `null` value.
+     */
+    @get:Suppress("AutoBoxing")
+    val selectedStartDateMillis by derivedStateOf {
+        stateData.selectedStartDate?.utcTimeMillis
+    }
+
+    /**
+     * A timestamp that represents the selected range end date.
+     *
+     * The timestamp would hold a value for the _start_ of the day in _UTC_ milliseconds from the
+     * epoch.
+     *
+     * In case an end date was not selected or provided, the state will hold a `null` value.
+     */
+    @get:Suppress("AutoBoxing")
+    val selectedEndDateMillis by derivedStateOf {
+        stateData.selectedEndDate?.utcTimeMillis
+    }
+
+    /**
+     * A mutable state of [DisplayMode] that represents the current display mode of the UI
+     * (i.e. picker or input).
+     */
+    var displayMode by stateData.displayMode
+
+    companion object {
+        /**
+         * The default [Saver] implementation for [DateRangePickerState].
+         */
+        fun Saver(): Saver<DateRangePickerState, *> = Saver(
+            save = { with(StateData.Saver()) { save(it.stateData) } },
+            restore = { value ->
+                DateRangePickerState(with(StateData.Saver()) { restore(value)!! })
+            }
+        )
+    }
+}
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/LegacyCalendarModelImpl.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/LegacyCalendarModelImpl.kt
index 642b9d0..e398668 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/LegacyCalendarModelImpl.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/LegacyCalendarModelImpl.kt
@@ -61,13 +61,14 @@
         add(Pair(weekdays[1], shortWeekdays[1]))
     }
 
-    override val dateInputFormat: DateInputFormat
-        get() = datePatternAsInputFormat(
+    override fun getDateInputFormat(locale: Locale): DateInputFormat {
+        return datePatternAsInputFormat(
             (DateFormat.getDateInstance(
                 DateFormat.SHORT,
-                Locale.getDefault()
+                locale
             ) as SimpleDateFormat).toPattern()
         )
+    }
 
     override fun getCanonicalDate(timeInMillis: Long): CalendarDate {
         val calendar = Calendar.getInstance(utcTimeZone)
@@ -124,19 +125,8 @@
         return getMonth(earlierMonth)
     }
 
-    override fun format(month: CalendarMonth, pattern: String): String {
-        val dateFormat = SimpleDateFormat(pattern, Locale.getDefault())
-        dateFormat.timeZone = utcTimeZone
-        dateFormat.isLenient = false
-        return dateFormat.format(month.toCalendar().timeInMillis)
-    }
-
-    override fun format(date: CalendarDate, pattern: String): String {
-        val dateFormat = SimpleDateFormat(pattern, Locale.getDefault())
-        dateFormat.timeZone = utcTimeZone
-        dateFormat.isLenient = false
-        return dateFormat.format(date.toCalendar(utcTimeZone).timeInMillis)
-    }
+    override fun formatWithPattern(utcTimeMillis: Long, pattern: String, locale: Locale): String =
+        LegacyCalendarModelImpl.formatWithPattern(utcTimeMillis, pattern, locale)
 
     override fun parse(date: String, pattern: String): CalendarDate? {
         val dateFormat = SimpleDateFormat(pattern)
@@ -157,6 +147,29 @@
         }
     }
 
+    companion object {
+
+        /**
+         * Formats a UTC timestamp into a string with a given date format pattern.
+         *
+         * @param utcTimeMillis a UTC timestamp to format (milliseconds from epoch)
+         * @param pattern a date format pattern
+         * @param locale the [Locale] to use when formatting the given timestamp
+         */
+        fun formatWithPattern(utcTimeMillis: Long, pattern: String, locale: Locale): String {
+            val dateFormat = SimpleDateFormat(pattern, locale)
+            dateFormat.timeZone = utcTimeZone
+            val calendar = Calendar.getInstance(utcTimeZone)
+            calendar.timeInMillis = utcTimeMillis
+            return dateFormat.format(calendar.timeInMillis)
+        }
+
+        /**
+         * Holds a UTC [TimeZone].
+         */
+        internal val utcTimeZone: TimeZone = TimeZone.getTimeZone("UTC")
+    }
+
     /**
      * Returns a given [Calendar] day number as a day representation under ISO-8601, where the first
      * day is defined as Monday.
@@ -196,6 +209,4 @@
         calendar[Calendar.DAY_OF_MONTH] = this.dayOfMonth
         return calendar
     }
-
-    private var utcTimeZone = TimeZone.getTimeZone("UTC")
 }
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt
index e05c5dc..6179ee1 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt
@@ -34,6 +34,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 
@@ -67,7 +68,6 @@
  * @param shadowElevation the shadow elevation of this list item
  */
 @Composable
-@ExperimentalMaterial3Api
 fun ListItem(
     headlineText: @Composable () -> Unit,
     modifier: Modifier = Modifier,
@@ -275,7 +275,6 @@
  * @param content the content to be displayed in the middle section of this list item
  */
 @Composable
-@ExperimentalMaterial3Api
 private fun ListItem(
     modifier: Modifier = Modifier,
     shape: Shape = ListItemDefaults.shape,
@@ -297,8 +296,9 @@
     ) {
         Row(
             modifier = Modifier
-            .heightIn(min = minHeight)
-            .padding(paddingValues),
+                .heightIn(min = minHeight)
+                .padding(paddingValues)
+                .semantics(mergeDescendants = true) {},
             content = content
         )
     }
@@ -364,7 +364,6 @@
 /**
  * Contains the default values used by list items.
  */
-@ExperimentalMaterial3Api
 object ListItemDefaults {
     /** The default elevation of a list item */
     val Elevation: Dp = ListTokens.ListItemContainerElevation
@@ -428,7 +427,6 @@
  *
  * - See [ListItemDefaults.colors] for the default colors used in a [ListItem].
  */
-@ExperimentalMaterial3Api
 @Immutable
 class ListItemColors internal constructor(
     private val containerColor: Color,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
new file mode 100644
index 0000000..eadc6ff
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
@@ -0,0 +1,316 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.animation.core.TweenSpec
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material3.SheetValue.Expanded
+import androidx.compose.material3.SheetValue.Collapsed
+import androidx.compose.material3.SheetValue.Hidden
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.isSpecified
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.semantics.collapse
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.dismiss
+import androidx.compose.ui.semantics.expand
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Popup
+import kotlin.math.max
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * <a href="https://m3.material.io/components/bottom-sheets/overview" class="external" target="_blank">Material Design modal bottom sheet</a>.
+ *
+ * Modal bottom sheets are used as an alternative to inline menus or simple dialogs on mobile,
+ * especially when offering a long list of action items, or when items require longer descriptions
+ * and icons. Like dialogs, modal bottom sheets appear in front of app content, disabling all other
+ * app functionality when they appear, and remaining on screen until confirmed, dismissed, or a
+ * required action has been taken.
+ *
+ * ![Bottom sheet image](https://developer.android.com/images/reference/androidx/compose/material3/bottom_sheet.png)
+ *
+ * A simple example of a modal bottom sheet looks like this:
+ *
+ * @sample androidx.compose.material3.samples.ModalBottomSheetSample
+ *
+ * @param onDismissRequest Executes when the user clicks outside of the bottom sheet, after sheet
+ * animates to [Hidden].
+ * @param modifier Optional [Modifier] for the bottom sheet.
+ * @param sheetState The state of the bottom sheet.
+ * @param shape The shape of the bottom sheet. By default, the shape changes from
+ * [BottomSheetDefaults.MinimizedShape] to [BottomSheetDefaults.ExpandedShape] when the
+ * sheet targets [Hidden] and non-Hidden states respectively.
+ * @param containerColor The color used for the background of this bottom sheet
+ * @param contentColor The preferred color for content inside this bottom sheet. Defaults to either
+ * the matching content color for [containerColor], or to the current [LocalContentColor] if
+ * [containerColor] is not a color from the theme.
+ * @param tonalElevation The tonal elevation of this bottom sheet.
+ * @param scrimColor Color of the scrim that obscures content when the bottom sheet is open.
+ * @param dragHandle Optional visual marker to swipe the bottom sheet.
+ * @param content The content to be displayed inside the bottom sheet.
+ */
+@Composable
+@ExperimentalMaterial3Api
+fun ModalBottomSheet(
+    onDismissRequest: () -> Unit,
+    modifier: Modifier = Modifier,
+    sheetState: SheetState = rememberSheetState(),
+    shape: Shape = if (sheetState.targetValue == Hidden) BottomSheetDefaults.MinimizedShape
+        else BottomSheetDefaults.ExpandedShape,
+    containerColor: Color = BottomSheetDefaults.ContainerColor,
+    contentColor: Color = contentColorFor(containerColor),
+    tonalElevation: Dp = BottomSheetDefaults.Elevation,
+    scrimColor: Color = BottomSheetDefaults.ScrimColor,
+    dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
+    content: @Composable ColumnScope.() -> Unit,
+) {
+    val scope = rememberCoroutineScope()
+
+    // Callback that is invoked when the anchors have changed.
+    val anchorChangeHandler = remember(sheetState, scope) {
+        ModalBottomSheetAnchorChangeHandler(
+            state = sheetState,
+            animateTo = { target, velocity ->
+                scope.launch { sheetState.swipeableState.animateTo(target, velocity = velocity) }
+            },
+            snapTo = { target -> scope.launch { sheetState.swipeableState.snapTo(target) } }
+        )
+    }
+    val systemBarHeight = WindowInsets.systemBarsForVisualComponents.getBottom(LocalDensity.current)
+
+    Popup {
+        BoxWithConstraints(Modifier.fillMaxSize()) {
+            val fullHeight = constraints.maxHeight
+            Scrim(
+                color = scrimColor,
+                onDismissRequest = {
+                    scope.launch { sheetState.hide() }.invokeOnCompletion {
+                        if (!sheetState.isVisible) { onDismissRequest() }
+                    }
+                },
+                visible = sheetState.targetValue != Hidden
+            )
+            Surface(
+                modifier = modifier
+                    .widthIn(max = BottomSheetMaxWidth)
+                    .fillMaxWidth()
+                    .align(Alignment.TopCenter)
+                    .offset {
+                        IntOffset(
+                            0,
+                            sheetState
+                                .requireOffset()
+                                .toInt()
+                        )
+                    }
+                    .nestedScroll(
+                        remember(sheetState.swipeableState, Orientation.Vertical) {
+                            ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
+                                state = sheetState.swipeableState,
+                                orientation = Orientation.Vertical
+                            )
+                        }
+                    )
+                    .modalBottomSheetSwipeable(
+                        sheetState = sheetState,
+                        scope = scope,
+                        onDismissRequest = onDismissRequest,
+                        anchorChangeHandler = anchorChangeHandler,
+                        screenHeight = fullHeight.toFloat(),
+                        bottomPadding = systemBarHeight.toFloat(),
+                    ),
+                shape = shape,
+                color = containerColor,
+                contentColor = contentColor,
+                tonalElevation = tonalElevation,
+            ) {
+                Column(Modifier.fillMaxWidth()) {
+                    if (dragHandle != null) {
+                        Box(Modifier.align(Alignment.CenterHorizontally)) {
+                            dragHandle()
+                        }
+                    }
+                    content()
+                }
+            }
+        }
+    }
+    if (sheetState.hasExpandedState) {
+        LaunchedEffect(sheetState) {
+            sheetState.show()
+        }
+    }
+}
+
+@Composable
+private fun Scrim(
+    color: Color,
+    onDismissRequest: () -> Unit,
+    visible: Boolean
+) {
+    val sheetDescription = getString(Strings.CloseSheet)
+    if (color.isSpecified) {
+        val alpha by animateFloatAsState(
+            targetValue = if (visible) 1f else 0f,
+            animationSpec = TweenSpec()
+        )
+        val dismissSheet = if (visible) {
+            Modifier
+                .pointerInput(onDismissRequest) {
+                    detectTapGestures {
+                        onDismissRequest()
+                    }
+                }
+                .semantics(mergeDescendants = true) {
+                    contentDescription = sheetDescription
+                    onClick { onDismissRequest(); true }
+                }
+        } else {
+            Modifier
+        }
+        Canvas(
+            Modifier
+                .fillMaxSize()
+                .then(dismissSheet)
+        ) {
+            drawRect(color = color, alpha = alpha)
+        }
+    }
+}
+
+@ExperimentalMaterial3Api
+private fun Modifier.modalBottomSheetSwipeable(
+    sheetState: SheetState,
+    scope: CoroutineScope,
+    onDismissRequest: () -> Unit,
+    anchorChangeHandler: AnchorChangeHandler<SheetValue>,
+    screenHeight: Float,
+    bottomPadding: Float,
+) = draggable(
+    state = sheetState.swipeableState.draggableState,
+    orientation = Orientation.Vertical,
+    enabled = sheetState.isVisible,
+    startDragImmediately = sheetState.swipeableState.isAnimationRunning,
+    onDragStopped = { velocity ->
+        try {
+            sheetState.settle(velocity)
+        } finally {
+            if (!sheetState.isVisible) onDismissRequest()
+        }
+    }
+).swipeAnchors(
+    state = sheetState.swipeableState,
+    anchorChangeHandler = anchorChangeHandler,
+    possibleValues = setOf(Hidden, Collapsed, Expanded),
+) { value, sheetSize ->
+    when (value) {
+        Hidden -> screenHeight + bottomPadding
+        Collapsed -> when {
+            sheetSize.height < screenHeight / 2 -> null
+            sheetState.skipCollapsed -> null
+            else -> sheetSize.height / 2f
+        }
+        Expanded -> if (sheetSize.height != 0) {
+            max(0f, screenHeight - sheetSize.height)
+        } else null
+    }
+}.semantics {
+    if (sheetState.isVisible) {
+        dismiss {
+            if (sheetState.swipeableState.confirmValueChange(Hidden)) {
+                scope.launch { sheetState.hide() }.invokeOnCompletion {
+                    if (!sheetState.isVisible) { onDismissRequest() }
+                }
+            }
+            true
+        }
+        if (sheetState.swipeableState.currentValue == Collapsed) {
+            expand {
+                if (sheetState.swipeableState.confirmValueChange(Expanded)) {
+                    scope.launch { sheetState.expand() }
+                }
+                true
+            }
+        } else if (sheetState.hasCollapsedState) {
+            collapse {
+                if (sheetState.swipeableState.confirmValueChange(Collapsed)) {
+                    scope.launch { sheetState.collapse() }
+                }
+                true
+            }
+        }
+    }
+}
+
+@ExperimentalMaterial3Api
+private fun ModalBottomSheetAnchorChangeHandler(
+    state: SheetState,
+    animateTo: (target: SheetValue, velocity: Float) -> Unit,
+    snapTo: (target: SheetValue) -> Unit,
+) = AnchorChangeHandler<SheetValue> { previousTarget, previousAnchors, newAnchors ->
+    val previousTargetOffset = previousAnchors[previousTarget]
+    val newTarget = when (previousTarget) {
+        Hidden -> Hidden
+        Collapsed, Expanded -> {
+            val hasCollapsedState = newAnchors.containsKey(Collapsed)
+            val newTarget = if (hasCollapsedState) Collapsed
+            else if (newAnchors.containsKey(Expanded)) Expanded else Hidden
+            newTarget
+        }
+    }
+    val newTargetOffset = newAnchors.getValue(newTarget)
+    if (newTargetOffset != previousTargetOffset) {
+        if (state.swipeableState.isAnimationRunning || previousAnchors.isEmpty()) {
+            // Re-target the animation to the new offset if it changed
+            animateTo(newTarget, state.swipeableState.lastVelocity)
+        } else {
+            // Snap to the new offset value of the target if no animation was running
+            snapTo(newTarget)
+        }
+    }
+}
+
+private val BottomSheetMaxWidth = 640.dp
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
index f2c6b55..2ed1ace 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
@@ -515,7 +515,7 @@
         LinearProgressIndicatorTokens.TrackColor.toColor()
 
     /** Default track color for a circular progress indicator. */
-    val circularTrackColor: Color get() = Color.Transparent
+    val circularTrackColor: Color @Composable get() = Color.Transparent
 
     /** Default stroke width for a circular progress indicator. */
     val CircularStrokeWidth: Dp = CircularProgressIndicatorTokens.ActiveIndicatorWidth
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Shapes.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Shapes.kt
index 1655707..5ad72df 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Shapes.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Shapes.kt
@@ -144,6 +144,11 @@
     return copy(bottomStart = CornerSize(0.0.dp), bottomEnd = CornerSize(0.0.dp))
 }
 
+/** Helper function for component shape tokens. Used to grab the top values of a shape parameter. */
+internal fun CornerBasedShape.bottom(): CornerBasedShape {
+    return copy(topStart = CornerSize(0.0.dp), topEnd = CornerSize(0.0.dp))
+}
+
 /** Helper function for component shape tokens. Used to grab the end values of a shape parameter. */
 internal fun CornerBasedShape.end(): CornerBasedShape {
     return copy(topStart = CornerSize(0.0.dp), bottomStart = CornerSize(0.0.dp))
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt
new file mode 100644
index 0000000..8149339
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt
@@ -0,0 +1,326 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.SheetValue.Collapsed
+import androidx.compose.material3.SheetValue.Hidden
+import androidx.compose.material3.SheetValue.Expanded
+import androidx.compose.material3.tokens.ScrimTokens
+import androidx.compose.material3.tokens.SheetBottomTokens
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.CancellationException
+
+/**
+ * Create and [remember] a [SheetState].
+ *
+ * @param skipHalfExpanded Whether the half expanded state, if the sheet is tall enough, should
+ * be skipped. If true, the sheet will always expand to the [Expanded] state and move to the
+ * [Hidden] state when hiding the sheet, either programmatically or by user interaction.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
+ */
+@Composable
+@ExperimentalMaterial3Api
+fun rememberSheetState(
+    skipHalfExpanded: Boolean = false,
+    confirmValueChange: (SheetValue) -> Boolean = { true }
+): SheetState {
+    return rememberSaveable(
+        skipHalfExpanded, confirmValueChange,
+        saver = SheetState.Saver(
+            skipHalfExpanded = skipHalfExpanded,
+            confirmValueChange = confirmValueChange
+        )
+    ) {
+        SheetState(skipHalfExpanded, confirmValueChange = confirmValueChange)
+    }
+}
+
+/**
+ * State of a sheet composable, such as [ModalBottomSheet]
+ *
+ * Contains states relating to it's swipe position as well as animations between state values.
+ *
+ * @param skipCollapsed Whether the collapsed state, if the bottom sheet is tall enough, should
+ * be skipped. If true, the sheet will always expand to the [Expanded] state and move to the
+ * [Hidden] state when hiding the sheet, either programmatically or by user interaction.
+ * @param initialValue The initial value of the state.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
+ */
+@Stable
+@ExperimentalMaterial3Api
+class SheetState(
+    internal val skipCollapsed: Boolean,
+    initialValue: SheetValue = Hidden,
+    confirmValueChange: (SheetValue) -> Boolean = { true }
+) {
+    /**
+     * The current value of the state.
+     *
+     * If no swipe or animation is in progress, this corresponds to the state the bottom sheet is
+     * currently in. If a swipe or an animation is in progress, this corresponds the state the sheet
+     * was in before the swipe or animation started.
+     */
+    val currentValue: SheetValue get() = swipeableState.currentValue
+
+    /**
+     * The target value of the bottom sheet state.
+     *
+     * If a swipe is in progress, this is the value that the sheet would animate to if the
+     * swipe finishes. If an animation is running, this is the target value of that animation.
+     * Finally, if no swipe or animation is in progress, this is the same as the [currentValue].
+     */
+    val targetValue: SheetValue get() = swipeableState.targetValue
+
+    /**
+     * Whether the bottom sheet is visible.
+     */
+    val isVisible: Boolean
+        get() = swipeableState.currentValue != Hidden
+
+    /**
+     * Require the current offset (in pixels) of the bottom sheet.
+     *
+     * @throws IllegalStateException If the offset has not been initialized yet
+     */
+    fun requireOffset(): Float = swipeableState.requireOffset()
+
+    /**
+     * Show the bottom sheet with animation and suspend until it's shown. If the sheet is taller
+     * than 50% of the parent's height, the bottom sheet will be half expanded. Otherwise it will be
+     * fully expanded.
+     *
+     * @throws [CancellationException] if the animation is interrupted
+     */
+    suspend fun show() {
+        val targetValue = when {
+            hasCollapsedState -> Collapsed
+            else -> Expanded
+        }
+        swipeableState.animateTo(targetValue)
+    }
+
+    /**
+     * Hide the bottom sheet with animation and suspend until it is fully hidden or animation has
+     * been cancelled.
+     * @throws [CancellationException] if the animation is interrupted
+     */
+    suspend fun hide() {
+        swipeableState.animateTo(Hidden)
+    }
+
+    /**
+     * Hide the bottom sheet with animation and suspend until it is collapsed or animation has
+     * been cancelled.
+     * @throws [CancellationException] if the animation is interrupted
+     */
+    suspend fun collapse() {
+        swipeableState.animateTo(Collapsed)
+    }
+
+    companion object {
+        /**
+         * The default [Saver] implementation for [SheetState].
+         */
+        fun Saver(
+            skipHalfExpanded: Boolean,
+            confirmValueChange: (SheetValue) -> Boolean
+        ) = Saver<SheetState, SheetValue>(
+            save = { it.currentValue },
+            restore = { SheetState(skipHalfExpanded, it, confirmValueChange) }
+        )
+    }
+
+    internal var swipeableState = SwipeableV2State(
+        initialValue = initialValue,
+        animationSpec = SwipeableV2Defaults.AnimationSpec,
+        confirmValueChange = confirmValueChange,
+    )
+
+    internal val hasCollapsedState: Boolean
+        get() = swipeableState.hasAnchorForValue(Collapsed)
+
+    internal val hasExpandedState: Boolean
+        get() = swipeableState.hasAnchorForValue(Expanded)
+
+    internal val offset: Float? get() = swipeableState.offset
+
+    /**
+     * Fully expand the bottom sheet with animation and suspend until it if fully expanded or
+     * animation has been cancelled.
+     * *
+     * @throws [CancellationException] if the animation is interrupted
+     */
+    internal suspend fun expand() {
+        swipeableState.animateTo(Expanded)
+    }
+
+    /**
+     * Find the closest anchor taking into account the velocity and settle at it with an animation.
+     */
+    internal suspend fun settle(velocity: Float) {
+        swipeableState.settle(velocity)
+    }
+}
+
+/**
+ * Possible values of [SheetState].
+ */
+@ExperimentalMaterial3Api
+enum class SheetValue {
+    /**
+     * The sheet is not visible.
+     */
+    Hidden,
+
+    /**
+     * The sheet is visible at full height.
+     */
+    Expanded,
+
+    /**
+     * The sheet is partially visible.
+     */
+    Collapsed,
+}
+
+/**
+ * Contains the default values used by [ModalBottomSheet].
+ */
+@Stable
+@ExperimentalMaterial3Api
+object BottomSheetDefaults {
+    /** The default shape for a [ModalBottomSheet] in a [Hidden] state. */
+    val MinimizedShape: Shape
+        @Composable get() =
+        SheetBottomTokens.DockedMinimizedContainerShape.toShape()
+
+    /** The default shape for a [ModalBottomSheet] in [Collapsed] and [Expanded] states. */
+    val ExpandedShape: Shape
+        @Composable get() =
+        SheetBottomTokens.DockedContainerShape.toShape()
+
+    /** The default container color for a bottom sheet. */
+    val ContainerColor: Color
+        @Composable get() =
+        SheetBottomTokens.DockedContainerColor.toColor()
+
+    /** The default elevation for a bottom sheet. */
+    val Elevation = SheetBottomTokens.DockedModalContainerElevation
+
+    /** The default color of the scrim overlay for background content. */
+    val ScrimColor: Color
+        @Composable get() =
+        ScrimTokens.ContainerColor.toColor().copy(ScrimTokens.ContainerOpacity)
+
+    @Composable
+    fun DragHandle(
+        modifier: Modifier = Modifier,
+        width: Dp = SheetBottomTokens.DockedDragHandleWidth,
+        height: Dp = SheetBottomTokens.DockedDragHandleHeight,
+        shape: Shape = MaterialTheme.shapes.extraLarge,
+        color: Color = SheetBottomTokens.DockedDragHandleColor.toColor()
+            .copy(SheetBottomTokens.DockedDragHandleOpacity),
+    ) {
+        Surface(
+            modifier = modifier.padding(vertical = DragHandleVerticalPadding),
+            color = color,
+            shape = shape
+        ) {
+            Box(
+                Modifier
+                    .size(
+                        width = width,
+                        height = height
+                    )
+            )
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+internal fun ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
+    state: SwipeableV2State<*>,
+    orientation: Orientation
+): NestedScrollConnection = object : NestedScrollConnection {
+    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+        val delta = available.toFloat()
+        return if (delta < 0 && source == NestedScrollSource.Drag) {
+            state.dispatchRawDelta(delta).toOffset()
+        } else {
+            Offset.Zero
+        }
+    }
+
+    override fun onPostScroll(
+        consumed: Offset,
+        available: Offset,
+        source: NestedScrollSource
+    ): Offset {
+        return if (source == NestedScrollSource.Drag) {
+            state.dispatchRawDelta(available.toFloat()).toOffset()
+        } else {
+            Offset.Zero
+        }
+    }
+
+    override suspend fun onPreFling(available: Velocity): Velocity {
+        val toFling = available.toFloat()
+        val currentOffset = state.requireOffset()
+        return if (toFling < 0 && currentOffset > state.minBound) {
+            state.settle(velocity = toFling)
+            // since we go to the anchor with tween settling, consume all for the best UX
+            available
+        } else {
+            Velocity.Zero
+        }
+    }
+
+    override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+        state.settle(velocity = available.toFloat())
+        return available
+    }
+
+    private fun Float.toOffset(): Offset = Offset(
+        x = if (orientation == Orientation.Horizontal) this else 0f,
+        y = if (orientation == Orientation.Vertical) this else 0f
+    )
+
+    @JvmName("velocityToFloat")
+    private fun Velocity.toFloat() = if (orientation == Orientation.Horizontal) x else y
+
+    @JvmName("offsetToFloat")
+    private fun Offset.toFloat(): Float = if (orientation == Orientation.Horizontal) x else y
+}
+
+private val DragHandleVerticalPadding = 22.dp
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
index e9b3e91..7235f1e 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
@@ -143,7 +143,11 @@
  * [Interaction]s and customize the appearance / behavior of this slider in different states.
  */
 // TODO(b/229979132): Add m.io link
-@OptIn(ExperimentalMaterial3Api::class)
+@Deprecated(
+    message = "Maintained for binary compatibility. " +
+        "Please use the non-experimental API that allows for custom thumb and tracks.",
+    level = DeprecationLevel.HIDDEN
+)
 @Composable
 fun Slider(
     value: Float,
@@ -157,16 +161,17 @@
     colors: SliderColors = SliderDefaults.colors(),
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
 ) {
-    Slider(
-        value = value,
-        onValueChange = onValueChange,
+    require(steps >= 0) { "steps should be >= 0" }
+
+    SliderImpl(
         modifier = modifier,
         enabled = enabled,
-        valueRange = valueRange,
-        steps = steps,
-        onValueChangeFinished = onValueChangeFinished,
-        colors = colors,
         interactionSource = interactionSource,
+        onValueChange = onValueChange,
+        onValueChangeFinished = onValueChangeFinished,
+        steps = steps,
+        value = value,
+        valueRange = valueRange,
         thumb = {
             SliderDefaults.Thumb(
                 interactionSource = interactionSource,
@@ -224,6 +229,11 @@
  * receives a [SliderPositions] which is used to obtain the current active track and the tick positions
  * if the slider is discrete.
  */
+@Deprecated(
+    message = "Maintained for binary compatibility. " +
+        "Please use the non-experimental API that allows for custom thumb and tracks.",
+    level = DeprecationLevel.HIDDEN
+)
 @Composable
 @ExperimentalMaterial3Api
 fun Slider(
@@ -239,16 +249,17 @@
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     thumb: @Composable (SliderPositions) -> Unit
 ) {
-    Slider(
-        value = value,
-        onValueChange = onValueChange,
+    require(steps >= 0) { "steps should be >= 0" }
+
+    SliderImpl(
         modifier = modifier,
         enabled = enabled,
-        valueRange = valueRange,
-        steps = steps,
-        onValueChangeFinished = onValueChangeFinished,
-        colors = colors,
         interactionSource = interactionSource,
+        onValueChange = onValueChange,
+        onValueChangeFinished = onValueChangeFinished,
+        steps = steps,
+        value = value,
+        valueRange = valueRange,
         thumb = thumb,
         track = { sliderPositions ->
             SliderDefaults.Track(
@@ -301,6 +312,11 @@
  * receives a [SliderPositions] which is used to obtain the current active track and the tick positions
  * if the slider is discrete.
  */
+@Deprecated(
+    message = "Maintained for binary compatibility. " +
+        "Please use the non-experimental API that allows for custom thumb and tracks.",
+    level = DeprecationLevel.HIDDEN
+)
 @Composable
 @ExperimentalMaterial3Api
 fun Slider(
@@ -340,7 +356,195 @@
 }
 
 /**
- * Material Design Range slider
+ * <a href="https://m3.material.io/components/sliders/overview" class="external" target="_blank">Material Design slider</a>.
+ *
+ * Sliders allow users to make selections from a range of values.
+ *
+ * Sliders reflect a range of values along a bar, from which users may select a single value.
+ * They are ideal for adjusting settings such as volume, brightness, or applying image filters.
+ *
+ * ![Sliders image](https://developer.android.com/images/reference/androidx/compose/material3/sliders.png)
+ *
+ * Use continuous sliders to allow users to make meaningful selections that don’t
+ * require a specific value:
+ *
+ * @sample androidx.compose.material3.samples.SliderSample
+ *
+ * You can allow the user to choose only between predefined set of values by specifying the amount
+ * of steps between min and max values:
+ *
+ * @sample androidx.compose.material3.samples.StepsSliderSample
+ *
+ * Slider using a custom thumb:
+ *
+ * @sample androidx.compose.material3.samples.SliderWithCustomThumbSample
+ *
+ * Slider using custom track and thumb:
+ *
+ * @sample androidx.compose.material3.samples.SliderWithCustomTrackAndThumb
+ *
+ * @param value current value of the slider. If outside of [valueRange] provided, value will be
+ * coerced to this range.
+ * @param onValueChange callback in which value should be updated
+ * @param modifier the [Modifier] to be applied to this slider
+ * @param enabled controls the enabled state of this slider. When `false`, this component will not
+ * respond to user input, and it will appear visually disabled and disabled to accessibility
+ * services.
+ * @param valueRange range of values that this slider can take. The passed [value] will be coerced
+ * to this range.
+ * @param onValueChangeFinished called when value change has ended. This should not be used to
+ * update the slider value (use [onValueChange] instead), but rather to know when the user has
+ * completed selecting a new value by ending a drag or a click.
+ * @param colors [SliderColors] that will be used to resolve the colors used for this slider in
+ * different states. See [SliderDefaults.colors].
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this slider. You can create and pass in your own `remember`ed instance to observe
+ * [Interaction]s and customize the appearance / behavior of this slider in different states.
+ * @param thumb the thumb to be displayed on the slider, it is placed on top of the track. The lambda
+ * receives a [SliderPositions] which is used to obtain the current active track and the tick positions
+ * if the slider is discrete.
+ * @param track the track to be displayed on the slider, it is placed underneath the thumb. The lambda
+ * receives a [SliderPositions] which is used to obtain the current active track and the tick positions
+ * if the slider is discrete.
+ * @param steps if greater than 0, specifies the amount of discrete allowable values, evenly
+ * distributed across the whole value range. If 0, the slider will behave continuously and allow any
+ * value from the range specified. Must not be negative.
+ */
+@Composable
+fun Slider(
+    value: Float,
+    onValueChange: (Float) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
+    onValueChangeFinished: (() -> Unit)? = null,
+    colors: SliderColors = SliderDefaults.colors(),
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    thumb: @Composable (SliderPositions) -> Unit = {
+        SliderDefaults.Thumb(
+            interactionSource = interactionSource,
+            colors = colors,
+            enabled = enabled
+        )
+    },
+    track: @Composable (SliderPositions) -> Unit = { sliderPositions ->
+        SliderDefaults.Track(
+            colors = colors,
+            enabled = enabled,
+            sliderPositions = sliderPositions
+        )
+    },
+    /*@IntRange(from = 0)*/
+    steps: Int = 0,
+) {
+    require(steps >= 0) { "steps should be >= 0" }
+
+    SliderImpl(
+        value = value,
+        onValueChange = onValueChange,
+        modifier = modifier,
+        enabled = enabled,
+        valueRange = valueRange,
+        steps = steps,
+        onValueChangeFinished = onValueChangeFinished,
+        interactionSource = interactionSource,
+        thumb = thumb,
+        track = track
+    )
+}
+
+/**
+ * <a href="https://m3.material.io/components/sliders/overview" class="external" target="_blank">Material Design Range slider</a>.
+ *
+ * Range Sliders expand upon [Slider] using the same concepts but allow the user to select 2 values.
+ *
+ * The two values are still bounded by the value range but they also cannot cross each other.
+ *
+ * Use continuous Range Sliders to allow users to make meaningful selections that don’t
+ * require a specific values:
+ *
+ * @sample androidx.compose.material3.samples.RangeSliderSample
+ *
+ * You can allow the user to choose only between predefined set of values by specifying the amount
+ * of steps between min and max values:
+ *
+ * @sample androidx.compose.material3.samples.StepRangeSliderSample
+ *
+ * @param value current values of the RangeSlider. If either value is outside of [valueRange]
+ * provided, it will be coerced to this range.
+ * @param onValueChange lambda in which values should be updated
+ * @param modifier modifiers for the Range Slider layout
+ * @param enabled whether or not component is enabled and can we interacted with or not
+ * @param valueRange range of values that Range Slider values can take. Passed [value] will be
+ * coerced to this range
+ * @param steps if greater than 0, specifies the amounts of discrete values, evenly distributed
+ * between across the whole value range. If 0, range slider will behave as a continuous slider and
+ * allow to choose any value from the range specified. Must not be negative.
+ * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
+ * shouldn't be used to update the range slider values (use [onValueChange] for that), but rather to
+ * know when the user has completed selecting a new value by ending a drag or a click.
+ * @param colors [SliderColors] that will be used to determine the color of the Range Slider
+ * parts in different state. See [SliderDefaults.colors] to customize.
+ */
+@Deprecated(
+    message = "Maintained for binary compatibility. " +
+        "Please use the non-experimental API that allows for custom thumbs and tracks.",
+    level = DeprecationLevel.HIDDEN
+)
+@Composable
+@ExperimentalMaterial3Api
+fun RangeSlider(
+    value: ClosedFloatingPointRange<Float>,
+    onValueChange: (ClosedFloatingPointRange<Float>) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
+    /*@IntRange(from = 0)*/
+    steps: Int = 0,
+    onValueChangeFinished: (() -> Unit)? = null,
+    colors: SliderColors = SliderDefaults.colors()
+) {
+    val startInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+    val endInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+
+    require(steps >= 0) { "steps should be >= 0" }
+
+    RangeSliderImpl(
+        modifier = modifier,
+        value = value,
+        onValueChange = onValueChange,
+        enabled = enabled,
+        valueRange = valueRange,
+        steps = steps,
+        onValueChangeFinished = onValueChangeFinished,
+        startInteractionSource = startInteractionSource,
+        endInteractionSource = endInteractionSource,
+        startThumb = {
+            SliderDefaults.Thumb(
+                interactionSource = startInteractionSource,
+                colors = colors,
+                enabled = enabled
+            )
+        },
+        endThumb = {
+            SliderDefaults.Thumb(
+                interactionSource = endInteractionSource,
+                colors = colors,
+                enabled = enabled
+            )
+        },
+        track = { sliderPositions ->
+            SliderDefaults.Track(
+                colors = colors,
+                enabled = enabled,
+                sliderPositions = sliderPositions
+            )
+        }
+    )
+}
+
+/**
+ * <a href="https://m3.material.io/components/sliders/overview" class="external" target="_blank">Material Design Range slider</a>.
  *
  * Range Sliders expand upon [Slider] using the same concepts but allow the user to select 2 values.
  *
@@ -397,7 +601,6 @@
  * tick positions if the range slider is discrete.
  */
 @Composable
-@ExperimentalMaterial3Api
 fun RangeSlider(
     value: ClosedFloatingPointRange<Float>,
     onValueChange: (ClosedFloatingPointRange<Float>) -> Unit,
@@ -450,7 +653,6 @@
     )
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun SliderImpl(
     modifier: Modifier,
@@ -600,7 +802,6 @@
     }
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun RangeSliderImpl(
     modifier: Modifier,
@@ -929,7 +1130,6 @@
      * accessibility services.
      */
     @Composable
-    @ExperimentalMaterial3Api
     fun Thumb(
         interactionSource: MutableInteractionSource,
         modifier: Modifier = Modifier,
@@ -987,7 +1187,6 @@
      * accessibility services.
      */
     @Composable
-    @ExperimentalMaterial3Api
     fun Track(
         sliderPositions: SliderPositions,
         modifier: Modifier = Modifier,
@@ -1454,7 +1653,6 @@
  * and fractional positions where the discrete ticks should be drawn on the track.
  */
 @Stable
-@ExperimentalMaterial3Api
 class SliderPositions(
     initialActiveRange: ClosedFloatingPointRange<Float> = 0f..1f,
     initialTickFractions: FloatArray = floatArrayOf()
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..ce64cb0 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
@@ -18,10 +18,13 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.ReadOnlyComposable
 
 @Immutable
-@kotlin.jvm.JvmInline
-internal value class Strings private constructor(@Suppress("unused") private val value: Int) {
+@JvmInline
+internal value class Strings private constructor(
+    @Suppress("unused") private val value: Int
+) {
     companion object {
         val NavigationMenu = Strings(0)
         val CloseDrawer = Strings(1)
@@ -38,12 +41,41 @@
         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)
+        val DatePickerTodayDescription = Strings(23)
+        val DateInputTitle = Strings(24)
+        val DateInputHeadline = Strings(25)
+        val DateInputLabel = Strings(26)
+        val DateInputHeadlineDescription = Strings(27)
+        val DateInputNoInputDescription = Strings(28)
+        val DateInputInvalidNotAllowed = Strings(29)
+        val DateInputInvalidForPattern = Strings(30)
+        val DateInputInvalidYearRange = Strings(31)
+        val DatePickerSwitchToCalendarMode = Strings(32)
+        val DatePickerSwitchToInputMode = Strings(33)
+        val TooltipLongPressLabel = Strings(34)
+        val TimePickerAM = Strings(35)
+        val TimePickerPM = Strings(36)
+        val TimePickerPeriodToggle = Strings(37)
+        val TimePickerHourSelection = Strings(38)
+        val TimePickerMinuteSelection = Strings(39)
+        val TimePickerHourSuffix = Strings(40)
+        val TimePicker24HourSuffix = Strings(41)
+        val TimePickerMinuteSuffix = Strings(42)
     }
 }
 
 @Composable
+@ReadOnlyComposable
 internal expect fun getString(string: Strings): String
+
+@Composable
+@ReadOnlyComposable
+internal expect fun getString(string: Strings, vararg formatArgs: Any): String
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismiss.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismiss.kt
index c16b1f0..271d0cf 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismiss.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismiss.kt
@@ -237,6 +237,7 @@
     directions: Set<DismissDirection> = setOf(EndToStart, StartToEnd),
 ) {
     val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+
     Box(
         modifier
             .swipeableV2(
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
index 6a64e73..d57af0c 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
@@ -119,9 +119,10 @@
             }
         }
         if (previousAnchors != newAnchors) {
-            state.updateAnchors(newAnchors)
-            if (previousAnchors.isNotEmpty()) {
-                anchorChangeHandler?.onAnchorsChanged(previousAnchors, newAnchors)
+            val previousTarget = state.targetValue
+            val stateRequiresCleanup = state.updateAnchors(newAnchors)
+            if (stateRequiresCleanup) {
+                anchorChangeHandler?.onAnchorsChanged(previousTarget, previousAnchors, newAnchors)
             }
         }
     },
@@ -241,8 +242,8 @@
     var lastVelocity: Float by mutableStateOf(0f)
         private set
 
-    private val minBound by derivedStateOf { anchors.minOrNull() ?: Float.NEGATIVE_INFINITY }
-    private val maxBound by derivedStateOf { anchors.maxOrNull() ?: Float.POSITIVE_INFINITY }
+    val minBound by derivedStateOf { anchors.minOrNull() ?: Float.NEGATIVE_INFINITY }
+    val maxBound by derivedStateOf { anchors.maxOrNull() ?: Float.POSITIVE_INFINITY }
 
     private var animationTarget: T? by mutableStateOf(null)
     internal val draggableState = DraggableState {
@@ -253,12 +254,25 @@
 
     internal var density: Density? = null
 
-    internal fun updateAnchors(newAnchors: Map<T, Float>) {
+    /**
+     * Update the anchors.
+     * If the previous set of anchors was empty, attempt to update the offset to match the initial
+     * value's anchor.
+     *
+     * @return true if the state needs to be adjusted after updating the anchors, e.g. if the
+     * initial value is not found in the initial set of anchors. false if no further updates are
+     * needed.
+     */
+    internal fun updateAnchors(newAnchors: Map<T, Float>): Boolean {
         val previousAnchorsEmpty = anchors.isEmpty()
         anchors = newAnchors
-        if (previousAnchorsEmpty) {
-            offset = anchors.requireAnchor(this.currentValue)
-        }
+        val initialValueHasAnchor = if (previousAnchorsEmpty) {
+            val initialValueAnchor = anchors[currentValue]
+            val initialValueHasAnchor = initialValueAnchor != null
+            if (initialValueHasAnchor) offset = initialValueAnchor
+            initialValueHasAnchor
+        } else true
+        return !initialValueHasAnchor || !previousAnchorsEmpty
     }
 
     /**
@@ -289,6 +303,8 @@
 
     /**
      * Animate to a [targetValue].
+     * If the [targetValue] is not in the set of anchors, the [currentValue] will be updated to the
+     * [targetValue] without updating the offset.
      *
      * @throws CancellationException if the interaction interrupted by another interaction like a
      * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
@@ -300,30 +316,34 @@
         targetValue: T,
         velocity: Float = lastVelocity,
     ) {
-        val targetOffset = anchors.requireAnchor(targetValue)
-        try {
-            draggableState.drag {
-                animationTarget = targetValue
-                var prev = offset ?: 0f
-                animate(prev, targetOffset, velocity, animationSpec) { value, velocity ->
-                    // Our onDrag coerces the value within the bounds, but an animation may
-                    // overshoot, for example a spring animation or an overshooting interpolator
-                    // We respect the user's intention and allow the overshoot, but still use
-                    // DraggableState's drag for its mutex.
-                    offset = value
-                    prev = value
-                    lastVelocity = velocity
+        val targetOffset = anchors[targetValue]
+        if (targetOffset != null) {
+            try {
+                draggableState.drag {
+                    animationTarget = targetValue
+                    var prev = offset ?: 0f
+                    animate(prev, targetOffset, velocity, animationSpec) { value, velocity ->
+                        // Our onDrag coerces the value within the bounds, but an animation may
+                        // overshoot, for example a spring animation or an overshooting interpolator
+                        // We respect the user's intention and allow the overshoot, but still use
+                        // DraggableState's drag for its mutex.
+                        offset = value
+                        prev = value
+                        lastVelocity = velocity
+                    }
+                    lastVelocity = 0f
                 }
-                lastVelocity = 0f
+            } finally {
+                animationTarget = null
+                val endOffset = requireOffset()
+                val endState = anchors
+                    .entries
+                    .firstOrNull { (_, anchorOffset) -> abs(anchorOffset - endOffset) < 0.5f }
+                    ?.key
+                this.currentValue = endState ?: currentValue
             }
-        } finally {
-            animationTarget = null
-            val endOffset = requireOffset()
-            val endState = anchors
-                .entries
-                .firstOrNull { (_, anchorOffset) -> abs(anchorOffset - endOffset) < 0.5f }
-                ?.key
-            this.currentValue = endState ?: currentValue
+        } else {
+            currentValue = targetValue
         }
     }
 
@@ -533,8 +553,7 @@
         state: SwipeableV2State<T>,
         animate: (target: T, velocity: Float) -> Unit,
         snap: (target: T) -> Unit
-    ) = AnchorChangeHandler { previousAnchors, newAnchors ->
-        val previousTarget = state.targetValue
+    ) = AnchorChangeHandler { previousTarget, previousAnchors, newAnchors ->
         val previousTargetOffset = previousAnchors[previousTarget]
         val newTargetOffset = newAnchors[previousTarget]
         if (previousTargetOffset != newTargetOffset) {
@@ -562,10 +581,15 @@
      * Callback that is invoked when the anchors have changed, after the [SwipeableV2State] has been
      * updated with them. Use this hook to re-launch animations or interrupt them if needed.
      *
+     * @param previousTargetValue The target value before the anchors were updated
      * @param previousAnchors The previously set anchors
      * @param newAnchors The newly set anchors
      */
-    fun onAnchorsChanged(previousAnchors: Map<T, Float>, newAnchors: Map<T, Float>)
+    fun onAnchorsChanged(
+        previousTargetValue: T,
+        previousAnchors: Map<T, Float>,
+        newAnchors: Map<T, Float>
+    )
 }
 
 @Stable
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimeFormat.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimeFormat.kt
new file mode 100644
index 0000000..01f635a
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimeFormat.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+
+internal expect val is24HourFormat: Boolean
+  @Composable
+  @ReadOnlyComposable get
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
new file mode 100644
index 0000000..89aa6d8
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
@@ -0,0 +1,1007 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.animation.Crossfade
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.MutatorMutex
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.CornerBasedShape
+import androidx.compose.material3.tokens.MotionTokens
+import androidx.compose.material3.tokens.TimePickerTokens
+import androidx.compose.material3.tokens.TimePickerTokens.ClockDialColor
+import androidx.compose.material3.tokens.TimePickerTokens.ClockDialContainerSize
+import androidx.compose.material3.tokens.TimePickerTokens.ClockDialLabelTextFont
+import androidx.compose.material3.tokens.TimePickerTokens.ClockDialSelectedLabelTextColor
+import androidx.compose.material3.tokens.TimePickerTokens.ClockDialSelectorCenterContainerSize
+import androidx.compose.material3.tokens.TimePickerTokens.ClockDialSelectorHandleContainerColor
+import androidx.compose.material3.tokens.TimePickerTokens.ClockDialSelectorHandleContainerSize
+import androidx.compose.material3.tokens.TimePickerTokens.ClockDialSelectorTrackContainerWidth
+import androidx.compose.material3.tokens.TimePickerTokens.ClockDialUnselectedLabelTextColor
+import androidx.compose.material3.tokens.TimePickerTokens.ContainerColor
+import androidx.compose.material3.tokens.TimePickerTokens.PeriodSelectorContainerShape
+import androidx.compose.material3.tokens.TimePickerTokens.PeriodSelectorOutlineColor
+import androidx.compose.material3.tokens.TimePickerTokens.PeriodSelectorSelectedContainerColor
+import androidx.compose.material3.tokens.TimePickerTokens.PeriodSelectorSelectedLabelTextColor
+import androidx.compose.material3.tokens.TimePickerTokens.PeriodSelectorUnselectedLabelTextColor
+import androidx.compose.material3.tokens.TimePickerTokens.PeriodSelectorVerticalContainerHeight
+import androidx.compose.material3.tokens.TimePickerTokens.PeriodSelectorVerticalContainerWidth
+import androidx.compose.material3.tokens.TimePickerTokens.TimeSelectorContainerHeight
+import androidx.compose.material3.tokens.TimePickerTokens.TimeSelectorContainerShape
+import androidx.compose.material3.tokens.TimePickerTokens.TimeSelectorContainerWidth
+import androidx.compose.material3.tokens.TimePickerTokens.TimeSelectorLabelTextFont
+import androidx.compose.material3.tokens.TimePickerTokens.TimeSelectorSelectedContainerColor
+import androidx.compose.material3.tokens.TimePickerTokens.TimeSelectorSelectedLabelTextColor
+import androidx.compose.material3.tokens.TimePickerTokens.TimeSelectorUnselectedContainerColor
+import androidx.compose.material3.tokens.TimePickerTokens.TimeSelectorUnselectedLabelTextColor
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.structuralEqualityPolicy
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.center
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.selectableGroup
+import androidx.compose.ui.semantics.selected
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.center
+import androidx.compose.ui.unit.dp
+import java.text.NumberFormat
+import kotlin.math.PI
+import kotlin.math.abs
+import kotlin.math.atan2
+import kotlin.math.cos
+import kotlin.math.hypot
+import kotlin.math.roundToInt
+import kotlin.math.sin
+import kotlinx.coroutines.launch
+
+/**
+ * Time pickers help users select and set a specific time.
+ *
+ * Shows a picker that allows the user to select time.
+ * Subscribe to updates through [TimePickerState]
+ *
+ * @sample androidx.compose.material3.samples.TimePickerSample
+ *
+ * [state] state for this timepicker, allows to subscribe to changes to [TimePickerState.hour] and
+ * [TimePickerState.minute], and set the initial time for this picker.
+ *
+ */
+@Composable
+@ExperimentalMaterial3Api
+fun TimePicker(
+    state: TimePickerState,
+    colors: TimePickerColors = TimePickerDefaults.colors()
+) {
+    Column(horizontalAlignment = Alignment.CenterHorizontally) {
+        ClockDisplay(state, colors)
+        Spacer(modifier = Modifier.height(ClockDisplayBottomMargin))
+        ClockFace(state, colors)
+        Spacer(modifier = Modifier.height(ClockFaceBottomMargin))
+    }
+}
+
+/**
+ * Contains the default values used by [TimePicker]
+ */
+@ExperimentalMaterial3Api
+@Stable
+object TimePickerDefaults {
+
+    /**
+     * Default colors used by a [TimePicker] in different states
+     *
+     * @param clockDialColor The color of the clock dial.
+     * @param clockDialSelectedContentColor the color of the numbers of the clock dial when they
+     * are selected or overlapping with the selector
+     * @param clockDialUnselectedContentColor the color of the numbers of the clock dial when they
+     * are unselected
+     * @param selectorColor The color of the clock dial selector.
+     * @param containerColor The container color of the time picker.
+     * @param periodSelectorBorderColor the color used for the border of the AM/PM toggle.
+     * @param periodSelectorSelectedContainerColor the color used for the selected container of
+     * the AM/PM toggle
+     * @param periodSelectorUnselectedContainerColor the color used for the unselected container
+     * of the AM/PM toggle
+     * @param periodSelectorSelectedContentColor color used for the selected content of
+     * the AM/PM toggle
+     * @param periodSelectorUnselectedContentColor color used for the unselected content
+     * of the AM/PM toggle
+     * @param timeSelectorSelectedContainerColor color used for the selected container of the
+     * display buttons to switch between hour and minutes
+     * @param timeSelectorUnselectedContainerColor color used for the unselected container of the
+     * display buttons to switch between hour and minutes
+     * @param timeSelectorSelectedContentColor color used for the selected content of the display
+     * buttons to switch between hour and minutes
+     * @param timeSelectorUnselectedContentColor color used for the unselected content of the
+     * display buttons to switch between hour and minutes
+     */
+    @Composable
+    fun colors(
+        clockDialColor: Color = ClockDialColor.toColor(),
+        clockDialSelectedContentColor: Color = ClockDialSelectedLabelTextColor.toColor(),
+        clockDialUnselectedContentColor: Color = ClockDialUnselectedLabelTextColor.toColor(),
+        selectorColor: Color = ClockDialSelectorHandleContainerColor.toColor(),
+        containerColor: Color = ContainerColor.toColor(),
+        periodSelectorBorderColor: Color = PeriodSelectorOutlineColor.toColor(),
+        periodSelectorSelectedContainerColor: Color =
+            PeriodSelectorSelectedContainerColor.toColor(),
+        periodSelectorUnselectedContainerColor: Color = Color.Transparent,
+        periodSelectorSelectedContentColor: Color =
+            PeriodSelectorSelectedLabelTextColor.toColor(),
+        periodSelectorUnselectedContentColor: Color =
+            PeriodSelectorUnselectedLabelTextColor.toColor(),
+        timeSelectorSelectedContainerColor: Color =
+            TimeSelectorSelectedContainerColor.toColor(),
+        timeSelectorUnselectedContainerColor: Color =
+            TimeSelectorUnselectedContainerColor.toColor(),
+        timeSelectorSelectedContentColor: Color =
+            TimeSelectorSelectedLabelTextColor.toColor(),
+        timeSelectorUnselectedContentColor: Color =
+            TimeSelectorUnselectedLabelTextColor.toColor(),
+    ) = TimePickerColors(
+        clockDialColor = clockDialColor,
+        clockDialSelectedContentColor = clockDialSelectedContentColor,
+        clockDialUnselectedContentColor = clockDialUnselectedContentColor,
+        selectorColor = selectorColor,
+        containerColor = containerColor,
+        periodSelectorBorderColor = periodSelectorBorderColor,
+        periodSelectorSelectedContainerColor = periodSelectorSelectedContainerColor,
+        periodSelectorUnselectedContainerColor = periodSelectorUnselectedContainerColor,
+        periodSelectorSelectedContentColor = periodSelectorSelectedContentColor,
+        periodSelectorUnselectedContentColor = periodSelectorUnselectedContentColor,
+        timeSelectorSelectedContainerColor = timeSelectorSelectedContainerColor,
+        timeSelectorUnselectedContainerColor = timeSelectorUnselectedContainerColor,
+        timeSelectorSelectedContentColor = timeSelectorSelectedContentColor,
+        timeSelectorUnselectedContentColor = timeSelectorUnselectedContentColor
+    )
+}
+
+/**
+ * Represents the colors used by a [TimePicker] in different states
+ *
+ * See [TimePickerDefaults.colors] for the default implementation that follows Material
+ * specifications.
+ */
+@Immutable
+@ExperimentalMaterial3Api
+class TimePickerColors internal constructor(
+    internal val clockDialColor: Color,
+    internal val selectorColor: Color,
+    internal val containerColor: Color,
+    internal val periodSelectorBorderColor: Color,
+    private val clockDialSelectedContentColor: Color,
+    private val clockDialUnselectedContentColor: Color,
+    private val periodSelectorSelectedContainerColor: Color,
+    private val periodSelectorUnselectedContainerColor: Color,
+    private val periodSelectorSelectedContentColor: Color,
+    private val periodSelectorUnselectedContentColor: Color,
+    private val timeSelectorSelectedContainerColor: Color,
+    private val timeSelectorUnselectedContainerColor: Color,
+    private val timeSelectorSelectedContentColor: Color,
+    private val timeSelectorUnselectedContentColor: Color,
+) {
+    internal fun periodSelectorContainerColor(selected: Boolean) =
+        if (selected) {
+            periodSelectorSelectedContainerColor
+        } else {
+            periodSelectorUnselectedContainerColor
+        }
+
+    internal fun periodSelectorContentColor(selected: Boolean) =
+        if (selected) {
+            periodSelectorSelectedContentColor
+        } else {
+            periodSelectorUnselectedContentColor
+        }
+
+    internal fun timeSelectorContainerColor(selected: Boolean) =
+        if (selected) {
+            timeSelectorSelectedContainerColor
+        } else {
+            timeSelectorUnselectedContainerColor
+        }
+
+    internal fun timeSelectorContentColor(selected: Boolean) =
+        if (selected) {
+            timeSelectorSelectedContentColor
+        } else {
+            timeSelectorUnselectedContentColor
+        }
+
+    internal fun clockDialContentColor(selected: Boolean) =
+        if (selected) {
+            clockDialSelectedContentColor
+        } else {
+            clockDialUnselectedContentColor
+        }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as TimePickerColors
+
+        if (clockDialColor != other.clockDialColor) return false
+        if (selectorColor != other.selectorColor) return false
+        if (containerColor != other.containerColor) return false
+        if (periodSelectorBorderColor != other.periodSelectorBorderColor) return false
+        if (periodSelectorSelectedContainerColor != other.periodSelectorSelectedContainerColor)
+            return false
+        if (periodSelectorUnselectedContainerColor != other.periodSelectorUnselectedContainerColor)
+            return false
+        if (periodSelectorSelectedContentColor != other.periodSelectorSelectedContentColor)
+            return false
+        if (periodSelectorUnselectedContentColor != other.periodSelectorUnselectedContentColor)
+            return false
+        if (timeSelectorSelectedContainerColor != other.timeSelectorSelectedContainerColor)
+            return false
+        if (timeSelectorUnselectedContainerColor != other.timeSelectorUnselectedContainerColor)
+            return false
+        if (timeSelectorSelectedContentColor != other.timeSelectorSelectedContentColor)
+            return false
+        if (timeSelectorUnselectedContentColor != other.timeSelectorUnselectedContentColor)
+            return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = clockDialColor.hashCode()
+        result = 31 * result + selectorColor.hashCode()
+        result = 31 * result + containerColor.hashCode()
+        result = 31 * result + periodSelectorBorderColor.hashCode()
+        result = 31 * result + periodSelectorSelectedContainerColor.hashCode()
+        result = 31 * result + periodSelectorUnselectedContainerColor.hashCode()
+        result = 31 * result + periodSelectorSelectedContentColor.hashCode()
+        result = 31 * result + periodSelectorUnselectedContentColor.hashCode()
+        result = 31 * result + timeSelectorSelectedContainerColor.hashCode()
+        result = 31 * result + timeSelectorUnselectedContainerColor.hashCode()
+        result = 31 * result + timeSelectorSelectedContentColor.hashCode()
+        result = 31 * result + timeSelectorUnselectedContentColor.hashCode()
+        return result
+    }
+}
+
+/**
+ * Creates a [TimePickerState] for a time picker that is remembered across compositions
+ * and configuration changes.
+ *
+ * @param initialHour starting hour for this state, will be displayed in the time picker when launched
+ * Ranges from 0 to 23
+ * @param initialMinute starting minute for this state, will be displayed in the time picker when
+ * launched. Ranges from 0 to 59
+ * @param is24Hour The format for this time picker `false` for 12 hour format with an AM/PM toggle
+ * or `true` for 24 hour format without toggle. Defaults to follow system setting.
+ */
+@Composable
+@ExperimentalMaterial3Api
+fun rememberTimePickerState(
+    initialHour: Int = 0,
+    initialMinute: Int = 0,
+    is24Hour: Boolean = is24HourFormat,
+): TimePickerState = rememberSaveable(
+    saver = TimePickerState.Saver()
+) {
+    TimePickerState(
+        initialHour = initialHour,
+        initialMinute = initialMinute,
+        is24Hour = is24Hour,
+    )
+}
+
+/**
+ * A class to handle state changes in a [TimePicker]
+ *
+ * @sample androidx.compose.material3.samples.TimePickerSample
+ *
+ * @param initialHour
+ *  starting hour for this state, will be displayed in the time picker when launched
+ *  Ranges from 0 to 23
+ * @param initialMinute
+ *  starting minute for this state, will be displayed in the time picker when launched.
+ *  Ranges from 0 to 59
+ * @param is24Hour The format for this time picker `false` for 12 hour format with an AM/PM toggle
+ *  or `true` for 24 hour format without toggle.
+ */
+@Stable
+class TimePickerState(
+    initialHour: Int,
+    initialMinute: Int,
+    is24Hour: Boolean,
+) {
+    init {
+        require(initialHour in 0..23) { "initialHour should in [0..23] range" }
+        require(initialHour in 0..59) { "initialMinute should be in [0..59] range" }
+    }
+
+    val minute: Int get() = minuteAngle.toMinute()
+    val hour: Int get() = hourAngle.toHour() + if (isAfternoon) 12 else 0
+    val is24hour: Boolean = is24Hour
+
+    internal val hourForDisplay: Int get() = hourForDisplay(hour)
+    internal val selectorPos by derivedStateOf(structuralEqualityPolicy()) {
+        val inInnerCircle = isInnerCircle
+        val handleRadiusPx = ClockDialSelectorHandleContainerSize / 2
+        val selectorLength = if (is24Hour && inInnerCircle && selection == Selection.Hour) {
+            InnerCircleRadius
+        } else {
+            OuterCircleSizeRadius
+        }.minus(handleRadiusPx)
+
+        val length = selectorLength + handleRadiusPx
+        val offsetX = length * cos(currentAngle.value) + ClockDialContainerSize / 2
+        val offsetY = length * sin(currentAngle.value) + ClockDialContainerSize / 2
+
+        DpOffset(offsetX, offsetY)
+    }
+
+    internal val values by derivedStateOf {
+        if (selection == Selection.Minute) Minutes else Hours
+    }
+
+    internal var selection by mutableStateOf(Selection.Hour)
+    internal var isAfternoonToggle by mutableStateOf(initialHour > 12 && !is24Hour)
+    internal var isInnerCircle by mutableStateOf(initialHour > 12 || initialHour == 0)
+
+    private var hourAngle by mutableStateOf(RadiansPerHour * initialHour % 12 - FullCircle / 4)
+    private var minuteAngle by mutableStateOf(RadiansPerMinute * initialMinute - FullCircle / 4)
+
+    private val mutex = MutatorMutex()
+    private val isAfternoon by derivedStateOf {
+        (is24hour && isInnerCircle && hourAngle.toHour() != 0) || isAfternoonToggle
+    }
+
+    internal val currentAngle = Animatable(hourAngle)
+
+    internal fun isSelected(value: Int): Boolean =
+        if (selection == Selection.Minute) {
+            value == minute
+        } else {
+            hour == (value + if (isAfternoon) 12 else 0)
+        }
+
+    internal suspend fun update(value: Float, fromTap: Boolean = false) {
+        mutex.mutate(MutatePriority.UserInput) {
+            if (selection == Selection.Hour) {
+                hourAngle = value.toHour() % 12 * RadiansPerHour
+            } else if (fromTap) {
+                minuteAngle = (value.toMinute() - value.toMinute() % 5) * RadiansPerMinute
+            } else {
+                minuteAngle = value.toMinute() * RadiansPerMinute
+            }
+
+            if (fromTap) {
+                currentAngle.snapTo(minuteAngle)
+            } else {
+                currentAngle.snapTo(offsetHour(value))
+            }
+        }
+    }
+
+    internal suspend fun animateToCurrent() {
+        val (start, end) = if (selection == Selection.Hour) {
+            valuesForAnimation(minuteAngle, hourAngle)
+        } else {
+            valuesForAnimation(hourAngle, minuteAngle)
+        }
+
+        currentAngle.snapTo(start)
+        currentAngle.animateTo(end, tween(200))
+    }
+
+    private fun hourForDisplay(hour: Int): Int = when {
+        is24hour && isInnerCircle && hour == 0 -> 12
+        is24hour -> hour % 24
+        hour % 12 == 0 -> 12
+        isAfternoon -> hour - 12
+        else -> hour
+    }
+
+    private fun offsetHour(angle: Float): Float {
+        val ret = angle + QuarterCircle.toFloat()
+        return if (ret < 0) ret + FullCircle else ret
+    }
+
+    private fun Float.toHour(): Int {
+        val hourOffset: Float = RadiansPerHour / 2
+        val totalOffset = hourOffset + QuarterCircle
+        return ((this + totalOffset) / RadiansPerHour).toInt() % 12
+    }
+
+    private fun Float.toMinute(): Int {
+        val hourOffset: Float = RadiansPerMinute / 2
+        val totalOffset = hourOffset + QuarterCircle
+        return ((this + totalOffset) / RadiansPerMinute).toInt() % 60
+    }
+
+    suspend fun settle() {
+        val targetValue = valuesForAnimation(currentAngle.value, minuteAngle)
+        currentAngle.snapTo(targetValue.first)
+        currentAngle.animateTo(targetValue.second, tween(200))
+    }
+
+    companion object {
+        /**
+         * The default [Saver] implementation for [TimePickerState].
+         */
+        fun Saver(): Saver<TimePickerState, *> = Saver(
+            save = {
+                listOf(
+                    it.minute,
+                    it.hour,
+                    it.is24hour
+                )
+            },
+            restore = { value ->
+                TimePickerState(
+                    initialHour = value[0] as Int,
+                    initialMinute = value[1] as Int,
+                    is24Hour = value[2] as Boolean
+                )
+            }
+        )
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun ClockDisplay(state: TimePickerState, colors: TimePickerColors) {
+    Row(horizontalArrangement = Arrangement.Center) {
+        TimeSelector(state.hourForDisplay, state, Selection.Hour, colors = colors)
+        DisplaySeparator()
+        TimeSelector(state.minute, state, Selection.Minute, colors = colors)
+        if (!state.is24hour) {
+            Spacer(modifier = Modifier.width(PeriodToggleTopMargin))
+            PeriodToggle(state, colors)
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun PeriodToggle(state: TimePickerState, colors: TimePickerColors) {
+    val borderStroke = BorderStroke(
+        TimePickerTokens.PeriodSelectorOutlineWidth,
+        colors.periodSelectorBorderColor
+    )
+
+    val shape = PeriodSelectorContainerShape.toShape() as CornerBasedShape
+    val contentDescription = getString(Strings.TimePickerPeriodToggle)
+    Column(
+        Modifier
+            .semantics { this.contentDescription = contentDescription }
+            .selectableGroup()
+            .size(PeriodSelectorVerticalContainerWidth, PeriodSelectorVerticalContainerHeight)
+            .border(border = borderStroke, shape = shape)
+    ) {
+        ToggleItem(
+            checked = !state.isAfternoonToggle,
+            shape = shape.top(),
+            onClick = {
+                state.isAfternoonToggle = false
+            },
+            colors = colors,
+        ) { Text(text = getString(string = Strings.TimePickerAM)) }
+        Spacer(
+            Modifier
+                .fillMaxWidth()
+                .height(TimePickerTokens.PeriodSelectorOutlineWidth)
+                .background(color = PeriodSelectorOutlineColor.toColor())
+        )
+        ToggleItem(
+            checked =
+            state.isAfternoonToggle,
+            shape = shape.bottom(),
+            onClick = {
+                state.isAfternoonToggle = true
+            },
+            colors = colors,
+        ) { Text(getString(string = Strings.TimePickerPM)) }
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun ColumnScope.ToggleItem(
+    checked: Boolean,
+    shape: Shape,
+    onClick: () -> Unit,
+    colors: TimePickerColors,
+    content: @Composable RowScope.() -> Unit,
+) {
+    val contentColor = colors.periodSelectorContentColor(checked)
+    val containerColor = colors.periodSelectorContainerColor(checked)
+
+    TextButton(
+        modifier = Modifier
+            .weight(1f)
+            .semantics { selected = checked },
+        contentPadding = PaddingValues(0.dp),
+        shape = shape,
+        onClick = onClick,
+        content = content,
+        colors = ButtonDefaults.textButtonColors(
+            contentColor = contentColor,
+            containerColor = containerColor
+        )
+    )
+}
+
+@Composable
+private fun DisplaySeparator() {
+    val style = copyAndSetFontPadding(
+        style = MaterialTheme.typography.fromToken(TimeSelectorLabelTextFont).copy(
+            textAlign = TextAlign.Center,
+            lineHeightStyle = LineHeightStyle(
+                alignment = LineHeightStyle.Alignment.Center, trim = LineHeightStyle.Trim.Both
+            )
+        ), includeFontPadding = false
+    )
+
+    Box(
+        modifier = Modifier.size(
+            DisplaySeparatorWidth,
+            PeriodSelectorVerticalContainerHeight
+        ), contentAlignment = Alignment.Center
+    ) { Text(text = ":", style = style) }
+}
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+private fun TimeSelector(
+    value: Int,
+    state: TimePickerState,
+    selection: Selection,
+    colors: TimePickerColors,
+) {
+    val selected = state.selection == selection
+    val selectorContentDescription = getString(
+        if (selection == Selection.Hour) {
+            Strings.TimePickerHourSelection
+        } else {
+            Strings.TimePickerMinuteSelection
+        }
+    )
+
+    val containerColor = colors.timeSelectorContainerColor(selected)
+    val contentColor = colors.timeSelectorContentColor(selected)
+    val scope = rememberCoroutineScope()
+    Surface(
+        modifier = Modifier
+            .size(TimeSelectorContainerWidth, TimeSelectorContainerHeight)
+            .semantics(mergeDescendants = true) {
+                role = Role.RadioButton
+                this.contentDescription = selectorContentDescription
+            },
+        onClick = {
+            if (selection != state.selection) {
+                state.selection = selection
+                scope.launch {
+                    state.animateToCurrent()
+                }
+            }
+        },
+        selected = selected,
+        shape = TimeSelectorContainerShape.toShape(),
+        color = containerColor,
+    ) {
+        val valueContentDescription = getString(
+            numberContentDescription(
+                selection = selection,
+                is24Hour = state.is24hour
+            ),
+            value
+        )
+        Box(contentAlignment = Alignment.Center) {
+            val textStyle = MaterialTheme.typography.fromToken(TimeSelectorLabelTextFont)
+            Text(
+                modifier = Modifier.semantics { contentDescription = valueContentDescription },
+                text = value.toLocalString(minDigits = 2),
+                color = contentColor,
+                style = textStyle,
+            )
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun ClockFace(state: TimePickerState, colors: TimePickerColors) {
+    Crossfade(
+        modifier = Modifier
+            .background(shape = CircleShape, color = colors.clockDialColor)
+            .size(ClockDialContainerSize)
+            .semantics {
+                selectableGroup()
+            },
+        targetState = state.values,
+        animationSpec = tween(durationMillis = MotionTokens.DurationMedium3.toInt())
+    ) { screen ->
+        CircularLayout(
+            modifier = Modifier
+                .clockDial(state)
+                .size(ClockDialContainerSize)
+                .drawSelector(state, colors),
+            radius = OuterCircleSizeRadius,
+        ) {
+            CompositionLocalProvider(
+                LocalContentColor provides colors.clockDialContentColor(false)
+            ) {
+                repeat(screen.size) {
+                    val outerValue = if (!state.is24hour) screen[it] else screen[it] % 12
+                    ClockText(
+                        is24Hour = state.is24hour,
+                        selection = state.selection,
+                        value = outerValue,
+                        selected = state.isSelected(it)
+                    )
+                }
+
+                if (state.selection == Selection.Hour && state.is24hour) {
+                    CircularLayout(
+                        modifier = Modifier
+                            .layoutId(LayoutId.InnerCircle)
+                            .size(ClockDialContainerSize)
+                            .background(shape = CircleShape, color = Color.Transparent),
+                        radius = InnerCircleRadius
+                    ) {
+                        repeat(ExtraHours.size) {
+                            val innerValue = ExtraHours[it]
+                            ClockText(
+                                is24Hour = true,
+                                selection = state.selection,
+                                value = innerValue,
+                                selected = state.isSelected(it % 11)
+                            )
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+private fun Modifier.drawSelector(
+    state: TimePickerState,
+    colors: TimePickerColors,
+): Modifier = this.drawWithContent {
+    val selectorOffsetPx = Offset(state.selectorPos.x.toPx(), state.selectorPos.y.toPx())
+
+    val selectorRadius = ClockDialSelectorHandleContainerSize.toPx() / 2
+    val selectorColor = colors.selectorColor
+
+    // clear out the selector section
+    drawCircle(
+        radius = selectorRadius,
+        center = selectorOffsetPx,
+        color = Color.Black,
+        blendMode = BlendMode.Clear,
+    )
+
+    // draw the text composables
+    drawContent()
+
+    // draw the selector and clear out the numbers overlapping
+    drawCircle(
+        radius = selectorRadius,
+        center = selectorOffsetPx,
+        color = selectorColor,
+        blendMode = BlendMode.Xor
+    )
+
+    val strokeWidth = ClockDialSelectorTrackContainerWidth.toPx()
+    val lineLength = selectorOffsetPx.minus(
+        Offset(
+            (selectorRadius * cos(state.currentAngle.value)),
+            (selectorRadius * sin(state.currentAngle.value))
+        )
+    )
+
+    // draw the selector line
+    drawLine(
+        start = size.center,
+        strokeWidth = strokeWidth,
+        end = lineLength,
+        color = selectorColor,
+        blendMode = BlendMode.SrcOver
+    )
+
+    // draw the selector small dot
+    drawCircle(
+        radius = ClockDialSelectorCenterContainerSize.toPx() / 2,
+        center = size.center,
+        color = selectorColor,
+    )
+
+    // draw the portion of the number that was overlapping
+    drawCircle(
+        radius = selectorRadius,
+        center = selectorOffsetPx,
+        color = colors.clockDialContentColor(selected = true),
+        blendMode = BlendMode.DstOver
+    )
+}
+
+private fun Modifier.clockDial(state: TimePickerState): Modifier = composed(debugInspectorInfo {
+    name = "clockDial"
+    properties["state"] = state
+}) {
+    var offsetX by remember { mutableStateOf(0f) }
+    var offsetY by remember { mutableStateOf(0f) }
+    var center by remember { mutableStateOf(IntOffset.Zero) }
+    val scope = rememberCoroutineScope()
+    val maxDist = with(LocalDensity.current) { MaxDistance.toPx() }
+    fun moveSelector(x: Float, y: Float) {
+        if (state.selection == Selection.Hour && state.is24hour) {
+            state.isInnerCircle = dist(x, y, center.x, center.y) < maxDist
+        }
+    }
+    Modifier
+        .onSizeChanged { center = it.center }
+        .pointerInput(state, maxDist, center) {
+            detectTapGestures(
+                onPress = {
+                    offsetX = it.x
+                    offsetY = it.y
+                },
+                onTap = {
+                    scope.launch {
+                        state.update(atan(it.y - center.y, it.x - center.x), true)
+                        moveSelector(it.x, it.y)
+
+                        if (state.selection == Selection.Hour) {
+                            state.selection = Selection.Minute
+                        } else {
+                            state.settle()
+                        }
+                    }
+                },
+            )
+        }
+        .pointerInput(state, maxDist, center) {
+            detectDragGestures(onDragEnd = {
+                scope.launch {
+                    if (state.selection == Selection.Hour) {
+                        state.selection = Selection.Minute
+                        state.animateToCurrent()
+                    } else {
+                        state.settle()
+                    }
+                }
+            }) { _, dragAmount ->
+                scope.launch {
+                    offsetX += dragAmount.x
+                    offsetY += dragAmount.y
+                    state.update(atan(offsetY - center.y, offsetX - center.x))
+                }
+                moveSelector(offsetX, offsetY)
+            }
+        }
+}
+
+@Composable
+private fun ClockText(
+    is24Hour: Boolean,
+    selected: Boolean,
+    selection: Selection,
+    value: Int
+) {
+    val style = MaterialTheme.typography.fromToken(ClockDialLabelTextFont).let {
+        remember(it) {
+            copyAndSetFontPadding(style = it, false)
+        }
+    }
+
+    val contentDescription = getString(
+        numberContentDescription(
+            selection = selection,
+            is24Hour = is24Hour
+        ),
+        value
+    )
+
+    Box(
+        contentAlignment = Alignment.Center,
+        modifier = Modifier
+            .minimumInteractiveComponentSize()
+            .size(MinimumInteractiveSize)
+            .focusable()
+            .semantics(mergeDescendants = true) {
+                this.selected = selected
+                this.contentDescription = contentDescription
+            }
+    ) {
+        Text(
+            text = value.toLocalString(minDigits = 1),
+            style = style,
+        )
+    }
+}
+
+/** Distribute elements evenly on a circle of [radius] */
+@Composable
+private fun CircularLayout(
+    modifier: Modifier = Modifier,
+    radius: Dp,
+    content: @Composable () -> Unit,
+) {
+    Layout(
+        modifier = modifier, content = content
+    ) { measurables, constraints ->
+        val radiusPx = radius.toPx()
+        val itemConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+        val placeables = measurables.filter {
+            it.layoutId != LayoutId.Selector && it.layoutId != LayoutId.InnerCircle
+        }.map { measurable -> measurable.measure(itemConstraints) }
+        val selectorMeasurable = measurables.find { it.layoutId == LayoutId.Selector }
+        val innerMeasurable = measurables.find { it.layoutId == LayoutId.InnerCircle }
+        val theta = FullCircle / (placeables.count())
+        val selectorPlaceable = selectorMeasurable?.measure(itemConstraints)
+        val innerCirclePlaceable = innerMeasurable?.measure(itemConstraints)
+
+        layout(
+            width = constraints.minWidth,
+            height = constraints.minHeight,
+        ) {
+            selectorPlaceable?.place(0, 0)
+
+            placeables.forEachIndexed { i, it ->
+                val centerOffsetX = constraints.maxWidth / 2 - it.width / 2
+                val centerOffsetY = constraints.maxHeight / 2 - it.height / 2
+                val offsetX = radiusPx * cos(theta * i - QuarterCircle) + centerOffsetX
+                val offsetY = radiusPx * sin(theta * i - QuarterCircle) + centerOffsetY
+                it.place(
+                    x = offsetX.roundToInt(), y = offsetY.roundToInt()
+                )
+            }
+
+            innerCirclePlaceable?.place(
+                (constraints.minWidth - innerCirclePlaceable.width) / 2,
+                (constraints.minHeight - innerCirclePlaceable.height) / 2
+            )
+        }
+    }
+}
+
+@Composable
+@ReadOnlyComposable
+private fun numberContentDescription(selection: Selection, is24Hour: Boolean): Strings {
+    if (selection == Selection.Minute) {
+        return Strings.TimePickerMinuteSuffix
+    }
+
+    if (is24Hour) {
+        return Strings.TimePicker24HourSuffix
+    }
+
+    return Strings.TimePickerHourSuffix
+}
+
+private fun valuesForAnimation(current: Float, new: Float): Pair<Float, Float> {
+    var start = current
+    var end = new
+    if (abs(start - end) <= PI) {
+        return Pair(start, end)
+    }
+
+    if (start > PI && end < PI) {
+        end += FullCircle
+    } else if (current < PI && new > PI) {
+        start += FullCircle
+    }
+
+    return Pair(start, end)
+}
+
+private fun dist(x1: Float, y1: Float, x2: Int, y2: Int): Float {
+    val x = x2 - x1
+    val y = y2 - y1
+    return hypot(x.toDouble(), y.toDouble()).toFloat()
+}
+
+private fun atan(y: Float, x: Float): Float {
+    val ret = atan2(y, x) - QuarterCircle.toFloat()
+    return if (ret < 0) ret + FullCircle else ret
+}
+
+private enum class LayoutId {
+    Selector, InnerCircle,
+}
+
+internal enum class Selection {
+    Hour, Minute
+}
+
+private const val FullCircle: Float = (PI * 2).toFloat()
+private const val QuarterCircle = PI / 2
+private const val RadiansPerMinute: Float = FullCircle / 60
+private const val RadiansPerHour: Float = FullCircle / 12f
+
+private val OuterCircleSizeRadius = 101.dp
+private val InnerCircleRadius = 69.dp
+private val ClockDisplayBottomMargin = 36.dp
+private val ClockFaceBottomMargin = 24.dp
+private val PeriodToggleTopMargin = 12.dp
+private val DisplaySeparatorWidth = 24.dp
+
+private val MaxDistance = 74.dp
+private val MinimumInteractiveSize = 48.dp
+private val Minutes = listOf(0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55)
+private val Hours = listOf(12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
+private val ExtraHours = Hours.map { (it % 12 + 12) }
+
+private fun Int.toLocalString(minDigits: Int): String {
+    val formatter = NumberFormat.getIntegerInstance()
+    // Eliminate any use of delimiters when formatting the integer.
+    formatter.isGroupingUsed = false
+    formatter.minimumIntegerDigits = minDigits
+    return formatter.format(this)
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
index d6f7948..302f4c8 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 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.
@@ -28,13 +28,18 @@
 import androidx.compose.foundation.gestures.awaitFirstDown
 import androidx.compose.foundation.gestures.waitForUpOrCancellation
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.paddingFromBaseline
+import androidx.compose.foundation.layout.requiredHeightIn
 import androidx.compose.foundation.layout.sizeIn
 import androidx.compose.material3.tokens.PlainTooltipTokens
+import androidx.compose.material3.tokens.RichTooltipTokens
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -53,6 +58,7 @@
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.semantics.LiveRegionMode
 import androidx.compose.ui.semantics.liveRegion
+import androidx.compose.ui.semantics.onLongClick
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
@@ -62,9 +68,9 @@
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.Popup
 import androidx.compose.ui.window.PopupPositionProvider
-import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
 
 // TODO: add link to m3 doc once created by designer at the top
 /**
@@ -80,10 +86,7 @@
  *
  * @param tooltip the composable that will be used to populate the tooltip's content.
  * @param modifier the [Modifier] to be applied to the tooltip.
- * @param tooltipState handles the state of the tooltip's visibility. If nothing is passed to
- * tooltipState the tooltip will trigger on long press of the anchor content. If control
- * of when the tooltip is triggered is desired pass in a [TooltipState], please note that
- * this will deactivate the default behavior of triggering on long press of the anchor content.
+ * @param tooltipState handles the state of the tooltip's visibility.
  * @param shape the [Shape] that should be applied to the tooltip container.
  * @param containerColor [Color] that will be applied to the tooltip's container.
  * @param contentColor [Color] that will be applied to the tooltip's content.
@@ -94,17 +97,14 @@
 fun PlainTooltipBox(
     tooltip: @Composable () -> Unit,
     modifier: Modifier = Modifier,
-    tooltipState: TooltipState? = null,
+    tooltipState: PlainTooltipState = remember { PlainTooltipState() },
     shape: Shape = TooltipDefaults.plainTooltipContainerShape,
     containerColor: Color = TooltipDefaults.plainTooltipContainerColor,
     contentColor: Color = TooltipDefaults.plainTooltipContentColor,
-    content: @Composable BoxScope.() -> Unit
+    content: @Composable TooltipBoxScope.() -> Unit
 ) {
     val tooltipAnchorPadding = with(LocalDensity.current) { TooltipAnchorPadding.roundToPx() }
     val positionProvider = remember { PlainTooltipPositionProvider(tooltipAnchorPadding) }
-    val scope = rememberCoroutineScope()
-    val showOnLongPress = tooltipState == null
-    val state = tooltipState ?: remember { TooltipState() }
 
     TooltipBox(
         tooltipContent = {
@@ -114,21 +114,75 @@
             )
         },
         modifier = modifier,
-        tooltipState = state,
-        scope = scope,
+        tooltipState = tooltipState,
         shape = shape,
         containerColor = containerColor,
         tooltipPositionProvider = positionProvider,
         elevation = 0.dp,
         maxWidth = PlainTooltipMaxWidth,
-        content = if (showOnLongPress) { {
-            Box(
-                modifier = Modifier.appendLongClick(
-                    onLongClick = { scope.launch { state.show() } }
-                ),
-                content = content
+        content = content
+    )
+}
+
+// TODO: add link to m3 doc once created by designer
+/**
+ * Rich text tooltip that allows the user to pass in a title, text, and action.
+ * Tooltips are used to provide a descriptive message for an anchor.
+ *
+ * Tooltip that is invoked when the anchor is long pressed:
+ *
+ * @sample androidx.compose.material3.samples.RichTooltipSample
+ *
+ * If control of when the tooltip is shown is desired please see
+ *
+ * @sample androidx.compose.material3.samples.RichTooltipWithManualInvocationSample
+ *
+ * @param text the message to be displayed in the center of the tooltip.
+ * @param modifier the [Modifier] to be applied to the tooltip.
+ * @param tooltipState handles the state of the tooltip's visibility.
+ * @param title An optional title for the tooltip.
+ * @param action An optional action for the tooltip.
+ * @param shape the [Shape] that should be applied to the tooltip container.
+ * @param colors [RichTooltipColors] that will be applied to the tooltip's container and content.
+ * @param content the composable that the tooltip will anchor to.
+ */
+@Composable
+@ExperimentalMaterial3Api
+fun RichTooltipBox(
+    text: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    tooltipState: RichTooltipState = remember { RichTooltipState() },
+    title: (@Composable () -> Unit)? = null,
+    action: (@Composable () -> Unit)? = null,
+    shape: Shape = TooltipDefaults.richTooltipContainerShape,
+    colors: RichTooltipColors = TooltipDefaults.richTooltipColors(),
+    content: @Composable TooltipBoxScope.() -> Unit
+) {
+    val tooltipAnchorPadding = with(LocalDensity.current) { TooltipAnchorPadding.roundToPx() }
+    val positionProvider = remember { RichTooltipPositionProvider(tooltipAnchorPadding) }
+
+    SideEffect {
+        // Make the rich tooltip persistent if an action is provided.
+        tooltipState.isPersistent = (action != null)
+    }
+
+    TooltipBox(
+        tooltipContent = {
+            RichTooltipImpl(
+                colors = colors,
+                title = title,
+                text = text,
+                action = action
             )
-        } } else content
+        },
+        shape = shape,
+        containerColor = colors.containerColor,
+        tooltipPositionProvider = positionProvider,
+        tooltipState = tooltipState,
+        elevation = RichTooltipTokens.ContainerElevation,
+        maxWidth = RichTooltipMaxWidth,
+        modifier = modifier,
+        content = content
     )
 }
 
@@ -140,18 +194,63 @@
     modifier: Modifier,
     shape: Shape,
     tooltipState: TooltipState,
-    scope: CoroutineScope,
     containerColor: Color,
     elevation: Dp,
     maxWidth: Dp,
-    content: @Composable BoxScope.() -> Unit,
+    content: @Composable TooltipBoxScope.() -> Unit,
 ) {
+    val coroutineScope = rememberCoroutineScope()
+    val longPressLabel = getString(string = Strings.TooltipLongPressLabel)
+
+    val scope = remember {
+        object : TooltipBoxScope {
+            override fun Modifier.tooltipAnchor(): Modifier {
+                val onLongPress = {
+                    coroutineScope.launch {
+                        tooltipState.show()
+                    }
+                }
+                return pointerInput(tooltipState) {
+                        awaitEachGesture {
+                            val longPressTimeout = viewConfiguration.longPressTimeoutMillis
+                            val pass = PointerEventPass.Initial
+
+                            // wait for the first down press
+                            awaitFirstDown(pass = pass)
+
+                            try {
+                                // listen to if there is up gesture within the longPressTimeout limit
+                                withTimeout(longPressTimeout) {
+                                    waitForUpOrCancellation(pass = pass)
+                                }
+                            } catch (_: PointerEventTimeoutCancellationException) {
+                                // handle long press - Show the tooltip
+                                onLongPress()
+
+                                // consume the children's click handling
+                                val event = awaitPointerEvent(pass = pass)
+                                event.changes.forEach { it.consume() }
+                            }
+                        }
+                    }.semantics(mergeDescendants = true) {
+                        onLongClick(
+                            label = longPressLabel,
+                            action = {
+                                onLongPress()
+                                true
+                            }
+                        )
+                    }
+            }
+        }
+    }
+
     Box {
         Popup(
             popupPositionProvider = tooltipPositionProvider,
             onDismissRequest = {
                 if (tooltipState.isVisible) {
-                    scope.launch { tooltipState.dismiss() }
+                    coroutineScope.launch { tooltipState.dismiss() }
                 }
             }
         ) {
@@ -172,7 +271,7 @@
             )
         }
 
-        content()
+        scope.content()
     }
 }
 
@@ -191,28 +290,135 @@
     }
 }
 
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun RichTooltipImpl(
+    colors: RichTooltipColors,
+    text: @Composable () -> Unit,
+    title: (@Composable () -> Unit)?,
+    action: (@Composable () -> Unit)?
+) {
+    val actionLabelTextStyle =
+        MaterialTheme.typography.fromToken(RichTooltipTokens.ActionLabelTextFont)
+    val subheadTextStyle =
+        MaterialTheme.typography.fromToken(RichTooltipTokens.SubheadFont)
+    val supportingTextStyle =
+        MaterialTheme.typography.fromToken(RichTooltipTokens.SupportingTextFont)
+    Column(
+        modifier = Modifier.padding(horizontal = RichTooltipHorizontalPadding)
+    ) {
+        title?.let {
+            Box(
+                modifier = Modifier.paddingFromBaseline(top = HeightToSubheadFirstLine)
+            ) {
+                CompositionLocalProvider(
+                    LocalContentColor provides colors.titleContentColor,
+                    LocalTextStyle provides subheadTextStyle,
+                    content = it
+                )
+            }
+        }
+        Box(
+            modifier = Modifier.textVerticalPadding(title != null, action != null)
+        ) {
+            CompositionLocalProvider(
+                LocalContentColor provides colors.contentColor,
+                LocalTextStyle provides supportingTextStyle,
+                content = text
+            )
+        }
+        action?.let {
+            Box(
+                modifier = Modifier
+                    .requiredHeightIn(min = ActionLabelMinHeight)
+                    .padding(bottom = ActionLabelBottomPadding)
+            ) {
+                CompositionLocalProvider(
+                    LocalContentColor provides colors.actionContentColor,
+                    LocalTextStyle provides actionLabelTextStyle,
+                    content = it
+                )
+            }
+        }
+    }
+}
+
 /**
- * Tooltip defaults that contain default values for both [PlainTooltipBox] and RichTooltipBox
+ * Tooltip defaults that contain default values for both [PlainTooltipBox] and [RichTooltipBox]
  */
 @ExperimentalMaterial3Api
 object TooltipDefaults {
     /**
-     * The default [Shape] for the tooltip's container.
+     * The default [Shape] for a [PlainTooltipBox]'s container.
      */
     val plainTooltipContainerShape: Shape
         @Composable get() = PlainTooltipTokens.ContainerShape.toShape()
 
     /**
-     * The default [Color] for the tooltip's container.
+     * The default [Color] for a [PlainTooltipBox]'s container.
      */
     val plainTooltipContainerColor: Color
         @Composable get() = PlainTooltipTokens.ContainerColor.toColor()
 
     /**
-     * The default [color] for the content within the tooltip.
+     * The default [color] for the content within the [PlainTooltipBox].
      */
     val plainTooltipContentColor: Color
         @Composable get() = PlainTooltipTokens.SupportingTextColor.toColor()
+
+    /**
+     * The default [Shape] for a [RichTooltipBox]'s container.
+     */
+    val richTooltipContainerShape: Shape @Composable get() =
+        RichTooltipTokens.ContainerShape.toShape()
+
+    /**
+     * Method to create a [RichTooltipColors] for [RichTooltipBox]
+     * using [RichTooltipTokens] to obtain the default colors.
+     */
+    @Composable
+    fun richTooltipColors(
+        containerColor: Color = RichTooltipTokens.ContainerColor.toColor(),
+        contentColor: Color = RichTooltipTokens.SupportingTextColor.toColor(),
+        titleContentColor: Color = RichTooltipTokens.SubheadColor.toColor(),
+        actionContentColor: Color = RichTooltipTokens.ActionLabelTextColor.toColor(),
+    ): RichTooltipColors =
+        RichTooltipColors(
+            containerColor = containerColor,
+            contentColor = contentColor,
+            titleContentColor = titleContentColor,
+            actionContentColor = actionContentColor
+        )
+}
+
+@Stable
+@Immutable
+@ExperimentalMaterial3Api
+class RichTooltipColors(
+    val containerColor: Color,
+    val contentColor: Color,
+    val titleContentColor: Color,
+    val actionContentColor: Color
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is RichTooltipColors) return false
+
+        if (containerColor != other.containerColor) return false
+        if (contentColor != other.contentColor) return false
+        if (titleContentColor != other.titleContentColor) return false
+        if (actionContentColor != other.actionContentColor) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = containerColor.hashCode()
+        result = 31 * result + contentColor.hashCode()
+        result = 31 * result + titleContentColor.hashCode()
+        result = 31 * result + actionContentColor.hashCode()
+        return result
+    }
 }
 
 private class PlainTooltipPositionProvider(
@@ -236,6 +442,47 @@
     }
 }
 
+private data class RichTooltipPositionProvider(
+    val tooltipAnchorPadding: Int
+) : PopupPositionProvider {
+    override fun calculatePosition(
+        anchorBounds: IntRect,
+        windowSize: IntSize,
+        layoutDirection: LayoutDirection,
+        popupContentSize: IntSize
+    ): IntOffset {
+        var x = anchorBounds.right
+        // Try to shift it to the left of the anchor
+        // if the tooltip would collide with the right side of the screen
+        if (x + popupContentSize.width > windowSize.width) {
+            x = anchorBounds.left - popupContentSize.width
+            // Center if it'll also collide with the left side of the screen
+            if (x < 0) x = anchorBounds.left + (anchorBounds.width - popupContentSize.width) / 2
+        }
+
+        // Tooltip prefers to be above the anchor,
+        // but if this causes the tooltip to overlap with the anchor
+        // then we place it below the anchor
+        var y = anchorBounds.top - popupContentSize.height - tooltipAnchorPadding
+        if (y < 0)
+            y = anchorBounds.bottom + tooltipAnchorPadding
+        return IntOffset(x, y)
+    }
+}
+
+private fun Modifier.textVerticalPadding(
+    subheadExists: Boolean,
+    actionExists: Boolean
+): Modifier {
+    return if (!subheadExists && !actionExists) {
+        this.padding(vertical = PlainTooltipVerticalPadding)
+    } else {
+        this
+            .paddingFromBaseline(top = HeightFromSubheadToTextFirstLine)
+            .padding(bottom = TextBottomPadding)
+    }
+}
+
 private fun Modifier.animateTooltip(
     showTooltip: Boolean
 ): Modifier = composed(
@@ -291,98 +538,204 @@
     )
 }
 
-private fun Modifier.appendLongClick(
-    onLongClick: () -> Unit
-) = this.pointerInput(Unit) {
-        awaitEachGesture {
-            val longPressTimeout = viewConfiguration.longPressTimeoutMillis
-            val pass = PointerEventPass.Initial
-
-            // wait for the first down press
-            awaitFirstDown(pass = pass)
-
-            try {
-                // listen to if there is up gesture within the longPressTimeout limit
-                withTimeout(longPressTimeout) {
-                    waitForUpOrCancellation(pass = pass)
-                }
-            } catch (_: PointerEventTimeoutCancellationException) {
-                // handle long press
-                onLongClick()
-
-                // consume the children's click handling
-                val event = awaitPointerEvent(pass = pass)
-                event.changes.forEach { it.consume() }
-            }
-        }
-    }
+/**
+ * Scope of [PlainTooltipBox] and RichTooltipBox
+ */
+@ExperimentalMaterial3Api
+interface TooltipBoxScope {
+    /**
+     * [Modifier] that should be applied to the anchor composable when showing the tooltip
+     * after long pressing the anchor composable is desired. It appends a long click to
+     * the composable that this modifier is chained with.
+     */
+    fun Modifier.tooltipAnchor(): Modifier
+}
 
 /**
  * The state that is associated with an instance of a tooltip.
- * Each instance of tooltips should have its own [TooltipState]
- * while will be used to synchronize the tooltips shown.
+ * Each instance of tooltips should have its own [TooltipState] it
+ * will be used to synchronize the tooltips shown via [TooltipSync].
  */
 @Stable
 @ExperimentalMaterial3Api
-class TooltipState {
+internal sealed interface TooltipState {
     /**
      * [Boolean] that will be used to update the visibility
      * state of the associated tooltip.
      */
-    var isVisible by mutableStateOf(false)
-        private set
+    val isVisible: Boolean
 
     /**
      * Show the tooltip associated with the current [TooltipState].
+     * When this method is called all of the other tooltips currently
+     * being shown will dismiss.
      */
-    suspend fun show() { show(this) }
+    suspend fun show()
 
     /**
      * Dismiss the tooltip associated with
      * this [TooltipState] if it's currently being shown.
      */
-    suspend fun dismiss() {
-        if (this == mutexOwner)
-            dismissCurrentTooltip()
+    suspend fun dismiss()
+}
+
+/**
+ * The [TooltipState] that should be used with [RichTooltipBox]
+ */
+@Stable
+@ExperimentalMaterial3Api
+class RichTooltipState : TooltipState {
+    /**
+     * [Boolean] that will be used to update the visibility
+     * state of the associated tooltip.
+     */
+    override var isVisible: Boolean by mutableStateOf(false)
+        internal set
+
+    /**
+     * If isPersistent is true, then the tooltip will only be dismissed when the user clicks
+     * outside the bounds of the tooltip or if [TooltipState.dismiss] is called. When isPersistent
+     * is false, the tooltip will dismiss after a short duration. If an action composable is
+     * provided to the [RichTooltipBox] that the [RichTooltipState] is associated with, then the
+     * isPersistent will be set to true.
+     */
+    internal var isPersistent: Boolean by mutableStateOf(false)
+
+    /**
+     * Show the tooltip associated with the current [RichTooltipState].
+     * It will persist or dismiss after a short duration depending on [isPersistent].
+     * When this method is called, all of the other tooltips currently
+     * being shown will dismiss.
+     */
+    override suspend fun show() {
+        TooltipSync.show(
+            state = this,
+            persistent = isPersistent
+        )
     }
 
     /**
-     * Companion object used to synchronize
-     * multiple [TooltipState]s, ensuring that there will
-     * only be one tooltip shown on the screen at any given time.
+     * Dismiss the tooltip associated with
+     * this [RichTooltipState] if it's currently being shown.
      */
-    private companion object {
-        val mutatorMutex: MutatorMutex = MutatorMutex()
-        var mutexOwner: TooltipState? = null
+    override suspend fun dismiss() {
+        TooltipSync.dismissCurrentTooltip(this)
+    }
+}
 
-        /**
-         * Shows the tooltip associated with [TooltipState],
-         * it dismisses any tooltip currently being shown.
-         */
-        suspend fun show(
-            state: TooltipState
-        ) {
-            mutatorMutex.mutate(MutatePriority.Default) {
-                try {
-                    mutexOwner = state
-                    // show the tooltip associated with the
-                    // tooltipState until dismissal or timeout.
+/**
+ * The [TooltipState] that should be used with [RichTooltipBox]
+ */
+@Stable
+@ExperimentalMaterial3Api
+class PlainTooltipState : TooltipState {
+    /**
+     * [Boolean] that will be used to update the visibility
+     * state of the associated tooltip.
+     */
+    override var isVisible by mutableStateOf(false)
+        internal set
+
+    /**
+     * Show the tooltip associated with the current [PlainTooltipState].
+     * It will dismiss after a short duration. When this method is called,
+     * all of the other tooltips currently being shown will dismiss.
+     */
+    override suspend fun show() {
+        TooltipSync.show(
+            state = this,
+            persistent = false
+        )
+    }
+
+    /**
+     * Dismiss the tooltip associated with
+     * this [PlainTooltipState] if it's currently being shown.
+     */
+    override suspend fun dismiss() {
+        TooltipSync.dismissCurrentTooltip(this)
+    }
+}
+
+/**
+ * Object used to synchronize
+ * multiple [TooltipState]s, ensuring that there will
+ * only be one tooltip shown on the screen at any given time.
+ */
+@Stable
+@ExperimentalMaterial3Api
+private object TooltipSync {
+    val mutatorMutex: MutatorMutex = MutatorMutex()
+    var mutexOwner: TooltipState? = null
+
+    /**
+     * Shows the tooltip associated with [TooltipState],
+     * it dismisses any tooltip currently being shown.
+     */
+    suspend fun show(
+        state: TooltipState,
+        persistent: Boolean
+    ) {
+        val runBlock: suspend () -> Unit
+        val cleanUp: () -> Unit
+
+        when (state) {
+            is PlainTooltipState -> {
+                /**
+                 * Show associated tooltip for [TooltipDuration] amount of time.
+                 */
+                runBlock = {
                     state.isVisible = true
                     delay(TooltipDuration)
-                } finally {
-                    mutexOwner = null
-                    // timeout or cancellation has occurred
-                    // and we close out the current tooltip.
-                    state.isVisible = false
                 }
+                /**
+                 * When the mutex is taken, we just dismiss the associated tooltip.
+                 */
+                cleanUp = { state.isVisible = false }
+            }
+            is RichTooltipState -> {
+                /**
+                 * Show associated tooltip for [TooltipDuration] amount of time
+                 * or until tooltip is explicitly dismissed depending on [persistent].
+                 */
+                runBlock = {
+                    if (persistent) {
+                        suspendCancellableCoroutine<Unit> {
+                            state.isVisible = true
+                        }
+                    } else {
+                        state.isVisible = true
+                        delay(TooltipDuration)
+                    }
+                }
+                /**
+                 * When the mutex is taken, we just dismiss the associated tooltip.
+                 */
+                cleanUp = { state.isVisible = false }
             }
         }
 
-        /**
-         * Dismisses the tooltip currently
-         * being shown by freeing up the lock.
-         */
-        suspend fun dismissCurrentTooltip() {
+        mutatorMutex.mutate(MutatePriority.Default) {
+            try {
+                mutexOwner = state
+                runBlock()
+            } finally {
+                mutexOwner = null
+                // timeout or cancellation has occurred
+                // and we close out the current tooltip.
+                cleanUp()
+            }
+        }
+    }
+
+    /**
+     * Dismisses the tooltip currently
+     * being shown by freeing up the lock.
+     */
+    suspend fun dismissCurrentTooltip(
+        state: TooltipState
+    ) {
+        if (state == mutexOwner) {
             mutatorMutex.mutate(MutatePriority.UserInput) {
                 /* Do nothing, we're just freeing up the mutex */
             }
@@ -394,8 +747,18 @@
 internal val TooltipMinHeight = 24.dp
 internal val TooltipMinWidth = 40.dp
 private val PlainTooltipMaxWidth = 200.dp
-private val PlainTooltipContentPadding = PaddingValues(8.dp, 4.dp)
+private val PlainTooltipVerticalPadding = 4.dp
+private val PlainTooltipHorizontalPadding = 8.dp
+private val PlainTooltipContentPadding =
+    PaddingValues(PlainTooltipHorizontalPadding, PlainTooltipVerticalPadding)
+private val RichTooltipMaxWidth = 320.dp
+internal val RichTooltipHorizontalPadding = 16.dp
+private val HeightToSubheadFirstLine = 28.dp
+private val HeightFromSubheadToTextFirstLine = 24.dp
+private val TextBottomPadding = 16.dp
+private val ActionLabelMinHeight = 36.dp
+private val ActionLabelBottomPadding = 8.dp
 internal const val TooltipDuration = 1500L
-// No specification for fade in and fade out duration, so aligning it with the behavior for snackbar
+// No specification for fade in and fade out duration, so aligning it with the behavior for snack bar
 private const val TooltipFadeInDuration = 150
 private const val TooltipFadeOutDuration = 75
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/compose-material-3-documentation.md b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/compose-material-3-documentation.md
index 84336f2..e797721 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/compose-material-3-documentation.md
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/compose-material-3-documentation.md
@@ -10,7 +10,7 @@
 
 In this page, you'll find documentation for types, properties, and functions available in the `androidx.compose.material3` package.
 
-For more information, check out the <a href="https://developer.android.com/jetpack/compose/themes/material#material3" class="external" target="_blank">Material Design 3 and Material You</a> section in the Material Theming in Compose guide.
+For more information, check out <a href="https://developer.android.com/jetpack/compose/designsystems/material3" class="external" target="_blank">Material Design 3 in Compose</a>.
 
 ## Overview
 
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/DateInputModalTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/DateInputModalTokens.kt
index 4742993..8201608 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/DateInputModalTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/DateInputModalTokens.kt
@@ -1,4 +1,19 @@
-// VERSION: v0_126
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_157
 // GENERATED CODE - DO NOT MODIFY BY HAND
 
 package androidx.compose.material3.tokens
@@ -17,5 +32,5 @@
     val HeaderHeadlineColor = ColorSchemeKeyTokens.OnSurfaceVariant
     val HeaderHeadlineFont = TypographyKeyTokens.HeadlineLarge
     val HeaderSupportingTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
-    val HeaderSupportingTextFont = TypographyKeyTokens.LabelMedium
+    val HeaderSupportingTextFont = TypographyKeyTokens.LabelLarge
 }
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/DatePickerModalTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/DatePickerModalTokens.kt
index f84cc5d..fbb8baf 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/DatePickerModalTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/DatePickerModalTokens.kt
@@ -1,4 +1,19 @@
-// VERSION: v0_126
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_157
 // GENERATED CODE - DO NOT MODIFY BY HAND
 
 package androidx.compose.material3.tokens
@@ -8,14 +23,14 @@
 internal object DatePickerModalTokens {
     val ContainerColor = ColorSchemeKeyTokens.Surface
     val ContainerElevation = ElevationTokens.Level3
-    val ContainerHeight = 512.0.dp
+    val ContainerHeight = 568.0.dp
     val ContainerShape = ShapeKeyTokens.CornerExtraLarge
     val ContainerSurfaceTintLayerColor = ColorSchemeKeyTokens.SurfaceTint
-    val ContainerWidth = 328.0.dp
+    val ContainerWidth = 360.0.dp
     val DateContainerHeight = 40.0.dp
     val DateContainerShape = ShapeKeyTokens.CornerFull
     val DateContainerWidth = 40.0.dp
-    val DateLabelTextFont = TypographyKeyTokens.BodySmall
+    val DateLabelTextFont = TypographyKeyTokens.BodyLarge
     val DateSelectedContainerColor = ColorSchemeKeyTokens.Primary
     val DateSelectedLabelTextColor = ColorSchemeKeyTokens.OnPrimary
     val DateStateLayerHeight = 40.0.dp
@@ -26,11 +41,11 @@
     val DateTodayLabelTextColor = ColorSchemeKeyTokens.Primary
     val DateUnselectedLabelTextColor = ColorSchemeKeyTokens.OnSurface
     val HeaderContainerHeight = 120.0.dp
-    val HeaderContainerWidth = 328.0.dp
+    val HeaderContainerWidth = 360.0.dp
     val HeaderHeadlineColor = ColorSchemeKeyTokens.OnSurfaceVariant
     val HeaderHeadlineFont = TypographyKeyTokens.HeadlineLarge
     val HeaderSupportingTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
-    val HeaderSupportingTextFont = TypographyKeyTokens.LabelMedium
+    val HeaderSupportingTextFont = TypographyKeyTokens.LabelLarge
     val RangeSelectionActiveIndicatorContainerColor = ColorSchemeKeyTokens.PrimaryContainer
     val RangeSelectionActiveIndicatorContainerHeight = 40.0.dp
     val RangeSelectionActiveIndicatorContainerShape = ShapeKeyTokens.CornerFull
@@ -42,7 +57,7 @@
     val RangeSelectionMonthSubheadColor = ColorSchemeKeyTokens.OnSurfaceVariant
     val RangeSelectionMonthSubheadFont = TypographyKeyTokens.TitleSmall
     val WeekdaysLabelTextColor = ColorSchemeKeyTokens.OnSurface
-    val WeekdaysLabelTextFont = TypographyKeyTokens.BodySmall
+    val WeekdaysLabelTextFont = TypographyKeyTokens.BodyLarge
     val SelectionYearContainerHeight = 36.0.dp
     val SelectionYearContainerWidth = 72.0.dp
     val SelectionYearLabelTextFont = TypographyKeyTokens.BodyLarge
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/PlainTooltipTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/PlainTooltipTokens.kt
index f26a370..37b4364 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/PlainTooltipTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/PlainTooltipTokens.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 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.
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/RichTooltipTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/RichTooltipTokens.kt
new file mode 100644
index 0000000..9df3345
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/RichTooltipTokens.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.tokens
+
+internal object RichTooltipTokens {
+    val ActionFocusLabelTextColor = ColorSchemeKeyTokens.Primary
+    val ActionHoverLabelTextColor = ColorSchemeKeyTokens.Primary
+    val ActionLabelTextColor = ColorSchemeKeyTokens.Primary
+    val ActionLabelTextFont = TypographyKeyTokens.LabelLarge
+    val ActionPressedLabelTextColor = ColorSchemeKeyTokens.Primary
+    val ContainerColor = ColorSchemeKeyTokens.Surface
+    val ContainerElevation = ElevationTokens.Level2
+    val ContainerShape = ShapeKeyTokens.CornerSmall
+    val ContainerSurfaceTintLayerColor = ColorSchemeKeyTokens.SurfaceTint
+    val SubheadColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val SubheadFont = TypographyKeyTokens.TitleSmall
+    val SupportingTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val SupportingTextFont = TypographyKeyTokens.BodyMedium
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SheetBottomTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SheetBottomTokens.kt
new file mode 100644
index 0000000..ecc53a4
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SheetBottomTokens.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_126
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object SheetBottomTokens {
+    val DockedContainerColor = ColorSchemeKeyTokens.Surface
+    val DockedContainerShape = ShapeKeyTokens.CornerExtraLargeTop
+    val DockedContainerSurfaceTintLayerColor = ColorSchemeKeyTokens.SurfaceTint
+    val DockedDragHandleColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val DockedDragHandleHeight = 4.0.dp
+    const val DockedDragHandleOpacity = 0.4f
+    val DockedDragHandleWidth = 32.0.dp
+    val DockedMinimizedContainerShape = ShapeKeyTokens.CornerNone
+    val DockedModalContainerElevation = ElevationTokens.Level1
+    val DockedStandardContainerElevation = ElevationTokens.Level1
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TimePickerTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TimePickerTokens.kt
new file mode 100644
index 0000000..335414e
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TimePickerTokens.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_157
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object TimePickerTokens {
+    val ClockDialColor = ColorSchemeKeyTokens.SurfaceVariant
+    val ClockDialContainerSize = 256.0.dp
+    val ClockDialLabelTextFont = TypographyKeyTokens.BodyLarge
+    val ClockDialSelectedLabelTextColor = ColorSchemeKeyTokens.OnPrimary
+    val ClockDialSelectorCenterContainerColor = ColorSchemeKeyTokens.Primary
+    val ClockDialSelectorCenterContainerShape = ShapeKeyTokens.CornerFull
+    val ClockDialSelectorCenterContainerSize = 8.0.dp
+    val ClockDialSelectorHandleContainerColor = ColorSchemeKeyTokens.Primary
+    val ClockDialSelectorHandleContainerShape = ShapeKeyTokens.CornerFull
+    val ClockDialSelectorHandleContainerSize = 48.0.dp
+    val ClockDialSelectorTrackContainerColor = ColorSchemeKeyTokens.Primary
+    val ClockDialSelectorTrackContainerWidth = 2.0.dp
+    val ClockDialShape = ShapeKeyTokens.CornerFull
+    val ClockDialUnselectedLabelTextColor = ColorSchemeKeyTokens.OnSurface
+    val ContainerColor = ColorSchemeKeyTokens.Surface
+    val ContainerElevation = ElevationTokens.Level3
+    val ContainerShape = ShapeKeyTokens.CornerExtraLarge
+    val HeadlineColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val HeadlineFont = TypographyKeyTokens.LabelMedium
+    val PeriodSelectorContainerShape = ShapeKeyTokens.CornerSmall
+    val PeriodSelectorHorizontalContainerHeight = 38.0.dp
+    val PeriodSelectorHorizontalContainerWidth = 216.0.dp
+    val PeriodSelectorLabelTextFont = TypographyKeyTokens.TitleMedium
+    val PeriodSelectorOutlineColor = ColorSchemeKeyTokens.Outline
+    val PeriodSelectorOutlineWidth = 1.0.dp
+    val PeriodSelectorSelectedContainerColor = ColorSchemeKeyTokens.TertiaryContainer
+    val PeriodSelectorSelectedFocusLabelTextColor = ColorSchemeKeyTokens.OnTertiaryContainer
+    val PeriodSelectorSelectedHoverLabelTextColor = ColorSchemeKeyTokens.OnTertiaryContainer
+    val PeriodSelectorSelectedLabelTextColor = ColorSchemeKeyTokens.OnTertiaryContainer
+    val PeriodSelectorSelectedPressedLabelTextColor = ColorSchemeKeyTokens.OnTertiaryContainer
+    val PeriodSelectorUnselectedFocusLabelTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val PeriodSelectorUnselectedHoverLabelTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val PeriodSelectorUnselectedLabelTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val PeriodSelectorUnselectedPressedLabelTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val PeriodSelectorVerticalContainerHeight = 80.0.dp
+    val PeriodSelectorVerticalContainerWidth = 52.0.dp
+    val SurfaceTintLayerColor = ColorSchemeKeyTokens.SurfaceTint
+    val TimeSelector24HVerticalContainerWidth = 114.0.dp
+    val TimeSelectorContainerHeight = 80.0.dp
+    val TimeSelectorContainerShape = ShapeKeyTokens.CornerSmall
+    val TimeSelectorContainerWidth = 96.0.dp
+    val TimeSelectorLabelTextFont = TypographyKeyTokens.DisplayLarge
+    val TimeSelectorSelectedContainerColor = ColorSchemeKeyTokens.PrimaryContainer
+    val TimeSelectorSelectedFocusLabelTextColor = ColorSchemeKeyTokens.OnPrimaryContainer
+    val TimeSelectorSelectedHoverLabelTextColor = ColorSchemeKeyTokens.OnPrimaryContainer
+    val TimeSelectorSelectedLabelTextColor = ColorSchemeKeyTokens.OnPrimaryContainer
+    val TimeSelectorSelectedPressedLabelTextColor = ColorSchemeKeyTokens.OnPrimaryContainer
+    val TimeSelectorSeparatorColor = ColorSchemeKeyTokens.OnSurface
+    val TimeSelectorSeparatorFont = TypographyKeyTokens.DisplayLarge
+    val TimeSelectorUnselectedContainerColor = ColorSchemeKeyTokens.SurfaceVariant
+    val TimeSelectorUnselectedFocusLabelTextColor = ColorSchemeKeyTokens.OnSurface
+    val TimeSelectorUnselectedHoverLabelTextColor = ColorSchemeKeyTokens.OnSurface
+    val TimeSelectorUnselectedLabelTextColor = ColorSchemeKeyTokens.OnSurface
+    val TimeSelectorUnselectedPressedLabelTextColor = ColorSchemeKeyTokens.OnSurface
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/CalendarModel.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/CalendarModel.desktop.kt
index ee15b51..5d4556e7 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/CalendarModel.desktop.kt
+++ b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/CalendarModel.desktop.kt
@@ -16,8 +16,43 @@
 
 package androidx.compose.material3
 
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import java.util.Locale
+
 /**
- * Creates a [CalendarModel] to be used by the date picker.
+ * Returns a [CalendarModel] to be used by the date picker.
  */
 @ExperimentalMaterial3Api
-internal actual fun createCalendarModel(): CalendarModel = LegacyCalendarModelImpl()
+internal actual fun CalendarModel(): CalendarModel = LegacyCalendarModelImpl()
+
+/**
+ * Formats a UTC timestamp into a string with a given date format skeleton.
+ *
+ * @param utcTimeMillis a UTC timestamp to format (milliseconds from epoch)
+ * @param skeleton a date format skeleton
+ * @param locale the [Locale] to use when formatting the given timestamp
+ */
+@ExperimentalMaterial3Api
+internal actual fun formatWithSkeleton(
+    utcTimeMillis: Long,
+    skeleton: String,
+    locale: Locale
+): String {
+    // Note: there is no equivalent in Java for Android's DateFormat.getBestDateTimePattern.
+    // The JDK SimpleDateFormat expects a pattern, so the results will be "2023Jan7",
+    // "2023January", etc. in case a skeleton holds an actual ICU skeleton and not a pattern.
+    return LegacyCalendarModelImpl.formatWithPattern(
+        utcTimeMillis = utcTimeMillis,
+        pattern = skeleton,
+        locale = locale
+    )
+}
+
+/**
+ * A composable function that returns the default [Locale].
+ */
+@Composable
+@ReadOnlyComposable
+@ExperimentalMaterial3Api
+internal actual fun defaultLocale(): Locale = Locale.getDefault()
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..0421c3b 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
@@ -17,8 +17,12 @@
 package androidx.compose.material3
 
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+
+import java.util.Locale
 
 @Composable
+@ReadOnlyComposable
 internal actual fun getString(string: Strings): String {
     return when (string) {
         Strings.NavigationMenu -> "Navigation menu"
@@ -35,10 +39,39 @@
         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"
+        Strings.DatePickerTodayDescription -> "Today"
+        Strings.DateInputTitle -> "Select date"
+        Strings.DateInputHeadline -> "Entered date"
+        Strings.DateInputLabel -> "Date"
+        Strings.DateInputHeadlineDescription -> "Entered date: %1$"
+        Strings.DateInputNoInputDescription -> "None"
+        Strings.DateInputInvalidNotAllowed -> "Date not allowed: %1$"
+        Strings.DateInputInvalidForPattern -> "Date does not match expected pattern: %1$"
+        Strings.DateInputInvalidYearRange -> "Date out of expected year range %1$ - %2$"
+        Strings.DatePickerSwitchToCalendarMode -> "Switch to calendar input mode"
+        Strings.DatePickerSwitchToInputMode -> "Switch to text input mode"
+        Strings.TooltipLongPressLabel -> "Show tooltip"
+        Strings.TimePickerAM -> "AM"
+        Strings.TimePickerPM -> "PM"
+        Strings.TimePickerPeriodToggle -> "Select AM or PM"
+        Strings.TimePickerMinuteSelection -> "Select minutes"
+        Strings.TimePickerHourSelection -> "Select hour"
+        Strings.TimePickerHourSuffix -> "%1$ o\\'clock"
+        Strings.TimePickerMinuteSuffix -> "%1$ minutes"
+        Strings.TimePicker24HourSuffix -> "%1$ hours"
         else -> ""
     }
 }
+@Composable
+@ReadOnlyComposable
+internal actual fun getString(string: Strings, vararg formatArgs: Any): String =
+    String.format(getString(string), Locale.getDefault(), *formatArgs)
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/TimeFormat.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/TimeFormat.desktop.kt
new file mode 100644
index 0000000..6f365ff
--- /dev/null
+++ b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/TimeFormat.desktop.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+
+internal actual val is24HourFormat: Boolean
+    @Composable
+    @ReadOnlyComposable get() = false
\ No newline at end of file
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 6e0f585..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
@@ -32,6 +32,10 @@
 import com.intellij.psi.PsiMethod
 import org.jetbrains.uast.UCallExpression
 import java.util.EnumSet
+import org.jetbrains.kotlin.psi.KtCallExpression
+import org.jetbrains.kotlin.psi.KtLambdaExpression
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.toUElementOfType
 
 /**
  * [Detector] that checks `remember` calls to make sure they are not returning [Unit].
@@ -40,15 +44,42 @@
     override fun getApplicableMethodNames(): List<String> = listOf(Names.Runtime.Remember.shortName)
 
     override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
-        if (method.isInPackageName(Names.Runtime.PackageName)) {
-            if (node.getExpressionType().isVoidOrUnit) {
-                context.report(
-                    RememberReturnType,
-                    node,
-                    context.getNameLocation(node),
-                    "`remember` calls must not return `Unit`"
-                )
+        if (!method.isInPackageName(Names.Runtime.PackageName)) return
+        val callExpressionType = node.getExpressionType()
+        if (!callExpressionType.isVoidOrUnit) return
+
+        val sourcePsi = node.sourcePsi
+        val isReallyUnit = when {
+            node.typeArguments.singleOrNull()?.isVoidOrUnit == true -> {
+                // Call with an explicit type argument, e.g., remember<Unit> { 42 }
+                true
             }
+            sourcePsi is KtCallExpression -> {
+                // Even though the return type is Unit, we should double check if the type of
+                // 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
+        }
+        if (isReallyUnit) {
+            context.report(
+                RememberReturnType,
+                node,
+                context.getNameLocation(node),
+                "`remember` calls must not return `Unit`"
+            )
         }
     }
 
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 4fa7427..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,36 +145,98 @@
 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
             """
             )
     }
 
     @Test
+    fun returnsUnit_dueToTypeError() {
+        lint().files(
+            kotlin(
+                """
+                package androidx.compose.runtime.foo
+
+                import androidx.compose.runtime.Composable
+                import androidx.compose.runtime.remember
+
+                @Composable
+                fun Test() {
+                    val shouldBeError = remember { Unknown() }
+                    val stillError = remember {
+                        val local = Unknown()
+                        local
+                    }
+                    val shouldBeInt = remember { 42 }
+                    val stillInt = remember {
+                        val local = Unknown()
+                        42
+                    }
+                }
+                """
+            ),
+            Stubs.Composable,
+            Stubs.Remember
+        )
+            .run()
+            .expectClean()
+    }
+
+    @Test
     fun returnsValue_explicitUnitType() {
         lint().files(
             kotlin(
diff --git a/compose/runtime/runtime-livedata/build.gradle b/compose/runtime/runtime-livedata/build.gradle
index 8e02589..d9d8529 100644
--- a/compose/runtime/runtime-livedata/build.gradle
+++ b/compose/runtime/runtime-livedata/build.gradle
@@ -30,7 +30,7 @@
     implementation(libs.kotlinStdlib)
 
     api(project(":compose:runtime:runtime"))
-    api("androidx.lifecycle:lifecycle-livedata:2.2.0")
+    api(project(":lifecycle:lifecycle-livedata"))
     api(projectOrArtifact(":lifecycle:lifecycle-runtime"))
     implementation("androidx.compose.ui:ui:1.2.1")
 
diff --git a/compose/runtime/runtime-rxjava3/src/main/java/androidx/compose/runtime/rxjava3/RxJava3Adapter.kt b/compose/runtime/runtime-rxjava3/src/main/java/androidx/compose/runtime/rxjava3/RxJava3Adapter.kt
index 9994c9c..c4c31a5 100644
--- a/compose/runtime/runtime-rxjava3/src/main/java/androidx/compose/runtime/rxjava3/RxJava3Adapter.kt
+++ b/compose/runtime/runtime-rxjava3/src/main/java/androidx/compose/runtime/rxjava3/RxJava3Adapter.kt
@@ -45,7 +45,7 @@
  * @param initial The initial value for the returned [State] which will be asynchronously updated
  * with the real one once we receive it from the stream
  */
-@Suppress("UPPER_BOUND_VIOLATED_BASED_ON_JAVA_ANNOTATIONS")
+@Suppress("UPPER_BOUND_VIOLATED")
 @Composable
 fun <R, T : R> Observable<T>.subscribeAsState(initial: R): State<R> =
     asState(initial) { subscribe(it) }
@@ -66,7 +66,7 @@
  * @param initial The initial value for the returned [State] which will be asynchronously updated
  * with the real one once we receive it from the stream
  */
-@Suppress("UPPER_BOUND_VIOLATED_BASED_ON_JAVA_ANNOTATIONS")
+@Suppress("UPPER_BOUND_VIOLATED")
 @Composable
 fun <R, T : R> Flowable<T>.subscribeAsState(initial: R): State<R> =
     asState(initial) { subscribe(it) }
@@ -87,7 +87,7 @@
  * @param initial The initial value for the returned [State] which will be asynchronously updated
  * with the real one once we receive it from the stream
  */
-@Suppress("UPPER_BOUND_VIOLATED_BASED_ON_JAVA_ANNOTATIONS")
+@Suppress("UPPER_BOUND_VIOLATED")
 @Composable
 fun <R, T : R> Single<T>.subscribeAsState(initial: R): State<R> =
     asState(initial) { subscribe(it) }
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.ignore b/compose/runtime/runtime/api/current.ignore
index e6959b9..b2e1b75 100644
--- a/compose/runtime/runtime/api/current.ignore
+++ b/compose/runtime/runtime/api/current.ignore
@@ -5,3 +5,7 @@
     Added method androidx.compose.runtime.Composer.endToMarker(int)
 AddedAbstractMethod: androidx.compose.runtime.Composer#getCurrentMarker():
     Added method androidx.compose.runtime.Composer.getCurrentMarker()
+
+
+RemovedMethod: androidx.compose.runtime.collection.MutableVectorKt#mutableVectorOf(T):
+    Removed method androidx.compose.runtime.collection.MutableVectorKt.mutableVectorOf(T)
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index cd22d03..71912fe 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -97,6 +97,12 @@
   @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ComposeCompilerApi {
   }
 
+  public interface ComposeNodeLifecycleCallback {
+    method public void onDeactivate();
+    method public void onRelease();
+    method public void onReuse();
+  }
+
   public sealed interface Composer {
     method @androidx.compose.runtime.ComposeCompilerApi public <V, T> void apply(V? value, kotlin.jvm.functions.Function2<? super T,? super V,kotlin.Unit> block);
     method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(Object? value);
@@ -113,7 +119,7 @@
     method @androidx.compose.runtime.ComposeCompilerApi public <T> void createNode(kotlin.jvm.functions.Function0<? extends T> factory);
     method @androidx.compose.runtime.ComposeCompilerApi public void deactivateToEndGroup(boolean changed);
     method @androidx.compose.runtime.ComposeCompilerApi public void disableReusing();
-    method public void disableSourceInformation();
+    method @org.jetbrains.annotations.TestOnly public void disableSourceInformation();
     method @androidx.compose.runtime.ComposeCompilerApi public void enableReusing();
     method @androidx.compose.runtime.ComposeCompilerApi public void endDefaults();
     method @androidx.compose.runtime.ComposeCompilerApi public void endMovableGroup();
@@ -576,6 +582,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);
@@ -647,7 +656,7 @@
     method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T>! MutableVector(optional int capacity);
     method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> MutableVector(int size, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends T> init);
     method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T>! mutableVectorOf();
-    method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> mutableVectorOf(T? elements);
+    method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> mutableVectorOf(T?... elements);
   }
 
 }
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index 9e24b69..291f5f9 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -102,6 +102,12 @@
   @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ComposeCompilerApi {
   }
 
+  public interface ComposeNodeLifecycleCallback {
+    method public void onDeactivate();
+    method public void onRelease();
+    method public void onReuse();
+  }
+
   public sealed interface Composer {
     method @androidx.compose.runtime.ComposeCompilerApi public <V, T> void apply(V? value, kotlin.jvm.functions.Function2<? super T,? super V,kotlin.Unit> block);
     method @androidx.compose.runtime.InternalComposeApi public androidx.compose.runtime.CompositionContext buildContext();
@@ -120,7 +126,7 @@
     method @androidx.compose.runtime.ComposeCompilerApi public <T> void createNode(kotlin.jvm.functions.Function0<? extends T> factory);
     method @androidx.compose.runtime.ComposeCompilerApi public void deactivateToEndGroup(boolean changed);
     method @androidx.compose.runtime.ComposeCompilerApi public void disableReusing();
-    method public void disableSourceInformation();
+    method @org.jetbrains.annotations.TestOnly public void disableSourceInformation();
     method @androidx.compose.runtime.ComposeCompilerApi public void enableReusing();
     method @androidx.compose.runtime.ComposeCompilerApi public void endDefaults();
     method @androidx.compose.runtime.ComposeCompilerApi public void endMovableGroup();
@@ -625,6 +631,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);
@@ -696,7 +705,7 @@
     method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T>! MutableVector(optional int capacity);
     method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> MutableVector(int size, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends T> init);
     method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T>! mutableVectorOf();
-    method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> mutableVectorOf(T? elements);
+    method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> mutableVectorOf(T?... elements);
   }
 
 }
diff --git a/compose/runtime/runtime/api/restricted_current.ignore b/compose/runtime/runtime/api/restricted_current.ignore
index e6959b9..b2e1b75 100644
--- a/compose/runtime/runtime/api/restricted_current.ignore
+++ b/compose/runtime/runtime/api/restricted_current.ignore
@@ -5,3 +5,7 @@
     Added method androidx.compose.runtime.Composer.endToMarker(int)
 AddedAbstractMethod: androidx.compose.runtime.Composer#getCurrentMarker():
     Added method androidx.compose.runtime.Composer.getCurrentMarker()
+
+
+RemovedMethod: androidx.compose.runtime.collection.MutableVectorKt#mutableVectorOf(T):
+    Removed method androidx.compose.runtime.collection.MutableVectorKt.mutableVectorOf(T)
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 40275f6..f0329cb 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -99,6 +99,12 @@
   @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ComposeCompilerApi {
   }
 
+  public interface ComposeNodeLifecycleCallback {
+    method public void onDeactivate();
+    method public void onRelease();
+    method public void onReuse();
+  }
+
   public sealed interface Composer {
     method @androidx.compose.runtime.ComposeCompilerApi public <V, T> void apply(V? value, kotlin.jvm.functions.Function2<? super T,? super V,kotlin.Unit> block);
     method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(Object? value);
@@ -115,7 +121,7 @@
     method @androidx.compose.runtime.ComposeCompilerApi public <T> void createNode(kotlin.jvm.functions.Function0<? extends T> factory);
     method @androidx.compose.runtime.ComposeCompilerApi public void deactivateToEndGroup(boolean changed);
     method @androidx.compose.runtime.ComposeCompilerApi public void disableReusing();
-    method public void disableSourceInformation();
+    method @org.jetbrains.annotations.TestOnly public void disableSourceInformation();
     method @androidx.compose.runtime.ComposeCompilerApi public void enableReusing();
     method @androidx.compose.runtime.ComposeCompilerApi public void endDefaults();
     method @androidx.compose.runtime.ComposeCompilerApi public void endMovableGroup();
@@ -603,6 +609,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);
@@ -676,7 +685,7 @@
     method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T>! MutableVector(optional int capacity);
     method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> MutableVector(int size, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends T> init);
     method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T>! mutableVectorOf();
-    method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> mutableVectorOf(T? elements);
+    method public static inline <reified T> androidx.compose.runtime.collection.MutableVector<T> mutableVectorOf(T?... elements);
   }
 
 }
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/Composables.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composables.kt
index ce6ea33..dd3b5c1 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composables.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composables.kt
@@ -290,9 +290,7 @@
     } else {
         currentComposer.useNode()
     }
-    currentComposer.disableReusing()
     Updater<T>(currentComposer).update()
-    currentComposer.enableReusing()
     currentComposer.endNode()
 }
 
@@ -371,9 +369,7 @@
     } else {
         currentComposer.useNode()
     }
-    currentComposer.disableReusing()
     Updater<T>(currentComposer).update()
-    currentComposer.enableReusing()
     content()
     currentComposer.endNode()
 }
@@ -464,9 +460,7 @@
     } else {
         currentComposer.useNode()
     }
-    currentComposer.disableReusing()
     Updater<T>(currentComposer).update()
-    currentComposer.enableReusing()
     SkippableUpdater<T>(currentComposer).skippableUpdate()
     currentComposer.startReplaceableGroup(0x7ab4aae9)
     content()
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeNodeLifecycleCallback.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeNodeLifecycleCallback.kt
new file mode 100644
index 0000000..55377e1
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeNodeLifecycleCallback.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
+
+/**
+ * Observes lifecycle of the node emitted with [ReusableComposeNode] or [ComposeNode] inside
+ * [ReusableContentHost] and [ReusableContent].
+ *
+ * The [ReusableContentHost] introduces the concept of reusing (or recycling) nodes, as well as
+ * deactivating parts of composition, while keeping the nodes around to reuse common structures
+ * in the next iteration. In this state, [RememberObserver] is not sufficient to track lifetime
+ * of data associated with reused node, as deactivated or reused parts of composition is disposed.
+ *
+ * These callbacks track intermediate states of the node in reusable groups for managing
+ * data contained inside reusable nodes or associated with them (e.g. subcomposition).
+ *
+ * Important: the runtime only supports node implementation of this interface.
+ */
+interface ComposeNodeLifecycleCallback {
+    /**
+     * Invoked when the node was reused in the composition.
+     * Consumers might use this callback to reset data associated with the previous content, as
+     * it is no longer valid.
+     */
+    fun onReuse()
+
+    /**
+     * Invoked when the group containing the node was deactivated.
+     * This happens when the content of [ReusableContentHost] is deactivated.
+     *
+     * The node will not be reused in this recompose cycle, but might be reused or released in
+     * the future. Consumers might use this callback to release expensive resources or stop
+     * continuous process that was dependent on the node being used in composition.
+     *
+     * If the node is reused, [onReuse] will be called again to prepare the node for reuse.
+     * Similarly, [onRelease] will indicate that deactivated node will never be reused again.
+     */
+    fun onDeactivate()
+
+    /**
+     * Invoked when the node exits the composition entirely and won't be reused again.
+     * All intermediate data related to the node can be safely disposed.
+     */
+    fun onRelease()
+}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
index be676a1..8e676fb 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
@@ -28,5 +28,5 @@
      * IMPORTANT: Whenever updating this value, please make sure to also update `versionTable` and
      * `minimumRuntimeVersionInt` in `VersionChecker.kt` of the compiler.
      */
-    const val version: Int = 9300
+    const val version: Int = 9400
 }
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..f0f620f 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
@@ -78,6 +79,16 @@
      * notifications are sent.
      */
     fun sideEffect(effect: () -> Unit)
+
+    /**
+     * The [ComposeNodeLifecycleCallback] is being deactivated.
+     */
+    fun deactivating(instance: ComposeNodeLifecycleCallback)
+
+    /**
+     * The [ComposeNodeLifecycleCallback] is being released.
+     */
+    fun releasing(instance: ComposeNodeLifecycleCallback)
 }
 
 /**
@@ -1058,6 +1069,15 @@
     val composition: ControlledComposition
         @TestOnly get
 
+    /**
+     * Disable the collection of source information, that may introduce groups to store the source
+     * information, in order to be able to more accurately calculate the actual number of groups a
+     * composable function generates in a release build.
+     *
+     * This function is only safe to call in a test and will produce incorrect composition results
+     * if called on a composer not under test.
+     */
+    @TestOnly
     fun disableSourceInformation()
 
     companion object {
@@ -1248,7 +1268,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
@@ -1614,7 +1634,14 @@
     override fun useNode() {
         validateNodeExpected()
         runtimeCheck(!inserting) { "useNode() called while inserting" }
-        recordDown(reader.node)
+        val node = reader.node
+        recordDown(node)
+
+        if (reusing && node is ComposeNodeLifecycleCallback) {
+            recordApplierOperation { applier, _, _ ->
+                (applier.current as ComposeNodeLifecycleCallback).onReuse()
+            }
+        }
     }
 
     /**
@@ -1885,12 +1912,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 +1936,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
@@ -2740,6 +2770,14 @@
             val start = reader.currentGroup
             val end = reader.currentEnd
             for (group in start until end) {
+                if (reader.isNode(group)) {
+                    val node = reader.node(group)
+                    if (node is ComposeNodeLifecycleCallback) {
+                        record { _, _, rememberManager ->
+                            rememberManager.deactivating(node)
+                        }
+                    }
+                }
                 reader.forEachData(group) { index, data ->
                     when (data) {
                         is RememberObserver -> {
@@ -4167,18 +4205,20 @@
 
     // To ensure this order, we call `enters` as a pre-order traversal
     // of the group tree, and then call `leaves` in the inverse order.
-
     for (slot in groupSlots()) {
-        when (slot) {
-            is RememberObserver -> {
-                rememberManager.forgetting(slot)
-            }
-            is RecomposeScopeImpl -> {
-                val composition = slot.composition
-                if (composition != null) {
-                    composition.pendingInvalidScopes = true
-                    slot.release()
-                }
+        // even that in the documentation we claim ComposeNodeLifecycleCallback should be only
+        // implemented on the nodes we do not really enforce it here as doing so will be expensive.
+        if (slot is ComposeNodeLifecycleCallback) {
+            rememberManager.releasing(slot)
+        }
+        if (slot is RememberObserver) {
+            rememberManager.forgetting(slot)
+        }
+        if (slot is RecomposeScopeImpl) {
+            val composition = slot.composition
+            if (composition != null) {
+                composition.pendingInvalidScopes = true
+                slot.release()
             }
         }
     }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index 0f27069..165ab21 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -622,6 +622,7 @@
                         }
                         applier.clear()
                         manager.dispatchRememberObservers()
+                        manager.dispatchNodeCallbacks()
                     }
                     manager.dispatchAbandons()
                 }
@@ -792,6 +793,7 @@
             writer.removeCurrentGroup(manager)
         }
         manager.dispatchRememberObservers()
+        manager.dispatchNodeCallbacks()
     }
 
     private fun applyChangesInLocked(changes: MutableList<Change>) {
@@ -816,6 +818,7 @@
             // that implement RememberObserver receive onRemembered before a side effect
             // that captured it and operates on it can run.
             manager.dispatchRememberObservers()
+            manager.dispatchNodeCallbacks()
             manager.dispatchSideEffects()
 
             if (pendingInvalidScopes) {
@@ -1033,9 +1036,6 @@
 
     /**
      * Helper for collecting remember observers for later strictly ordered dispatch.
-     *
-     * This includes support for the deprecated [RememberObserver] which should be
-     * removed with it.
      */
     private class RememberEventDispatcher(
         private val abandoning: MutableSet<RememberObserver>
@@ -1043,6 +1043,8 @@
         private val remembering = mutableListOf<RememberObserver>()
         private val forgetting = mutableListOf<RememberObserver>()
         private val sideEffects = mutableListOf<() -> Unit>()
+        private var deactivating: MutableList<ComposeNodeLifecycleCallback>? = null
+        private var releasing: MutableList<ComposeNodeLifecycleCallback>? = null
 
         override fun remembering(instance: RememberObserver) {
             forgetting.lastIndexOf(instance).let { index ->
@@ -1070,6 +1072,18 @@
             sideEffects += effect
         }
 
+        override fun deactivating(instance: ComposeNodeLifecycleCallback) {
+            (deactivating ?: mutableListOf<ComposeNodeLifecycleCallback>().also {
+                deactivating = it
+            }) += instance
+        }
+
+        override fun releasing(instance: ComposeNodeLifecycleCallback) {
+            (releasing ?: mutableListOf<ComposeNodeLifecycleCallback>().also {
+                releasing = it
+            }) += instance
+        }
+
         fun dispatchRememberObservers() {
             // Send forgets
             if (forgetting.isNotEmpty()) {
@@ -1117,6 +1131,32 @@
                 }
             }
         }
+
+        fun dispatchNodeCallbacks() {
+            val deactivating = deactivating
+            if (!deactivating.isNullOrEmpty()) {
+                trace("Compose:deactivations") {
+                    for (i in deactivating.size - 1 downTo 0) {
+                        val instance = deactivating[i]
+                        instance.onDeactivate()
+                    }
+                }
+                deactivating.clear()
+            }
+
+            val releasing = releasing
+            if (!releasing.isNullOrEmpty()) {
+                // note that in contrast with objects from `forgetting` we will invoke the callback
+                // even for objects being abandoned.
+                trace("Compose:releases") {
+                    for (i in releasing.size - 1 downTo 0) {
+                        val instance = releasing[i]
+                        instance.onRelease()
+                    }
+                }
+                releasing.clear()
+            }
+        }
     }
 }
 
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index 79d6d48..62189bc 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -32,7 +32,6 @@
 import kotlin.coroutines.EmptyCoroutineContext
 import kotlin.coroutines.coroutineContext
 import kotlin.coroutines.resume
-import kotlin.native.concurrent.ThreadLocal
 import kotlinx.coroutines.CancellableContinuation
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
@@ -1243,7 +1242,8 @@
      *
      * This annotation WILL BE REMOVED with the new memory model of Kotlin/Native.
      */
-    @ThreadLocal
+    @Suppress("OPTIONAL_DECLARATION_USAGE_IN_NON_COMMON_SOURCE")
+    @kotlin.native.concurrent.ThreadLocal
     companion object {
 
         private val _runningRecomposers = MutableStateFlow(persistentSetOf<RecomposerInfoImpl>())
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/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapContentIterators.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapContentIterators.kt
index b10deaf..d5619fd 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapContentIterators.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapContentIterators.kt
@@ -6,7 +6,6 @@
 package androidx.compose.runtime.external.kotlinx.collections.immutable.implementations.immutableMap
 
 import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.assert
-import kotlin.js.JsName
 
 internal const val TRIE_MAX_HEIGHT = 7
 
@@ -104,7 +103,8 @@
 ) : Iterator<T> {
 
     protected var pathLastIndex = 0
-    @JsName("_hasNext")
+    @Suppress("OPTIONAL_DECLARATION_USAGE_IN_NON_COMMON_SOURCE")
+    @kotlin.js.JsName("_hasNext")
     private var hasNext = true
 
     init {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableSet/PersistentHashSetIterator.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableSet/PersistentHashSetIterator.kt
index 8810019..b9738ef 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableSet/PersistentHashSetIterator.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableSet/PersistentHashSetIterator.kt
@@ -6,12 +6,12 @@
 package androidx.compose.runtime.external.kotlinx.collections.immutable.implementations.immutableSet
 
 import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.assert
-import kotlin.js.JsName
 
 internal open class PersistentHashSetIterator<E>(node: TrieNode<E>) : Iterator<E> {
     protected val path = mutableListOf(TrieNodeIterator<E>())
     protected var pathLastIndex = 0
-    @JsName("_hasNext")
+    @Suppress("OPTIONAL_DECLARATION_USAGE_IN_NON_COMMON_SOURCE")
+    @kotlin.js.JsName("_hasNext")
     private var hasNext = true
 
     init {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index 25e87d6..94957ba 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -743,13 +743,13 @@
         applied = true
 
         // Notify any apply observers that changes applied were seen
-        if (globalModified != null && globalModified.isNotEmpty()) {
+        if (!globalModified.isNullOrEmpty()) {
             observers.fastForEach {
                 it(globalModified, this)
             }
         }
 
-        if (modified != null && modified.isNotEmpty()) {
+        if (!modified.isNullOrEmpty()) {
             observers.fastForEach {
                 it(modified, this)
             }
@@ -760,6 +760,9 @@
         // before unpinning records that need to be retained in this case.
         sync {
             releasePinnedSnapshotsForCloseLocked()
+
+            globalModified?.forEach(::overwriteUnusedRecordsLocked)
+            modified?.forEach(::overwriteUnusedRecordsLocked)
         }
 
         return SnapshotApplyResult.Success
@@ -1117,9 +1120,9 @@
      * records that are already in the list cannot be moved in the list as this the change must
      * be atomic to all threads that cannot happen without a lock which this list cannot afford.
      *
-     * It is unsafe to remove a record as it might be in the process of being reused (see [used]).
+     * It is unsafe to remove a record as it might be in the process of being reused (see [usedLocked]).
      * If a record is removed care must be taken to ensure that it is not being claimed by some
-     * other thread. This would require changes to [used].
+     * other thread. This would require changes to [usedLocked].
      */
     internal var next: StateRecord? = null
 
@@ -1766,6 +1769,10 @@
         }
     }
 
+    sync {
+        modified?.forEach(::overwriteUnusedRecordsLocked)
+    }
+
     return result
 }
 
@@ -1838,8 +1845,9 @@
         // or will find a valid record. Being in a sync block prevents other threads from writing
         // to this state object until the read completes.
         val syncSnapshot = Snapshot.current
-        readable(this, syncSnapshot.id, syncSnapshot.invalid)
-    } ?: readError()
+        @Suppress("UNCHECKED_CAST")
+        readable(state.firstStateRecord as T, syncSnapshot.id, syncSnapshot.invalid) ?: readError()
+    }
 }
 
 /**
@@ -1864,7 +1872,7 @@
  * record created in an abandoned snapshot. It is also true if the record is valid in the
  * previous snapshot and is obscured by another record also valid in the previous state record.
  */
-private fun used(state: StateObject): StateRecord? {
+private fun usedLocked(state: StateObject): StateRecord? {
     var current: StateRecord? = state.firstStateRecord
     var validRecord: StateRecord? = null
     val reuseLimit = pinningTable.lowestOrDefault(nextSnapshotId) - 1
@@ -1872,8 +1880,8 @@
     while (current != null) {
         val currentId = current.snapshotId
         if (currentId == INVALID_SNAPSHOT) {
-            // Any records that were marked invalid by an abandoned snapshot can be used
-            // immediately.
+            // Any records that were marked invalid by an abandoned snapshot or is marked reachable
+            // can be used immediately.
             return current
         }
         if (valid(current, reuseLimit, invalid)) {
@@ -1890,6 +1898,56 @@
     return null
 }
 
+/**
+ * Clear records that cannot be selected in any currently open snapshot.
+ *
+ * This method uses the same technique as [usedLocked] which uses the [pinningTable] to
+ * determine lowest id in the invalid set for all snapshots. Only the record with the greatest
+ * id of all records less or equal to this lowest id can possibly be selected in any snapshot
+ * and all other records below that number can be overwritten.
+ *
+ * However, this technique doesn't find all records that will not be selected by any open snapshot
+ * as a record that has an id above that number could be reusable but will not be found.
+ *
+ * For example if snapshot 1 is open and 2 is created and modifies [state] then is applied, 3 is
+ * open and then 4 is open, and then 1 is applied. When 3 modifies [state] and then applies, as 1 is
+ * pinned by 4, it is uncertain whether the record for 2 is needed by 4 so it must be kept even if 4
+ * also modified [state] and would not select 2. Accurately determine if a record is selectable
+ * would require keeping a list of all open [Snapshot] instances which currently is not kept and
+ * would require keeping a list of all open [Snapshot] instances which currently is not kept and
+ * traversing that list for each record.
+ *
+ * If any such records are possible this method returns true. In other words, this method returns
+ * true if any records might be reusable but this function could not prove there were or not.
+ */
+private fun overwriteUnusedRecordsLocked(state: StateObject): Boolean {
+    var current: StateRecord? = state.firstStateRecord
+    var validRecord: StateRecord? = null
+    val reuseLimit = pinningTable.lowestOrDefault(nextSnapshotId) - 1
+    var uncertainRecords = 0
+    while (current != null) {
+        val currentId = current.snapshotId
+        if (currentId != INVALID_SNAPSHOT) {
+            if (currentId <= reuseLimit) {
+                if (validRecord == null) {
+                    validRecord = current
+                } else {
+                    val recordToOverwrite = if (current.snapshotId < validRecord.snapshotId) {
+                        current
+                    } else {
+                        validRecord.also { validRecord = current }
+                    }
+                    recordToOverwrite.snapshotId = INVALID_SNAPSHOT
+                    validRecord?.let { recordToOverwrite.assign(it) }
+                }
+            } else uncertainRecords++
+        }
+        current = current.next
+    }
+
+    return uncertainRecords < 1
+}
+
 @PublishedApi
 internal fun <T : StateRecord> T.writableRecord(state: StateObject, snapshot: Snapshot): T {
     if (snapshot.readOnly) {
@@ -1924,7 +1982,7 @@
 
     if (candidate.snapshotId == id) return candidate
 
-    val newData = newOverwritableRecord(state)
+    val newData = sync { newOverwritableRecordLocked(state) }
     newData.snapshotId = id
 
     snapshot.recordModified(state)
@@ -1932,7 +1990,10 @@
     return newData
 }
 
-internal fun <T : StateRecord> T.newWritableRecord(state: StateObject, snapshot: Snapshot): T {
+internal fun <T : StateRecord> T.newWritableRecord(state: StateObject, snapshot: Snapshot) =
+    sync { newWritableRecordLocked(state, snapshot) }
+
+private fun <T : StateRecord> T.newWritableRecordLocked(state: StateObject, snapshot: Snapshot): T {
     // Calling used() on a state object might return the same record for each thread calling
     // used() therefore selecting the record to reuse should be guarded.
 
@@ -1945,13 +2006,13 @@
     // cache the result of readable() as the mutating thread calls to writable() can change the
     // result of readable().
     @Suppress("UNCHECKED_CAST")
-    val newData = newOverwritableRecord(state)
+    val newData = newOverwritableRecordLocked(state)
     newData.assign(this)
     newData.snapshotId = snapshot.id
     return newData
 }
 
-internal fun <T : StateRecord> T.newOverwritableRecord(state: StateObject): T {
+internal fun <T : StateRecord> T.newOverwritableRecordLocked(state: StateObject): T {
     // Calling used() on a state object might return the same record for each thread calling
     // used() therefore selecting the record to reuse should be guarded.
 
@@ -1964,7 +2025,7 @@
     // cache the result of readable() as the mutating thread calls to writable() can change the
     // result of readable().
     @Suppress("UNCHECKED_CAST")
-    return (used(state) as T?)?.apply {
+    return (usedLocked(state) as T?)?.apply {
         snapshotId = Int.MAX_VALUE
     } ?: create().apply {
         snapshotId = Int.MAX_VALUE
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionReusingTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionReusingTests.kt
index 69e4e27..625b369 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionReusingTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionReusingTests.kt
@@ -346,6 +346,349 @@
         expectChanges()
         revalidate()
     }
+
+    @Test
+    fun onReuseIsCalledWhenReusableContentKeyChanges() = compositionTest {
+        var reuseKey by mutableStateOf(0)
+        var onReuseCalls = 0
+        val onReuse: () -> Unit = {
+            onReuseCalls++
+        }
+
+        compose {
+            ReusableContent(reuseKey) {
+                Linear(onReuse = onReuse) { }
+            }
+        }
+
+        validate {
+            Linear {
+            }
+        }
+
+        assertEquals(0, onReuseCalls)
+
+        reuseKey++
+        expectChanges()
+        revalidate()
+
+        assertEquals(1, onReuseCalls)
+
+        reuseKey++
+        expectChanges()
+        revalidate()
+
+        assertEquals(2, onReuseCalls)
+    }
+
+    @Test
+    fun onReuseIsCalledBeforeSetter() = compositionTest {
+        var reuseKey by mutableStateOf(0)
+        var onReuseCalls = 0
+        val onReuseCallsWhenSetCalled = mutableListOf<Int>()
+        val onReuse: () -> Unit = {
+            onReuseCalls++
+        }
+        val onSet: () -> Unit = {
+            onReuseCallsWhenSetCalled.add(onReuseCalls)
+        }
+
+        compose {
+            ReusableContent(reuseKey) {
+                Linear(onReuse = onReuse, onSet = onSet) { }
+            }
+        }
+
+        validate {
+            Linear {
+            }
+        }
+
+        assertEquals(listOf(0), onReuseCallsWhenSetCalled)
+        onReuseCallsWhenSetCalled.clear()
+
+        reuseKey++
+        expectChanges()
+        revalidate()
+
+        assertEquals(listOf(1), onReuseCallsWhenSetCalled)
+    }
+
+    @Test
+    fun onReuseIsCalledInApplyStage() = compositionTest {
+        var reuseKey by mutableStateOf(0)
+        var compositionFinished = false
+        val onReuseCalls = mutableListOf<Boolean>()
+        val onReuse: () -> Unit = {
+            onReuseCalls.add(compositionFinished)
+        }
+
+        compose {
+            ReusableContent(reuseKey) {
+                Linear(onReuse = onReuse) { }
+            }
+            compositionFinished = true
+        }
+
+        validate {
+            Linear {
+            }
+        }
+
+        assertEquals(emptyList(), onReuseCalls)
+        compositionFinished = false
+
+        reuseKey++
+        expectChanges()
+        revalidate()
+
+        assertEquals(listOf(true), onReuseCalls)
+    }
+
+    @Test
+    fun onDeactivateIsCalledWhenReusableContentDeactivated() = compositionTest {
+        var active by mutableStateOf(true)
+        var onDeactivateCalls = 0
+        val onDeactivate: () -> Unit = {
+            onDeactivateCalls++
+        }
+
+        compose {
+            ReusableContentHost(active) {
+                ReusableContent(0) {
+                    Linear(onDeactivate = onDeactivate) { }
+                }
+            }
+        }
+
+        validate {
+            Linear {
+            }
+        }
+
+        assertEquals(0, onDeactivateCalls)
+
+        active = false
+        expectChanges()
+        revalidate()
+
+        assertEquals(1, onDeactivateCalls)
+
+        active = true
+        expectChanges()
+        revalidate()
+
+        assertEquals(1, onDeactivateCalls)
+    }
+
+    @Test
+    fun onReuseIsCalledBeforeSetterAfterDeactivation() = compositionTest {
+        var active by mutableStateOf(true)
+        var onReuseCalls = 0
+        val onReuseCallsWhenSetCalled = mutableListOf<Int>()
+        val onReuse: () -> Unit = {
+            onReuseCalls++
+        }
+        val onSet: () -> Unit = {
+            onReuseCallsWhenSetCalled.add(onReuseCalls)
+        }
+
+        compose {
+            ReusableContentHost(active) {
+                ReusableContent(0) {
+                    Linear(onReuse = onReuse, onSet = onSet) { }
+                }
+            }
+        }
+
+        validate {
+            Linear {
+            }
+        }
+
+        active = false
+
+        expectChanges()
+        revalidate()
+
+        active = true
+
+        expectChanges()
+        revalidate()
+
+        assertEquals(listOf(0, 1), onReuseCallsWhenSetCalled)
+    }
+
+    @Test
+    fun onReuseIsNotCalledWhenDisposed() = compositionTest {
+        var emit by mutableStateOf(true)
+        var onReuseCalls = 0
+        val onReuse: () -> Unit = {
+            onReuseCalls++
+        }
+
+        compose {
+            if (emit) {
+                ReusableContent(0) {
+                    Linear(onReuse = onReuse) { }
+                }
+            }
+        }
+
+        emit = false
+        expectChanges()
+
+        assertEquals(0, onReuseCalls)
+    }
+
+    @Test
+    fun onDeactivateIsCalledInApplyStage() = compositionTest {
+        var active by mutableStateOf(true)
+        var compositionFinished = false
+        val onDeactivateCalls = mutableListOf<Boolean>()
+        val onDeactivate: () -> Unit = {
+            onDeactivateCalls.add(compositionFinished)
+        }
+
+        compose {
+            ReusableContentHost(active) {
+                ReusableContent(0) {
+                    Linear(onDeactivate = onDeactivate) { }
+                }
+            }
+            if (!active) {
+                compositionFinished = true
+            }
+        }
+
+        active = false
+        expectChanges()
+
+        assertEquals(listOf(true), onDeactivateCalls)
+    }
+
+    @Test
+    fun onReleaseIsCalledWhenNodeIsRemoved() = compositionTest {
+        var emit by mutableStateOf(true)
+        var onReleaseCalls = 0
+        val onRelease: () -> Unit = {
+            onReleaseCalls++
+        }
+
+        compose {
+            if (emit) {
+                ReusableContent(0) {
+                    Linear(onRelease = onRelease) { }
+                }
+            }
+        }
+
+        emit = false
+        expectChanges()
+
+        assertEquals(1, onReleaseCalls)
+    }
+
+    @Test
+    fun onReleaseIsNotCalledOnReuse() = compositionTest {
+        var key by mutableStateOf(0)
+        var onReleaseCalls = 0
+        val onRelease: () -> Unit = {
+            onReleaseCalls++
+        }
+
+        compose {
+            ReusableContent(key) {
+                Linear(onRelease = onRelease) { }
+            }
+        }
+
+        key++
+        expectChanges()
+
+        assertEquals(0, onReleaseCalls)
+    }
+
+    @Test
+    fun onReleaseIsNotCalledWithReusableContentHost() = compositionTest {
+        var active by mutableStateOf(true)
+        var emit by mutableStateOf(true)
+        var onReleaseCalls = 0
+        val onRelease: () -> Unit = {
+            onReleaseCalls++
+        }
+
+        compose {
+            if (emit) {
+                ReusableContentHost(active) {
+                    Linear(onRelease = onRelease) { }
+                }
+            }
+        }
+
+        active = false
+        expectChanges()
+
+        assertEquals(0, onReleaseCalls)
+
+        emit = false
+        expectChanges()
+
+        assertEquals(1, onReleaseCalls)
+    }
+
+    @Test
+    fun onReleaseIsNotCalledWithMovableContentMovement() = compositionTest {
+        var wrap by mutableStateOf(true)
+        var onReleaseCalls = 0
+        val onRelease: () -> Unit = {
+            onReleaseCalls++
+        }
+
+        val movableContent = movableContentOf {
+            Linear(onRelease = onRelease) { }
+        }
+
+        compose {
+            if (wrap) {
+                ReusableContent(0) {
+                    movableContent()
+                }
+            } else {
+                movableContent()
+            }
+        }
+
+        wrap = false
+        expectChanges()
+
+        assertEquals(0, onReleaseCalls)
+    }
+
+    @Test
+    fun onReleaseIsCalledInApplyStage() = compositionTest {
+        var emit by mutableStateOf(true)
+        var compositionFinished = false
+        val onReleaseCalls = mutableListOf<Boolean>()
+        val onRelease: () -> Unit = {
+            onReleaseCalls.add(compositionFinished)
+        }
+
+        compose {
+            if (emit) {
+                ReusableContent(0) {
+                    Linear(onRelease = onRelease) { }
+                }
+            } else {
+                compositionFinished = true
+            }
+        }
+
+        emit = false
+        expectChanges()
+
+        assertEquals(listOf(true), onReleaseCalls)
+    }
 }
 
 private fun View.findTextWith(contains: String) =
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/mock/Views.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/mock/Views.kt
index 5f90678..1c332b0 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/mock/Views.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/mock/Views.kt
@@ -20,7 +20,10 @@
 import androidx.compose.runtime.ComposeNode
 import androidx.compose.runtime.ReusableComposeNode
 import androidx.compose.runtime.NonRestartableComposable
+import androidx.compose.runtime.ComposeNodeLifecycleCallback
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.key
+import androidx.compose.runtime.rememberUpdatedState
 
 @Composable
 fun <T : Any> Repeated(
@@ -45,6 +48,45 @@
 }
 
 @Composable
+fun Linear(
+    onReuse: () -> Unit = {},
+    onDeactivate: () -> Unit = {},
+    onRelease: () -> Unit = {},
+    onSet: () -> Unit = {},
+    content: @Composable () -> Unit
+) {
+    val currentOnReuse by rememberUpdatedState(onReuse)
+    val currentOnDeactivate by rememberUpdatedState(onDeactivate)
+    val currentOnRelease by rememberUpdatedState(onRelease)
+    ReusableComposeNode<View, ViewApplier>(
+        factory = {
+            object : View(), ComposeNodeLifecycleCallback {
+                init {
+                    name = "linear"
+                }
+
+                override fun onRelease() {
+                    currentOnRelease()
+                }
+
+                override fun onReuse() {
+                    currentOnReuse()
+                }
+
+                override fun onDeactivate() {
+                    currentOnDeactivate()
+                }
+            }
+        },
+        update = {
+            set(onSet) { onSet() }
+        },
+    ) {
+        content()
+    }
+}
+
+@Composable
 fun NonReusableLinear(content: @Composable () -> Unit) {
     ComposeNode<View, ViewApplier>(
         factory = { View().also { it.name = "linear" } },
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/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTestsJvm.kt b/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTestsJvm.kt
index 307a8eb..da4987d 100644
--- a/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTestsJvm.kt
+++ b/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTestsJvm.kt
@@ -72,6 +72,7 @@
             running.set(false)
         }
 
+        exception.get()?.let { throw it }
         assertNull(exception.get())
     }
 }
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/api/current.ignore b/compose/ui/ui-graphics/api/current.ignore
new file mode 100644
index 0000000..ac8aa37
--- /dev/null
+++ b/compose/ui/ui-graphics/api/current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.ui.graphics.PathMeasure#getPosition(float):
+    Added method androidx.compose.ui.graphics.PathMeasure.getPosition(float)
+AddedAbstractMethod: androidx.compose.ui.graphics.PathMeasure#getTangent(float):
+    Added method androidx.compose.ui.graphics.PathMeasure.getTangent(float)
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index f1c4192..358b6e6 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -115,7 +115,9 @@
 
   public final class AndroidPathMeasure implements androidx.compose.ui.graphics.PathMeasure {
     method public float getLength();
+    method public long getPosition(float distance);
     method public boolean getSegment(float startDistance, float stopDistance, androidx.compose.ui.graphics.Path destination, boolean startWithMoveTo);
+    method public long getTangent(float distance);
     method public void setPath(androidx.compose.ui.graphics.Path? path, boolean forceClosed);
     property public float length;
   }
@@ -666,7 +668,9 @@
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface PathMeasure {
     method public float getLength();
+    method public long getPosition(float distance);
     method public boolean getSegment(float startDistance, float stopDistance, androidx.compose.ui.graphics.Path destination, optional boolean startWithMoveTo);
+    method public long getTangent(float distance);
     method public void setPath(androidx.compose.ui.graphics.Path? path, boolean forceClosed);
     property public abstract float length;
   }
@@ -730,8 +734,10 @@
   }
 
   public final class RectHelper_androidKt {
-    method public static android.graphics.Rect toAndroidRect(androidx.compose.ui.geometry.Rect);
+    method @Deprecated 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 90bb660..39ec0533 100644
--- a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
@@ -115,7 +115,9 @@
 
   public final class AndroidPathMeasure implements androidx.compose.ui.graphics.PathMeasure {
     method public float getLength();
+    method public long getPosition(float distance);
     method public boolean getSegment(float startDistance, float stopDistance, androidx.compose.ui.graphics.Path destination, boolean startWithMoveTo);
+    method public long getTangent(float distance);
     method public void setPath(androidx.compose.ui.graphics.Path? path, boolean forceClosed);
     property public float length;
   }
@@ -669,7 +671,9 @@
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface PathMeasure {
     method public float getLength();
+    method public long getPosition(float distance);
     method public boolean getSegment(float startDistance, float stopDistance, androidx.compose.ui.graphics.Path destination, optional boolean startWithMoveTo);
+    method public long getTangent(float distance);
     method public void setPath(androidx.compose.ui.graphics.Path? path, boolean forceClosed);
     property public abstract float length;
   }
@@ -733,8 +737,10 @@
   }
 
   public final class RectHelper_androidKt {
-    method public static android.graphics.Rect toAndroidRect(androidx.compose.ui.geometry.Rect);
+    method @Deprecated 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.ignore b/compose/ui/ui-graphics/api/restricted_current.ignore
new file mode 100644
index 0000000..ac8aa37
--- /dev/null
+++ b/compose/ui/ui-graphics/api/restricted_current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.ui.graphics.PathMeasure#getPosition(float):
+    Added method androidx.compose.ui.graphics.PathMeasure.getPosition(float)
+AddedAbstractMethod: androidx.compose.ui.graphics.PathMeasure#getTangent(float):
+    Added method androidx.compose.ui.graphics.PathMeasure.getTangent(float)
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index adad1a5..5dbda20 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -145,7 +145,9 @@
 
   public final class AndroidPathMeasure implements androidx.compose.ui.graphics.PathMeasure {
     method public float getLength();
+    method public long getPosition(float distance);
     method public boolean getSegment(float startDistance, float stopDistance, androidx.compose.ui.graphics.Path destination, boolean startWithMoveTo);
+    method public long getTangent(float distance);
     method public void setPath(androidx.compose.ui.graphics.Path? path, boolean forceClosed);
     property public float length;
   }
@@ -698,7 +700,9 @@
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface PathMeasure {
     method public float getLength();
+    method public long getPosition(float distance);
     method public boolean getSegment(float startDistance, float stopDistance, androidx.compose.ui.graphics.Path destination, optional boolean startWithMoveTo);
+    method public long getTangent(float distance);
     method public void setPath(androidx.compose.ui.graphics.Path? path, boolean forceClosed);
     property public abstract float length;
   }
@@ -762,8 +766,10 @@
   }
 
   public final class RectHelper_androidKt {
-    method public static android.graphics.Rect toAndroidRect(androidx.compose.ui.geometry.Rect);
+    method @Deprecated 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/PathMeasureTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathMeasureTest.kt
new file mode 100644
index 0000000..afacfe3
--- /dev/null
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathMeasureTest.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.graphics
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PathMeasureTest {
+
+    @Test
+    fun testGetPositionAndTangent() {
+        val width = 100f
+        val height = 100f
+        val path = Path().apply {
+            lineTo(width, height)
+        }
+        val pathMeasure = PathMeasure()
+
+        pathMeasure.setPath(path, false)
+        val distance = pathMeasure.length
+        val position = pathMeasure.getPosition(distance * 0.5f)
+
+        val tangent = pathMeasure.getTangent(distance * 0.5f)
+
+        Assert.assertEquals(50f, position.x)
+        Assert.assertEquals(50f, position.y)
+        Assert.assertEquals(0.707106f, tangent.x, 0.00001f)
+        Assert.assertEquals(0.707106f, tangent.y, 0.00001f)
+    }
+}
\ No newline at end of file
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..8c20469
--- /dev/null
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/RectHelperTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.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 {
+
+    @Suppress("DEPRECATION")
+    @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/AndroidPath.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
index 5e79774..0907e2c 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
@@ -133,12 +133,12 @@
 
     override fun addRect(rect: Rect) {
         check(_rectIsValid(rect))
-        rectF.set(rect.toAndroidRectF())
+        rectF.set(rect.left, rect.top, rect.right, rect.bottom)
         internalPath.addRect(rectF, android.graphics.Path.Direction.CCW)
     }
 
     override fun addOval(oval: Rect) {
-        rectF.set(oval.toAndroidRect())
+        rectF.set(oval.left, oval.top, oval.right, oval.bottom)
         internalPath.addOval(rectF, android.graphics.Path.Direction.CCW)
     }
 
@@ -148,7 +148,7 @@
 
     override fun addArc(oval: Rect, startAngleDegrees: Float, sweepAngleDegrees: Float) {
         check(_rectIsValid(oval))
-        rectF.set(oval.toAndroidRect())
+        rectF.set(oval.left, oval.top, oval.right, oval.bottom)
         internalPath.addArc(rectF, startAngleDegrees, sweepAngleDegrees)
     }
 
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathMeasure.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathMeasure.android.kt
index d9996fa..358b3af 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathMeasure.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathMeasure.android.kt
@@ -16,6 +16,8 @@
 
 package androidx.compose.ui.graphics
 
+import androidx.compose.ui.geometry.Offset
+
 actual fun PathMeasure(): PathMeasure = AndroidPathMeasure(android.graphics.PathMeasure())
 
 class AndroidPathMeasure internal constructor(
@@ -25,6 +27,10 @@
     override val length: Float
         get() = internalPathMeasure.length
 
+    private var positionArray: FloatArray? = null
+
+    private var tangentArray: FloatArray? = null
+
     override fun getSegment(
         startDistance: Float,
         stopDistance: Float,
@@ -42,4 +48,38 @@
     override fun setPath(path: Path?, forceClosed: Boolean) {
         internalPathMeasure.setPath(path?.asAndroidPath(), forceClosed)
     }
+
+    override fun getPosition(
+        distance: Float
+    ): Offset {
+        if (positionArray == null) {
+            positionArray = FloatArray(2)
+        }
+        if (tangentArray == null) {
+            tangentArray = FloatArray(2)
+        }
+        val result = internalPathMeasure.getPosTan(distance, positionArray, tangentArray)
+        return if (result) {
+            Offset(positionArray!![0], positionArray!![1])
+        } else {
+            Offset.Unspecified
+        }
+    }
+
+    override fun getTangent(
+        distance: Float
+    ): Offset {
+        if (positionArray == null) {
+            positionArray = FloatArray(2)
+        }
+        if (tangentArray == null) {
+            tangentArray = FloatArray(2)
+        }
+        val result = internalPathMeasure.getPosTan(distance, positionArray, tangentArray)
+        return if (result) {
+            Offset(tangentArray!![0], tangentArray!![1])
+        } else {
+            Offset.Unspecified
+        }
+    }
 }
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..9537090 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,11 +16,20 @@
 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
  * specified in the given [Rect]
  */
+@Deprecated(
+    "Converting Rect to android.graphics.Rect is lossy, and requires rounding. The " +
+        "behavior of toAndroidRect() truncates to an integral Rect, but you should choose the " +
+        "method of rounding most suitable for your use case.",
+    replaceWith = ReplaceWith(
+        "android.graphics.Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())"
+    )
+)
 fun Rect.toAndroidRect(): android.graphics.Rect {
     return android.graphics.Rect(
         left.toInt(),
@@ -53,4 +62,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-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathMeasure.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathMeasure.kt
index 9e168cb..d2ee71f 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathMeasure.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathMeasure.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.graphics
 
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.internal.JvmDefaultWithCompatibility
 
 /**
@@ -53,4 +54,22 @@
      * Assign a new path, or null to have none.
      */
     fun setPath(path: Path?, forceClosed: Boolean)
+
+    /**
+     * Pins distance to 0 <= distance <= getLength(), and then computes the corresponding position
+     *
+     * @param distance The distance along the current contour to sample
+     *
+     * @return [Offset.Unspecified] if there is no path set
+     */
+    fun getPosition(distance: Float): Offset
+
+    /**
+     * Pins distance to 0 <= distance <= getLength(), and then computes the corresponding tangent
+     *
+     * @param distance The distance along the current contour to sample
+     *
+     * @return [Offset.Unspecified] if there is no path set
+     */
+    fun getTangent(distance: Float): Offset
 }
diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt
index 7658c68..810a409 100644
--- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt
+++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.graphics
 
+import androidx.compose.ui.geometry.Offset
 /**
  * Convert the [org.jetbrains.skia.PathMeasure] instance into a Compose-compatible PathMeasure
  */
@@ -49,6 +50,28 @@
 
     override val length: Float
         get() = skia.length
+
+    override fun getPosition(
+        distance: Float
+    ): Offset {
+        val result = skia.getPosition(distance)
+        return if (result != null) {
+            Offset(result.x, result.y)
+        } else {
+            Offset.Unspecified
+        }
+    }
+
+    override fun getTangent(
+        distance: Float
+    ): Offset {
+        val result = skia.getTangent(distance)
+        return if (result != null) {
+            Offset(result.x, result.y)
+        } else {
+            Offset.Unspecified
+        }
+    }
 }
 
 actual fun PathMeasure(): PathMeasure =
diff --git a/compose/ui/ui-inspection/generate-packages/compose_packages_list.txt b/compose/ui/ui-inspection/generate-packages/compose_packages_list.txt
new file mode 100644
index 0000000..6daab2d
--- /dev/null
+++ b/compose/ui/ui-inspection/generate-packages/compose_packages_list.txt
@@ -0,0 +1,45 @@
+androidx.compose.animation
+androidx.compose.animation.core
+androidx.compose.animation.graphics.vector
+androidx.compose.desktop
+androidx.compose.foundation
+androidx.compose.foundation.gestures
+androidx.compose.foundation.gestures.snapping
+androidx.compose.foundation.interaction
+androidx.compose.foundation.layout
+androidx.compose.foundation.lazy
+androidx.compose.foundation.lazy.grid
+androidx.compose.foundation.lazy.layout
+androidx.compose.foundation.lazy.staggeredgrid
+androidx.compose.foundation.newtext.text
+androidx.compose.foundation.newtext.text.copypasta
+androidx.compose.foundation.newtext.text.copypasta.selection
+androidx.compose.foundation.pager
+androidx.compose.foundation.relocation
+androidx.compose.foundation.text
+androidx.compose.foundation.text.selection
+androidx.compose.foundation.window
+androidx.compose.material
+androidx.compose.material.internal
+androidx.compose.material.pullrefresh
+androidx.compose.material.ripple
+androidx.compose.material3
+androidx.compose.material3.internal
+androidx.compose.material3.windowsizeclass
+androidx.compose.runtime
+androidx.compose.runtime.livedata
+androidx.compose.runtime.mock
+androidx.compose.runtime.reflect
+androidx.compose.runtime.rxjava2
+androidx.compose.runtime.rxjava3
+androidx.compose.runtime.saveable
+androidx.compose.ui
+androidx.compose.ui.awt
+androidx.compose.ui.graphics.benchmark
+androidx.compose.ui.graphics.vector
+androidx.compose.ui.layout
+androidx.compose.ui.platform
+androidx.compose.ui.text
+androidx.compose.ui.util
+androidx.compose.ui.viewinterop
+androidx.compose.ui.window
\ No newline at end of file
diff --git a/compose/ui/ui-inspection/generate-packages/generate_compose_packages.py b/compose/ui/ui-inspection/generate-packages/generate_compose_packages.py
new file mode 100755
index 0000000..f35cfe3
--- /dev/null
+++ b/compose/ui/ui-inspection/generate-packages/generate_compose_packages.py
@@ -0,0 +1,141 @@
+#!/usr/bin/env python3
+
+import argparse, os, sys
+
+# List of directories we want to exclude when traversing the project files. This list should
+# contain directories related to tests, documentation/samples, API, resources, etc.
+EXCLUDED_DIRS = ['androidTest', 'androidAndroidTest', 'api', 'docs', 'res', 'samples', 'test']
+# List of packages that should be excluded when traversing the project files. These packages might
+# include Java/Kotlin source files that contain Composable, but we're not interested in including
+# them on the output list because it's unlikely that developers will use them in their code.
+# For example, this list should contain (test) utility and tooling-related packages.
+EXCLUDED_PACKAGES = ['benchmark-utils', 'compiler', 'integration-tests', 'test-utils',
+                     'ui-android-stubs', 'ui-tooling', 'ui-tooling-data', 'ui-tooling-preview']
+# Set of directories that will be excluded when traversing the project files. Excluding a directory
+# means our search won't look into its subdirectories, so this list should be populated
+# with caution.
+EXCLUDED_FROM_FILE_SEARCH = set(EXCLUDED_DIRS + EXCLUDED_PACKAGES)
+
+# The directory containing this script, relative to androidx-main root.
+SCRIPT_DIR_PATH = 'frameworks/support/compose/ui/ui-inspection/generate-packages/'
+# The file name of this script.
+SCRIPT_NAME = 'generate_compose_packages.py'
+# File containing an ordered list of packages that contain at least one Composable.
+# The file is formatted as one package per line.
+COMPOSE_PACKAGES_LIST_FILE = 'compose_packages_list.txt'
+
+# Reads a source file with the given file_path and adds its package to the current set of packages
+# if the file contains at least one Composable.
+def add_package_if_composable(file_path, packages):
+    with open(file_path, 'r') as file:
+        lines = file.readlines()
+        for line in lines:
+            if line.startswith('package '):
+                package = line.lstrip('package').strip().strip(';')
+                # Early return to prevent reading the rest of the file.
+                if package in packages: return
+            if line.lstrip().startswith('@Composable') and package:
+                packages.add(package)
+                return
+
+# Iterates on a directory recursively, looking for Java/Kotlin source files that contain Composable
+# functions, and add their corresponding packages to a set that will be returned when the traversal
+# is complete.
+def extract_packages_from_directory(directory):
+    packages = set()
+    for root, dirs, files in os.walk(directory, topdown=True):
+        dirs[:] = [d for d in dirs if d not in EXCLUDED_FROM_FILE_SEARCH]
+        for filename in files:
+            if filename.endswith('.java') or filename.endswith('.kt'):
+                add_package_if_composable(os.path.join(root, filename), packages)
+    return packages
+
+# Verifies that the given the list of packages match the ones currently listed on the
+# compose_packages_list.txt file
+def verify_packages(packages):
+    with open(COMPOSE_PACKAGES_LIST_FILE, 'r') as file:
+        file_packages = file.readlines()
+        if len(file_packages) != len(packages): report_failure_and_exit()
+        for i in range(len(file_packages)):
+            if packages[i] != file_packages[i].strip('\n'): report_failure_and_exit()
+
+def report_failure_and_exit():
+    print(
+        'Compose packages mismatch\n The current list of Compose packages does not match the list '
+        'stored in %s%s. If the current list of packages have changed, please regenerate the list '
+        'by running the following command:\n\t%s%s --regenerate' % (
+            SCRIPT_DIR_PATH,
+            COMPOSE_PACKAGES_LIST_FILE,
+            SCRIPT_DIR_PATH,
+            SCRIPT_NAME
+        ),
+        file=sys.stderr
+    )
+    sys.exit(1)
+
+# Regenerates the compose_packages_list.txt file, given the list of packages.
+def regenerate_packages_file(packages):
+    with open(COMPOSE_PACKAGES_LIST_FILE, 'w') as file:
+        file.write('\n'.join(packages))
+
+# Regenerates the PackageHashes.kt, given the list of packages. The file format is:
+# 1) Header indicating the file should not be edited manually
+# 2) Package definition
+# 3) Required imports
+# 4) packageNameHash function
+# 5) systemPackages val, which is a list containing the result of the packageNameHash
+#    function applied to each package name of the given packages list.
+def regenerate_packages_kt_file(packages):
+    kt_file = '../src/main/java/androidx/compose/ui/inspection/inspector/PackageHashes.kt'
+    header = (
+        '// WARNING: DO NOT EDIT THIS FILE MANUALLY. It\'s automatically generated by running:\n'
+        '//    %s%s -r\n' % (SCRIPT_DIR_PATH, SCRIPT_NAME)
+    )
+    package = 'package androidx.compose.ui.inspection.inspector\n\n'
+    imports = (
+        'import androidx.annotation.VisibleForTesting\n'
+        'import kotlin.math.absoluteValue\n\n'
+    )
+    package_name_hash_function = (
+        '@VisibleForTesting\n'
+        'fun packageNameHash(packageName: String) =\n'
+        '    packageName.fold(0) { hash, char -> hash * 31 + char.code }.absoluteValue\n\n'
+    )
+    system_packages_val = (
+        'val systemPackages = setOf(\n'
+        '    -1,\n'
+        '%s\n'
+        ')\n' % (
+            '\n'.join(['    packageNameHash("' + package + '"),' for package in packages])
+        )
+    )
+    with open(kt_file, 'w') as file:
+        file.write(
+            header + package + imports + package_name_hash_function + system_packages_val
+        )
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser(
+        description='This script is invoked to check whether the current list of packages '
+                    'containing Composables is up-to-date. This list is used by Layout Inspector '
+                    'and Compose Preview to filter out framework Composables.'
+    )
+    parser.add_argument(
+        '-r',
+        '--regenerate',
+        action='store_true',
+        help='this argument should be used to regenerate the list of packages'
+    )
+    args = parser.parse_args()
+
+    # cd into directory of script
+    os.chdir(os.path.dirname(os.path.abspath(__file__)))
+    # root should be `frameworks/support/compose/`, relative to the script directory
+    compose_root_directory = '../../..'
+    current_packages = sorted(extract_packages_from_directory(compose_root_directory))
+
+    if args.regenerate:
+        regenerate_packages_file(current_packages)
+        regenerate_packages_kt_file(current_packages)
+    else:
+        verify_packages(current_packages)
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..2212deb 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
@@ -46,34 +46,10 @@
 import java.util.ArrayDeque
 import java.util.Collections
 import java.util.IdentityHashMap
-import kotlin.math.absoluteValue
 import kotlin.math.max
 import kotlin.math.min
 import kotlin.math.roundToInt
 
-val systemPackages = setOf(
-    -1,
-    packageNameHash("androidx.compose.animation"),
-    packageNameHash("androidx.compose.animation.core"),
-    packageNameHash("androidx.compose.desktop"),
-    packageNameHash("androidx.compose.foundation"),
-    packageNameHash("androidx.compose.foundation.layout"),
-    packageNameHash("androidx.compose.foundation.text"),
-    packageNameHash("androidx.compose.material"),
-    packageNameHash("androidx.compose.material.ripple"),
-    packageNameHash("androidx.compose.runtime"),
-    packageNameHash("androidx.compose.runtime.saveable"),
-    packageNameHash("androidx.compose.ui"),
-    packageNameHash("androidx.compose.ui.graphics.vector"),
-    packageNameHash("androidx.compose.ui.layout"),
-    packageNameHash("androidx.compose.ui.platform"),
-    packageNameHash("androidx.compose.ui.tooling"),
-    packageNameHash("androidx.compose.ui.selection"),
-    packageNameHash("androidx.compose.ui.semantics"),
-    packageNameHash("androidx.compose.ui.viewinterop"),
-    packageNameHash("androidx.compose.ui.window"),
-)
-
 /**
  * The [InspectorNode.id] will be populated with:
  * - the layerId from a LayoutNode if this exists
@@ -96,10 +72,6 @@
     "ProvideCommonCompositionLocals",
 )
 
-@VisibleForTesting
-fun packageNameHash(packageName: String) =
-    packageName.fold(0) { hash, char -> hash * 31 + char.code }.absoluteValue
-
 /**
  * Generator of a tree for the Layout Inspector.
  */
@@ -452,6 +424,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/inspector/PackageHashes.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/PackageHashes.kt
new file mode 100644
index 0000000..bf606d1
--- /dev/null
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/PackageHashes.kt
@@ -0,0 +1,59 @@
+// WARNING: DO NOT EDIT THIS FILE MANUALLY. It's automatically generated by running:
+//    frameworks/support/compose/ui/ui-inspection/generate-packages/generate_compose_packages.py -r
+package androidx.compose.ui.inspection.inspector
+
+import androidx.annotation.VisibleForTesting
+import kotlin.math.absoluteValue
+
+@VisibleForTesting
+fun packageNameHash(packageName: String) =
+    packageName.fold(0) { hash, char -> hash * 31 + char.code }.absoluteValue
+
+val systemPackages = setOf(
+    -1,
+    packageNameHash("androidx.compose.animation"),
+    packageNameHash("androidx.compose.animation.core"),
+    packageNameHash("androidx.compose.animation.graphics.vector"),
+    packageNameHash("androidx.compose.desktop"),
+    packageNameHash("androidx.compose.foundation"),
+    packageNameHash("androidx.compose.foundation.gestures"),
+    packageNameHash("androidx.compose.foundation.gestures.snapping"),
+    packageNameHash("androidx.compose.foundation.interaction"),
+    packageNameHash("androidx.compose.foundation.layout"),
+    packageNameHash("androidx.compose.foundation.lazy"),
+    packageNameHash("androidx.compose.foundation.lazy.grid"),
+    packageNameHash("androidx.compose.foundation.lazy.layout"),
+    packageNameHash("androidx.compose.foundation.lazy.staggeredgrid"),
+    packageNameHash("androidx.compose.foundation.newtext.text"),
+    packageNameHash("androidx.compose.foundation.newtext.text.copypasta"),
+    packageNameHash("androidx.compose.foundation.newtext.text.copypasta.selection"),
+    packageNameHash("androidx.compose.foundation.pager"),
+    packageNameHash("androidx.compose.foundation.relocation"),
+    packageNameHash("androidx.compose.foundation.text"),
+    packageNameHash("androidx.compose.foundation.text.selection"),
+    packageNameHash("androidx.compose.foundation.window"),
+    packageNameHash("androidx.compose.material"),
+    packageNameHash("androidx.compose.material.internal"),
+    packageNameHash("androidx.compose.material.pullrefresh"),
+    packageNameHash("androidx.compose.material.ripple"),
+    packageNameHash("androidx.compose.material3"),
+    packageNameHash("androidx.compose.material3.internal"),
+    packageNameHash("androidx.compose.material3.windowsizeclass"),
+    packageNameHash("androidx.compose.runtime"),
+    packageNameHash("androidx.compose.runtime.livedata"),
+    packageNameHash("androidx.compose.runtime.mock"),
+    packageNameHash("androidx.compose.runtime.reflect"),
+    packageNameHash("androidx.compose.runtime.rxjava2"),
+    packageNameHash("androidx.compose.runtime.rxjava3"),
+    packageNameHash("androidx.compose.runtime.saveable"),
+    packageNameHash("androidx.compose.ui"),
+    packageNameHash("androidx.compose.ui.awt"),
+    packageNameHash("androidx.compose.ui.graphics.benchmark"),
+    packageNameHash("androidx.compose.ui.graphics.vector"),
+    packageNameHash("androidx.compose.ui.layout"),
+    packageNameHash("androidx.compose.ui.platform"),
+    packageNameHash("androidx.compose.ui.text"),
+    packageNameHash("androidx.compose.ui.util"),
+    packageNameHash("androidx.compose.ui.viewinterop"),
+    packageNameHash("androidx.compose.ui.window"),
+)
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-junit4/api/public_plus_experimental_current.txt b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
index f9e2d8a..850087e 100644
--- a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
@@ -7,7 +7,7 @@
   }
 
   @androidx.compose.ui.test.ExperimentalTestApi public abstract class AndroidComposeUiTestEnvironment<A extends androidx.activity.ComponentActivity> {
-    ctor public AndroidComposeUiTestEnvironment();
+    ctor public AndroidComposeUiTestEnvironment(optional kotlin.coroutines.CoroutineContext effectContext);
     method protected abstract A? getActivity();
     method public final androidx.compose.ui.test.AndroidComposeUiTest<A> getTest();
     method public final <R> R! runTest(kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.AndroidComposeUiTest<A>,? extends R> block);
@@ -31,14 +31,18 @@
   }
 
   public final class ComposeUiTestKt {
-    method @androidx.compose.ui.test.ExperimentalTestApi public static void runComposeUiTest(kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.ComposeUiTest,? extends kotlin.Unit> block);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static void runComposeUiTest(optional kotlin.coroutines.CoroutineContext effectContext, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.ComposeUiTest,? extends kotlin.Unit> block);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static void waitUntilAtLeastOneExists(androidx.compose.ui.test.ComposeUiTest, androidx.compose.ui.test.SemanticsMatcher matcher, optional long timeoutMillis);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static void waitUntilDoesNotExist(androidx.compose.ui.test.ComposeUiTest, androidx.compose.ui.test.SemanticsMatcher matcher, optional long timeoutMillis);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static void waitUntilExactlyOneExists(androidx.compose.ui.test.ComposeUiTest, androidx.compose.ui.test.SemanticsMatcher matcher, optional long timeoutMillis);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static void waitUntilNodeCount(androidx.compose.ui.test.ComposeUiTest, androidx.compose.ui.test.SemanticsMatcher matcher, int count, optional long timeoutMillis);
   }
 
   public final class ComposeUiTest_androidKt {
-    method @androidx.compose.ui.test.ExperimentalTestApi public static inline <A extends androidx.activity.ComponentActivity> androidx.compose.ui.test.AndroidComposeUiTestEnvironment<A> AndroidComposeUiTestEnvironment(kotlin.jvm.functions.Function0<? extends A> activityProvider);
-    method @androidx.compose.ui.test.ExperimentalTestApi public static <A extends androidx.activity.ComponentActivity> void runAndroidComposeUiTest(Class<A> activityClass, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.AndroidComposeUiTest<A>,kotlin.Unit> block);
-    method @androidx.compose.ui.test.ExperimentalTestApi public static inline <reified A extends androidx.activity.ComponentActivity> void runAndroidComposeUiTest(kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.AndroidComposeUiTest<A>,? extends kotlin.Unit> block);
-    method @androidx.compose.ui.test.ExperimentalTestApi public static void runComposeUiTest(kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.ComposeUiTest,kotlin.Unit> block);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static inline <A extends androidx.activity.ComponentActivity> androidx.compose.ui.test.AndroidComposeUiTestEnvironment<A> AndroidComposeUiTestEnvironment(optional kotlin.coroutines.CoroutineContext effectContext, kotlin.jvm.functions.Function0<? extends A> activityProvider);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static <A extends androidx.activity.ComponentActivity> void runAndroidComposeUiTest(Class<A> activityClass, optional kotlin.coroutines.CoroutineContext effectContext, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.AndroidComposeUiTest<A>,kotlin.Unit> block);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static inline <reified A extends androidx.activity.ComponentActivity> void runAndroidComposeUiTest(optional kotlin.coroutines.CoroutineContext effectContext, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.AndroidComposeUiTest<A>,? extends kotlin.Unit> block);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static void runComposeUiTest(kotlin.coroutines.CoroutineContext effectContext, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.ComposeUiTest,kotlin.Unit> block);
     method @androidx.compose.ui.test.ExperimentalTestApi public static void runEmptyComposeUiTest(kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.ComposeUiTest,kotlin.Unit> block);
   }
 
@@ -54,6 +58,7 @@
 
   public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeContentTestRule {
     ctor public AndroidComposeTestRule(R activityRule, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
+    ctor @androidx.compose.ui.test.ExperimentalTestApi public AndroidComposeTestRule(R activityRule, optional kotlin.coroutines.CoroutineContext effectContext, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
     method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
     method public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public A getActivity();
@@ -69,6 +74,10 @@
     method public void unregisterIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
     method public void waitForIdle();
     method public void waitUntil(long timeoutMillis, kotlin.jvm.functions.Function0<java.lang.Boolean> condition);
+    method @androidx.compose.ui.test.ExperimentalTestApi public void waitUntilAtLeastOneExists(androidx.compose.ui.test.SemanticsMatcher matcher, long timeoutMillis);
+    method @androidx.compose.ui.test.ExperimentalTestApi public void waitUntilDoesNotExist(androidx.compose.ui.test.SemanticsMatcher matcher, long timeoutMillis);
+    method @androidx.compose.ui.test.ExperimentalTestApi public void waitUntilExactlyOneExists(androidx.compose.ui.test.SemanticsMatcher matcher, long timeoutMillis);
+    method @androidx.compose.ui.test.ExperimentalTestApi public void waitUntilNodeCount(androidx.compose.ui.test.SemanticsMatcher matcher, int count, long timeoutMillis);
     property public final A activity;
     property public final R activityRule;
     property public androidx.compose.ui.unit.Density density;
@@ -82,9 +91,13 @@
 
   public final class AndroidComposeTestRule_androidKt {
     method public static <A extends androidx.activity.ComponentActivity> androidx.compose.ui.test.junit4.AndroidComposeTestRule<androidx.test.ext.junit.rules.ActivityScenarioRule<A>,A> createAndroidComposeRule(Class<A> activityClass);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static <A extends androidx.activity.ComponentActivity> androidx.compose.ui.test.junit4.AndroidComposeTestRule<androidx.test.ext.junit.rules.ActivityScenarioRule<A>,A> createAndroidComposeRule(Class<A> activityClass, optional kotlin.coroutines.CoroutineContext effectContext);
     method public static inline <reified A extends androidx.activity.ComponentActivity> androidx.compose.ui.test.junit4.AndroidComposeTestRule<androidx.test.ext.junit.rules.ActivityScenarioRule<A>,A> createAndroidComposeRule();
+    method @androidx.compose.ui.test.ExperimentalTestApi public static inline <reified A extends androidx.activity.ComponentActivity> androidx.compose.ui.test.junit4.AndroidComposeTestRule<androidx.test.ext.junit.rules.ActivityScenarioRule<A>,A> createAndroidComposeRule(optional kotlin.coroutines.CoroutineContext effectContext);
     method public static androidx.compose.ui.test.junit4.ComposeContentTestRule createComposeRule();
+    method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.junit4.ComposeContentTestRule createComposeRule(kotlin.coroutines.CoroutineContext effectContext);
     method public static androidx.compose.ui.test.junit4.ComposeTestRule createEmptyComposeRule();
+    method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.junit4.ComposeTestRule createEmptyComposeRule(optional kotlin.coroutines.CoroutineContext effectContext);
   }
 
   public final class AndroidSynchronization_androidKt {
@@ -110,6 +123,10 @@
     method public void unregisterIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
     method public void waitForIdle();
     method public void waitUntil(optional long timeoutMillis, kotlin.jvm.functions.Function0<java.lang.Boolean> condition);
+    method @androidx.compose.ui.test.ExperimentalTestApi public void waitUntilAtLeastOneExists(androidx.compose.ui.test.SemanticsMatcher matcher, optional long timeoutMillis);
+    method @androidx.compose.ui.test.ExperimentalTestApi public void waitUntilDoesNotExist(androidx.compose.ui.test.SemanticsMatcher matcher, optional long timeoutMillis);
+    method @androidx.compose.ui.test.ExperimentalTestApi public void waitUntilExactlyOneExists(androidx.compose.ui.test.SemanticsMatcher matcher, optional long timeoutMillis);
+    method @androidx.compose.ui.test.ExperimentalTestApi public void waitUntilNodeCount(androidx.compose.ui.test.SemanticsMatcher matcher, int count, optional long timeoutMillis);
     property public abstract androidx.compose.ui.unit.Density density;
     property public abstract androidx.compose.ui.test.MainTestClock mainClock;
   }
diff --git a/compose/ui/ui-test-junit4/build.gradle b/compose/ui/ui-test-junit4/build.gradle
index d4b8514..5e4501d 100644
--- a/compose/ui/ui-test-junit4/build.gradle
+++ b/compose/ui/ui-test-junit4/build.gradle
@@ -141,6 +141,15 @@
                 implementation(libs.truth)
                 implementation(libs.skiko)
             }
+
+            desktopTest.dependencies {
+                implementation(libs.truth)
+                implementation(libs.junit)
+                implementation(libs.kotlinTest)
+                implementation(libs.skikoCurrentOs)
+                implementation(project(":compose:foundation:foundation"))
+                implementation(project(":compose:ui:ui-test-junit4"))
+            }
         }
     }
 
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt
index 8d2b530..f820217 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt
@@ -17,11 +17,12 @@
 package androidx.compose.ui.test
 
 import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.LocalOverscrollConfiguration
 import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.LocalOverscrollConfiguration
 import androidx.compose.foundation.gestures.ScrollScope
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
@@ -41,22 +42,34 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.withFrameNanos
 import androidx.compose.testutils.WithTouchSlop
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.MotionDurationScale
 import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.junit4.createEmptyComposeRule
 import androidx.compose.ui.unit.dp
 import androidx.test.espresso.IdlingPolicies
 import androidx.test.espresso.IdlingPolicy
+import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
+import kotlin.coroutines.CoroutineContext
 import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineScope
 import org.junit.After
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
 import org.junit.runner.RunWith
+import org.junit.runners.model.Statement
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
@@ -64,6 +77,18 @@
 class ComposeUiTestTest {
 
     private var idlingPolicy: IdlingPolicy? = null
+    private lateinit var testDescription: Description
+
+    /**
+     * Records the current [testDescription] for tests that need to invoke the compose test rule
+     * directly.
+     */
+    @get:Rule
+    val testWatcher = object : TestWatcher() {
+        override fun starting(description: Description) {
+            testDescription = description
+        }
+    }
 
     @Before
     fun setup() {
@@ -130,7 +155,12 @@
         setContent {
             val offset = animateFloatAsState(target)
             Box(Modifier.fillMaxSize()) {
-                Box(Modifier.size(10.dp).offset(x = offset.value.dp).testTag("box"))
+                Box(
+                    Modifier
+                        .size(10.dp)
+                        .offset(x = offset.value.dp)
+                        .testTag("box")
+                )
             }
         }
         onNodeWithTag("box").assertLeftPositionInRootIsEqualTo(0.dp)
@@ -158,13 +188,20 @@
                 CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
                     Box(Modifier.fillMaxSize()) {
                         Column(
-                            Modifier.requiredSize(200.dp).verticalScroll(
-                                scrollState,
-                                flingBehavior = flingBehavior
-                            ).testTag("list")
+                            Modifier
+                                .requiredSize(200.dp)
+                                .verticalScroll(
+                                    scrollState,
+                                    flingBehavior = flingBehavior
+                                )
+                                .testTag("list")
                         ) {
                             repeat(n) {
-                                Spacer(Modifier.fillMaxWidth().height(30.dp))
+                                Spacer(
+                                    Modifier
+                                        .fillMaxWidth()
+                                        .height(30.dp)
+                                )
                             }
                         }
                     }
@@ -217,4 +254,124 @@
     fun getActivityTest() = runAndroidComposeUiTest<ComponentActivity> {
         assertThat(activity).isNotNull()
     }
+
+    @Test
+    fun effectContextPropagatedToComposition_runComposeUiTest() {
+        val testElement = TestCoroutineContextElement()
+        runComposeUiTest(effectContext = testElement) {
+            lateinit var compositionScope: CoroutineScope
+            setContent {
+                compositionScope = rememberCoroutineScope()
+            }
+
+            runOnIdle {
+                val elementFromComposition =
+                    compositionScope.coroutineContext[TestCoroutineContextElement]
+                assertThat(elementFromComposition).isSameInstanceAs(testElement)
+            }
+        }
+    }
+
+    @Test
+    fun effectContextPropagatedToComposition_createComposeRule() {
+        val testElement = TestCoroutineContextElement()
+        lateinit var compositionScope: CoroutineScope
+        val rule = createComposeRule(testElement)
+        val baseStatement = object : Statement() {
+            override fun evaluate() {
+                rule.setContent {
+                    compositionScope = rememberCoroutineScope()
+                }
+                rule.waitForIdle()
+            }
+        }
+        rule.apply(baseStatement, testDescription)
+            .evaluate()
+
+        val elementFromComposition =
+            compositionScope.coroutineContext[TestCoroutineContextElement]
+        assertThat(elementFromComposition).isSameInstanceAs(testElement)
+    }
+
+    @Test
+    fun effectContextPropagatedToComposition_createAndroidComposeRule() {
+        val testElement = TestCoroutineContextElement()
+        lateinit var compositionScope: CoroutineScope
+        val rule = createAndroidComposeRule<ComponentActivity>(testElement)
+        val baseStatement = object : Statement() {
+            override fun evaluate() {
+                rule.setContent {
+                    compositionScope = rememberCoroutineScope()
+                }
+                rule.waitForIdle()
+            }
+        }
+        rule.apply(baseStatement, testDescription)
+            .evaluate()
+
+        val elementFromComposition =
+            compositionScope.coroutineContext[TestCoroutineContextElement]
+        assertThat(elementFromComposition).isSameInstanceAs(testElement)
+    }
+
+    @Test
+    fun effectContextPropagatedToComposition_createEmptyComposeRule() {
+        val testElement = TestCoroutineContextElement()
+        lateinit var compositionScope: CoroutineScope
+        val composeRule = createEmptyComposeRule(testElement)
+        val activityRule = ActivityScenarioRule(ComponentActivity::class.java)
+        val baseStatement = object : Statement() {
+            override fun evaluate() {
+                activityRule.scenario.onActivity {
+                    it.setContent {
+                        compositionScope = rememberCoroutineScope()
+                    }
+                }
+                composeRule.waitForIdle()
+            }
+        }
+        activityRule.apply(composeRule.apply(baseStatement, testDescription), testDescription)
+            .evaluate()
+
+        val elementFromComposition =
+            compositionScope.coroutineContext[TestCoroutineContextElement]
+        assertThat(elementFromComposition).isSameInstanceAs(testElement)
+    }
+
+    @Test
+    fun motionDurationScale_defaultValue() = runComposeUiTest {
+        var lastRecordedMotionDurationScale: Float? = null
+        setContent {
+            val context = rememberCoroutineScope().coroutineContext
+            lastRecordedMotionDurationScale = context[MotionDurationScale]?.scaleFactor
+        }
+
+        runOnIdle {
+            assertThat(lastRecordedMotionDurationScale).isNull()
+        }
+    }
+
+    @Test
+    fun motionDurationScale_propagatedToCoroutines() {
+        val motionDurationScale = object : MotionDurationScale {
+            override val scaleFactor: Float get() = 0f
+        }
+        runComposeUiTest(effectContext = motionDurationScale) {
+            var lastRecordedMotionDurationScale: Float? = null
+            setContent {
+                val context = rememberCoroutineScope().coroutineContext
+                lastRecordedMotionDurationScale = context[MotionDurationScale]?.scaleFactor
+            }
+
+            runOnIdle {
+                assertThat(lastRecordedMotionDurationScale).isEqualTo(0f)
+            }
+        }
+    }
+
+    private class TestCoroutineContextElement : CoroutineContext.Element {
+        override val key: CoroutineContext.Key<*> get() = Key
+
+        companion object Key : CoroutineContext.Key<TestCoroutineContextElement>
+    }
 }
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeTestRuleWaitUntilTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeTestRuleWaitUntilTest.kt
new file mode 100644
index 0000000..3652cf4
--- /dev/null
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeTestRuleWaitUntilTest.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test.junit4
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.expectError
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ComposeTimeoutException
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalTestApi::class)
+class ComposeTestRuleWaitUntilTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    companion object {
+        private const val TestTag = "TestTag"
+        private const val Timeout = 500L
+    }
+
+    @Composable
+    private fun TaggedBox() = Box(
+        Modifier
+            .size(10.dp, 10.dp)
+            .testTag(TestTag)
+    )
+
+    @Test
+    fun waitUntilNodeCount_succeedsWhen_nodeCountCorrect() {
+        rule.setContent {
+            TaggedBox()
+            TaggedBox()
+            TaggedBox()
+        }
+
+        rule.waitUntilNodeCount(hasTestTag(TestTag), 3, Timeout)
+    }
+
+    @Test
+    fun waitUntilNodeCount_throwsWhen_nodeCountIncorrect() {
+        rule.setContent {
+            TaggedBox()
+            TaggedBox()
+            TaggedBox()
+        }
+
+        expectError<ComposeTimeoutException>(
+            expectedMessage = "Condition still not satisfied after $Timeout ms"
+        ) {
+            rule.waitUntilNodeCount(hasTestTag(TestTag), 2, Timeout)
+        }
+    }
+
+    @Test
+    fun waitUntilAtLeastOneExists_succeedsWhen_nodesExist() {
+        rule.setContent {
+            TaggedBox()
+            TaggedBox()
+        }
+
+        rule.waitUntilAtLeastOneExists(hasTestTag(TestTag))
+    }
+
+    @Test
+    fun waitUntilAtLeastOneExists_throwsWhen_nodesDoNotExist() {
+        rule.setContent {
+            Box(Modifier.size(10.dp))
+        }
+
+        expectError<ComposeTimeoutException>(
+            expectedMessage = "Condition still not satisfied after $Timeout ms"
+        ) {
+            rule.waitUntilAtLeastOneExists(hasTestTag(TestTag), Timeout)
+        }
+    }
+
+    @Test
+    fun waitUntilExactlyOneExists_succeedsWhen_oneNodeExists() {
+        rule.setContent {
+            TaggedBox()
+        }
+
+        rule.waitUntilExactlyOneExists(hasTestTag(TestTag))
+    }
+
+    @Test
+    fun waitUntilExactlyOneExists_throwsWhen_twoNodesExist() {
+        rule.setContent {
+            TaggedBox()
+            TaggedBox()
+        }
+
+        expectError<ComposeTimeoutException>(
+            expectedMessage = "Condition still not satisfied after $Timeout ms"
+        ) {
+            rule.waitUntilExactlyOneExists(hasTestTag(TestTag), Timeout)
+        }
+    }
+
+    @Test
+    fun waitUntilDoesNotExists_succeedsWhen_nodeDoesNotExist() {
+        rule.setContent {
+            Box(Modifier.size(10.dp))
+        }
+
+        rule.waitUntilDoesNotExist(hasTestTag(TestTag), timeoutMillis = Timeout)
+    }
+
+    @Test
+    fun waitUntilDoesNotExists_throwsWhen_nodeExistsUntilTimeout() {
+        rule.setContent {
+            TaggedBox()
+        }
+
+        expectError<ComposeTimeoutException>(
+            expectedMessage = "Condition still not satisfied after $Timeout ms"
+        ) {
+            rule.waitUntilDoesNotExist(hasTestTag(TestTag), timeoutMillis = Timeout)
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt
new file mode 100644
index 0000000..9fc70a6
--- /dev/null
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test.junit4
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.expectError
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ComposeTimeoutException
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.runComposeUiTest
+import androidx.compose.ui.test.waitUntilAtLeastOneExists
+import androidx.compose.ui.test.waitUntilDoesNotExist
+import androidx.compose.ui.test.waitUntilExactlyOneExists
+import androidx.compose.ui.test.waitUntilNodeCount
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalTestApi::class)
+class WaitUntilNodeCountTest {
+    companion object {
+        private const val TestTag = "TestTag"
+        private const val Timeout = 500L
+    }
+
+    @Composable
+    private fun TaggedBox() = Box(
+        Modifier
+            .size(10.dp, 10.dp)
+            .testTag(TestTag)
+    )
+
+    @Test
+    fun waitUntilNodeCount_succeedsWhen_nodeCountCorrect() = runComposeUiTest {
+        setContent {
+            TaggedBox()
+            TaggedBox()
+            TaggedBox()
+        }
+
+        waitUntilNodeCount(hasTestTag(TestTag), 3, Timeout)
+    }
+
+    @Test
+    fun waitUntilNodeCount_throwsWhen_nodeCountIncorrect() = runComposeUiTest {
+        setContent {
+            TaggedBox()
+            TaggedBox()
+            TaggedBox()
+        }
+
+        expectError<ComposeTimeoutException>(
+            expectedMessage = "Condition still not satisfied after $Timeout ms"
+        ) {
+            waitUntilNodeCount(hasTestTag(TestTag), 2, Timeout)
+        }
+    }
+
+    @Test
+    fun waitUntilAtLeastOneExists_succeedsWhen_nodesExist() = runComposeUiTest {
+        setContent {
+            TaggedBox()
+            TaggedBox()
+        }
+
+        waitUntilAtLeastOneExists(hasTestTag(TestTag))
+    }
+
+    @Test
+    fun waitUntilAtLeastOneExists_throwsWhen_nodesDoNotExist() = runComposeUiTest {
+        setContent {
+            Box(Modifier.size(10.dp))
+        }
+
+        expectError<ComposeTimeoutException>(
+            expectedMessage = "Condition still not satisfied after $Timeout ms"
+        ) {
+            waitUntilAtLeastOneExists(hasTestTag(TestTag), Timeout)
+        }
+    }
+
+    @Test
+    fun waitUntilExactlyOneExists_succeedsWhen_oneNodeExists() = runComposeUiTest {
+        setContent {
+            TaggedBox()
+        }
+
+        waitUntilExactlyOneExists(hasTestTag(TestTag))
+    }
+
+    @Test
+    fun waitUntilExactlyOneExists_throwsWhen_twoNodesExist() = runComposeUiTest {
+        setContent {
+            TaggedBox()
+            TaggedBox()
+        }
+
+        expectError<ComposeTimeoutException>(
+            expectedMessage = "Condition still not satisfied after $Timeout ms"
+        ) {
+            waitUntilExactlyOneExists(hasTestTag(TestTag), Timeout)
+        }
+    }
+
+    @Test
+    fun waitUntilDoesNotExists_succeedsWhen_nodeDoesNotExist() = runComposeUiTest {
+        setContent {
+            Box(Modifier.size(10.dp))
+        }
+
+        waitUntilDoesNotExist(hasTestTag(TestTag), timeoutMillis = Timeout)
+    }
+
+    @Test
+    fun waitUntilDoesNotExists_throwsWhen_nodeExistsUntilTimeout() = runComposeUiTest {
+        setContent {
+            TaggedBox()
+        }
+
+        expectError<ComposeTimeoutException>(
+            expectedMessage = "Condition still not satisfied after $Timeout ms"
+        ) {
+            waitUntilDoesNotExist(hasTestTag(TestTag), timeoutMillis = Timeout)
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
index 5453449..7b88012 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
@@ -48,6 +48,8 @@
 import androidx.compose.ui.unit.Density
 import androidx.test.core.app.ActivityScenario
 import androidx.test.core.app.ApplicationProvider
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -59,8 +61,8 @@
 import kotlinx.coroutines.test.runTest
 
 @ExperimentalTestApi
-actual fun runComposeUiTest(block: ComposeUiTest.() -> Unit) {
-    runAndroidComposeUiTest(ComponentActivity::class.java, block)
+actual fun runComposeUiTest(effectContext: CoroutineContext, block: ComposeUiTest.() -> Unit) {
+    runAndroidComposeUiTest(ComponentActivity::class.java, effectContext, block)
 }
 
 /**
@@ -71,12 +73,16 @@
  *
  * @param A The Activity type to be launched, which typically (but not necessarily) hosts the
  * Compose content
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ * @param block The test function.
  */
 @ExperimentalTestApi
 inline fun <reified A : ComponentActivity> runAndroidComposeUiTest(
+    effectContext: CoroutineContext = EmptyCoroutineContext,
     noinline block: AndroidComposeUiTest<A>.() -> Unit
 ) {
-    runAndroidComposeUiTest(A::class.java, block)
+    runAndroidComposeUiTest(A::class.java, effectContext, block)
 }
 
 /**
@@ -87,16 +93,21 @@
  *
  * @param A The Activity type to be launched, which typically (but not necessarily) hosts the
  * Compose content
+ * @param activityClass The [Class] of the Activity type to be launched, corresponding to [A].
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ * @param block The test function.
  */
 @ExperimentalTestApi
 fun <A : ComponentActivity> runAndroidComposeUiTest(
     activityClass: Class<A>,
+    effectContext: CoroutineContext = EmptyCoroutineContext,
     block: AndroidComposeUiTest<A>.() -> Unit
 ) {
     // Don't start the scenario now, wait until we're inside runTest { },
     // in case the Activity's onCreate/Start/Resume calls setContent
     var scenario: ActivityScenario<A>? = null
-    val environment = AndroidComposeUiTestEnvironment {
+    val environment = AndroidComposeUiTestEnvironment(effectContext) {
         requireNotNull(scenario) {
             "ActivityScenario has not yet been launched, or has already finished. Make sure that " +
                 "any call to ComposeUiTest.setContent() and AndroidComposeUiTest.getActivity() " +
@@ -195,13 +206,16 @@
  * @param activityProvider A lambda that should return the current Activity instance of type [A],
  * if it is available. If it is not available, it should return `null`.
  * @param A The Activity type to be interacted with, which typically (but not necessarily) is the
- * activity that was launched and hosts the Compose content
+ * activity that was launched and hosts the Compose content.
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
  */
 @ExperimentalTestApi
 inline fun <A : ComponentActivity> AndroidComposeUiTestEnvironment(
+    effectContext: CoroutineContext = EmptyCoroutineContext,
     crossinline activityProvider: () -> A?
 ): AndroidComposeUiTestEnvironment<A> {
-    return object : AndroidComposeUiTestEnvironment<A>() {
+    return object : AndroidComposeUiTestEnvironment<A>(effectContext) {
         override val activity: A?
             get() = activityProvider.invoke()
     }
@@ -213,11 +227,15 @@
  * as they require that the environment has been set up.
  *
  * @param A The Activity type to be interacted with, which typically (but not necessarily) is the
- * activity that was launched and hosts the Compose content
+ * activity that was launched and hosts the Compose content.
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
  */
 @ExperimentalTestApi
 @OptIn(InternalTestApi::class, ExperimentalCoroutinesApi::class, ExperimentalComposeUiApi::class)
-abstract class AndroidComposeUiTestEnvironment<A : ComponentActivity> {
+abstract class AndroidComposeUiTestEnvironment<A : ComponentActivity>(
+    effectContext: CoroutineContext = EmptyCoroutineContext
+) {
     private val idlingResourceRegistry = IdlingResourceRegistry()
 
     internal val composeRootRegistry = ComposeRootRegistry()
@@ -259,8 +277,12 @@
             }
         }
         recomposerCoroutineScope = CoroutineScope(
-            recomposerContinuationInterceptor + frameClock + infiniteAnimationPolicy +
-                coroutineExceptionHandler + Job()
+            effectContext +
+                recomposerContinuationInterceptor +
+                frameClock +
+                infiniteAnimationPolicy +
+                coroutineExceptionHandler +
+                Job()
         )
         recomposer = Recomposer(recomposerCoroutineScope.coroutineContext)
         composeIdlingResource = ComposeIdlingResource(
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
index 887eb84..626ca1e3 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
@@ -25,8 +25,14 @@
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.SemanticsNodeInteractionCollection
+import androidx.compose.ui.test.waitUntilAtLeastOneExists
+import androidx.compose.ui.test.waitUntilDoesNotExist
+import androidx.compose.ui.test.waitUntilExactlyOneExists
+import androidx.compose.ui.test.waitUntilNodeCount
 import androidx.compose.ui.unit.Density
 import androidx.test.ext.junit.rules.ActivityScenarioRule
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
@@ -34,6 +40,10 @@
 actual fun createComposeRule(): ComposeContentTestRule =
     createAndroidComposeRule<ComponentActivity>()
 
+@ExperimentalTestApi
+actual fun createComposeRule(effectContext: CoroutineContext): ComposeContentTestRule =
+    createAndroidComposeRule<ComponentActivity>(effectContext)
+
 /**
  * Factory method to provide android specific implementation of [createComposeRule], for a given
  * activity class type [A].
@@ -51,12 +61,38 @@
  */
 inline fun <reified A : ComponentActivity> createAndroidComposeRule():
     AndroidComposeTestRule<ActivityScenarioRule<A>, A> {
-        // TODO(b/138993381): By launching custom activities we are losing control over what content is
-        //  already there. This is issue in case the user already set some compose content and decides
-        //  to set it again via our API. In such case we won't be able to dispose the old composition.
-        //  Other option would be to provide a smaller interface that does not expose these methods.
-        return createAndroidComposeRule(A::class.java)
-    }
+    // TODO(b/138993381): By launching custom activities we are losing control over what content is
+    //  already there. This is issue in case the user already set some compose content and decides
+    //  to set it again via our API. In such case we won't be able to dispose the old composition.
+    //  Other option would be to provide a smaller interface that does not expose these methods.
+    return createAndroidComposeRule(A::class.java)
+}
+
+/**
+ * Factory method to provide android specific implementation of [createComposeRule], for a given
+ * activity class type [A].
+ *
+ * This method is useful for tests that require a custom Activity. This is usually the case for
+ * tests where the compose content is set by that Activity, instead of via the test rule's
+ * [setContent][ComposeContentTestRule.setContent]. Make sure that you add the provided activity
+ * into your app's manifest file (usually in main/AndroidManifest.xml).
+ *
+ * This creates a test rule that is using [ActivityScenarioRule] as the activity launcher. If you
+ * would like to use a different one you can create [AndroidComposeTestRule] directly and supply
+ * it with your own launcher.
+ *
+ * If your test doesn't require a specific Activity, use [createComposeRule] instead.
+ */
+@ExperimentalTestApi
+inline fun <reified A : ComponentActivity> createAndroidComposeRule(
+    effectContext: CoroutineContext = EmptyCoroutineContext
+): AndroidComposeTestRule<ActivityScenarioRule<A>, A> {
+    // TODO(b/138993381): By launching custom activities we are losing control over what content is
+    //  already there. This is issue in case the user already set some compose content and decides
+    //  to set it again via our API. In such case we won't be able to dispose the old composition.
+    //  Other option would be to provide a smaller interface that does not expose these methods.
+    return createAndroidComposeRule(A::class.java, effectContext)
+}
 
 /**
  * Factory method to provide android specific implementation of [createComposeRule], for a given
@@ -81,6 +117,31 @@
 )
 
 /**
+ * Factory method to provide android specific implementation of [createComposeRule], for a given
+ * [activityClass].
+ *
+ * This method is useful for tests that require a custom Activity. This is usually the case for
+ * tests where the compose content is set by that Activity, instead of via the test rule's
+ * [setContent][ComposeContentTestRule.setContent]. Make sure that you add the provided activity
+ * into your app's manifest file (usually in main/AndroidManifest.xml).
+ *
+ * This creates a test rule that is using [ActivityScenarioRule] as the activity launcher. If you
+ * would like to use a different one you can create [AndroidComposeTestRule] directly and supply
+ * it with your own launcher.
+ *
+ * If your test doesn't require a specific Activity, use [createComposeRule] instead.
+ */
+@ExperimentalTestApi
+fun <A : ComponentActivity> createAndroidComposeRule(
+    activityClass: Class<A>,
+    effectContext: CoroutineContext = EmptyCoroutineContext
+): AndroidComposeTestRule<ActivityScenarioRule<A>, A> = AndroidComposeTestRule(
+    activityRule = ActivityScenarioRule(activityClass),
+    activityProvider = ::getActivityFromTestRule,
+    effectContext = effectContext
+)
+
+/**
  * Factory method to provide an implementation of [ComposeTestRule] that doesn't create a compose
  * host for you in which you can set content.
  *
@@ -103,6 +164,35 @@
         }
     )
 
+/**
+ * Factory method to provide an implementation of [ComposeTestRule] that doesn't create a compose
+ * host for you in which you can set content.
+ *
+ * This method is useful for tests that need to create their own compose host during the test.
+ * The returned test rule will not create a host, and consequently does not provide a
+ * `setContent` method. To set content in tests using this rule, use the appropriate `setContent`
+ * methods from your compose host.
+ *
+ * A typical use case on Android is when the test needs to launch an Activity (the compose host)
+ * after one or more dependencies have been injected.
+ *
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ */
+@ExperimentalTestApi
+fun createEmptyComposeRule(
+    effectContext: CoroutineContext = EmptyCoroutineContext
+): ComposeTestRule = AndroidComposeTestRule<TestRule, ComponentActivity>(
+    activityRule = TestRule { base, _ -> base },
+    effectContext = effectContext,
+    activityProvider = {
+        error(
+            "createEmptyComposeRule() does not provide an Activity to set Compose content in." +
+                " Launch and use the Activity yourself, or use createAndroidComposeRule()."
+        )
+    }
+)
+
 @OptIn(ExperimentalTestApi::class)
 class AndroidComposeTestRule<R : TestRule, A : ComponentActivity> private constructor(
     val activityRule: R,
@@ -128,9 +218,43 @@
      * @param activityRule Test rule to use to launch the Activity.
      * @param activityProvider Function to retrieve the Activity from the given [activityRule].
      */
-    constructor(activityRule: R, activityProvider: (R) -> A) : this(
+    constructor(
+        activityRule: R,
+        activityProvider: (R) -> A
+    ) : this(
+        activityRule = activityRule,
+        effectContext = EmptyCoroutineContext,
+        activityProvider = activityProvider,
+    )
+
+    /**
+     * Android specific implementation of [ComposeContentTestRule], where compose content is hosted
+     * by an Activity.
+     *
+     * The Activity is normally launched by the given [activityRule] before the test starts, but it
+     * is possible to pass a test rule that chooses to launch an Activity on a later time. The
+     * Activity is retrieved from the [activityRule] by means of the [activityProvider], which can be
+     * thought of as a getter for the Activity on the [activityRule]. If you use an [activityRule]
+     * that launches an Activity on a later time, you should make sure that the Activity is launched
+     * by the time or while the [activityProvider] is called.
+     *
+     * The [AndroidComposeTestRule] wraps around the given [activityRule] to make sure the Activity
+     * is launched _after_ the [AndroidComposeTestRule] has completed all necessary steps to control
+     * and monitor the compose content.
+     *
+     * @param activityRule Test rule to use to launch the Activity.
+     * @param effectContext The [CoroutineContext] used to run the composition. The context for
+     * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+     * @param activityProvider Function to retrieve the Activity from the given [activityRule].
+     */
+    @ExperimentalTestApi
+    constructor(
+        activityRule: R,
+        effectContext: CoroutineContext = EmptyCoroutineContext,
+        activityProvider: (R) -> A,
+    ) : this(
         activityRule,
-        AndroidComposeUiTestEnvironment { activityProvider(activityRule) }
+        AndroidComposeUiTestEnvironment(effectContext) { activityProvider(activityRule) },
     )
 
     /**
@@ -181,6 +305,22 @@
     override fun waitUntil(timeoutMillis: Long, condition: () -> Boolean) =
         composeTest.waitUntil(timeoutMillis, condition)
 
+    @ExperimentalTestApi
+    override fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeoutMillis: Long) =
+        composeTest.waitUntilNodeCount(matcher, count, timeoutMillis)
+
+    @ExperimentalTestApi
+    override fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeoutMillis: Long) =
+        composeTest.waitUntilAtLeastOneExists(matcher, timeoutMillis)
+
+    @ExperimentalTestApi
+    override fun waitUntilExactlyOneExists(matcher: SemanticsMatcher, timeoutMillis: Long) =
+        composeTest.waitUntilExactlyOneExists(matcher, timeoutMillis)
+
+    @ExperimentalTestApi
+    override fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeoutMillis: Long) =
+        composeTest.waitUntilDoesNotExist(matcher, timeoutMillis)
+
     override fun registerIdlingResource(idlingResource: IdlingResource) =
         composeTest.registerIdlingResource(idlingResource)
 
diff --git a/compose/ui/ui-test-junit4/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt b/compose/ui/ui-test-junit4/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
index 7fd77b0..3414dab 100644
--- a/compose/ui/ui-test-junit4/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
+++ b/compose/ui/ui-test-junit4/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
@@ -18,6 +18,8 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.unit.Density
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
 
 /**
  * Sets up the test environment, runs the given [test][block] and then tears down the test
@@ -34,9 +36,16 @@
  * launch the host from within the test lambda as well depends on the platform.
  *
  * Keeping a reference to the [ComposeUiTest] outside of this function is an error.
+ *
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ * @param block The test function.
  */
 @ExperimentalTestApi
-expect fun runComposeUiTest(block: ComposeUiTest.() -> Unit)
+expect fun runComposeUiTest(
+    effectContext: CoroutineContext = EmptyCoroutineContext,
+    block: ComposeUiTest.() -> Unit
+)
 
 /**
  * A test environment that allows you to test and control composables, either in isolation or in
@@ -152,8 +161,8 @@
      * @param condition Condition that must be satisfied in order for this method to successfully
      * finish.
      *
-     * @throws ComposeTimeoutException If the condition is not satisfied after [timeoutMillis]
-     * (in wall clock time).
+     * @throws androidx.compose.ui.test.ComposeTimeoutException If the condition is not satisfied
+     * after [timeoutMillis] (in wall clock time).
      */
     fun waitUntil(timeoutMillis: Long = 1_000, condition: () -> Boolean)
 
@@ -177,4 +186,86 @@
     fun setContent(composable: @Composable () -> Unit)
 }
 
+/**
+ * Blocks until the number of nodes matching the given [matcher] is equal to the given [count].
+ *
+ * @see ComposeUiTest.waitUntil
+ *
+ * @param matcher The matcher that will be used to filter nodes.
+ * @param count The number of nodes that are expected to
+ * @param timeoutMillis The time after which this method throws an exception if the number of nodes
+ * that match the [matcher] is not [count]. This observes wall clock time, not frame time.
+ *
+ * @throws androidx.compose.ui.test.ComposeTimeoutException If the number of nodes that match the
+ * [matcher] is not [count] after [timeoutMillis] (in wall clock time).
+ */
+@ExperimentalTestApi
+fun ComposeUiTest.waitUntilNodeCount(
+    matcher: SemanticsMatcher,
+    count: Int,
+    timeoutMillis: Long = 1_000L
+) {
+    waitUntil(timeoutMillis) {
+        onAllNodes(matcher).fetchSemanticsNodes().size == count
+    }
+}
+
+/**
+ * Blocks until at least one node matches the given [matcher].
+ *
+ * @see ComposeUiTest.waitUntil
+ *
+ * @param matcher The matcher that will be used to filter nodes.
+ * @param timeoutMillis The time after which this method throws an exception if no nodes match the
+ * given [matcher]. This observes wall clock time, not frame time.
+ *
+ * @throws androidx.compose.ui.test.ComposeTimeoutException If no nodes match the given [matcher]
+ * after [timeoutMillis] (in wall clock time).
+ */
+@ExperimentalTestApi
+fun ComposeUiTest.waitUntilAtLeastOneExists(
+    matcher: SemanticsMatcher,
+    timeoutMillis: Long = 1_000L
+) {
+    waitUntil(timeoutMillis) {
+        onAllNodes(matcher).fetchSemanticsNodes().isNotEmpty()
+    }
+}
+
+/**
+ * Blocks until exactly one node matches the given [matcher].
+ *
+ * @see ComposeUiTest.waitUntil
+ *
+ * @param matcher The matcher that will be used to filter nodes.
+ * @param timeoutMillis The time after which this method throws an exception if exactly one node
+ * does not match the given [matcher]. This observes wall clock time, not frame time.
+ *
+ * @throws androidx.compose.ui.test.ComposeTimeoutException If exactly one node does not match the
+ * given [matcher] after [timeoutMillis] (in wall clock time).
+ */
+@ExperimentalTestApi
+fun ComposeUiTest.waitUntilExactlyOneExists(
+    matcher: SemanticsMatcher,
+    timeoutMillis: Long = 1_000L
+) = waitUntilNodeCount(matcher, 1, timeoutMillis)
+
+/**
+ * Blocks until no nodes match the given [matcher].
+ *
+ * @see ComposeUiTest.waitUntil
+ *
+ * @param matcher The matcher that will be used to filter nodes.
+ * @param timeoutMillis The time after which this method throws an exception if any nodes match
+ * the given [matcher]. This observes wall clock time, not frame time.
+ *
+ * @throws androidx.compose.ui.test.ComposeTimeoutException If any nodes match the given [matcher]
+ * after [timeoutMillis] (in wall clock time).
+ */
+@ExperimentalTestApi
+fun ComposeUiTest.waitUntilDoesNotExist(
+    matcher: SemanticsMatcher,
+    timeoutMillis: Long = 1_000L
+) = waitUntilNodeCount(matcher, 0, timeoutMillis)
+
 internal const val NanoSecondsPerMilliSecond = 1_000_000L
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt
index 84471eb..159e7d1 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt
@@ -30,6 +30,8 @@
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -40,14 +42,20 @@
 
 @ExperimentalTestApi
 @OptIn(InternalTestApi::class)
-actual fun runComposeUiTest(block: ComposeUiTest.() -> Unit) {
-    DesktopComposeUiTest().runTest(block)
+actual fun runComposeUiTest(effectContext: CoroutineContext, block: ComposeUiTest.() -> Unit) {
+    DesktopComposeUiTest(effectContext).runTest(block)
 }
 
+/**
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ */
 @InternalTestApi
 @ExperimentalTestApi
 @OptIn(ExperimentalComposeUiApi::class, ExperimentalCoroutinesApi::class)
-class DesktopComposeUiTest : ComposeUiTest {
+class DesktopComposeUiTest(
+    effectContext: CoroutineContext = EmptyCoroutineContext
+) : ComposeUiTest {
 
     override val density = Density(1f, 1f)
 
@@ -65,7 +73,11 @@
         }
     }
     private val coroutineContext =
-        coroutineDispatcher + uncaughtExceptionHandler + infiniteAnimationPolicy
+        effectContext +
+            coroutineDispatcher +
+            uncaughtExceptionHandler +
+            infiniteAnimationPolicy
+
     private val surface = Surface.makeRasterN32Premul(1024, 768)
 
     lateinit var scene: ComposeScene
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt
index e9933a0..9fd4197 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt
@@ -26,13 +26,24 @@
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.SemanticsNodeInteractionCollection
+import androidx.compose.ui.test.waitUntilAtLeastOneExists
+import androidx.compose.ui.test.waitUntilDoesNotExist
+import androidx.compose.ui.test.waitUntilExactlyOneExists
+import androidx.compose.ui.test.waitUntilNodeCount
 import androidx.compose.ui.unit.Density
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
 
 @OptIn(InternalTestApi::class)
 actual fun createComposeRule(): ComposeContentTestRule = DesktopComposeTestRule()
 
+@ExperimentalTestApi
+@OptIn(InternalTestApi::class)
+actual fun createComposeRule(effectContext: CoroutineContext): ComposeContentTestRule =
+    DesktopComposeTestRule(effectContext)
+
 @InternalTestApi
 @OptIn(ExperimentalTestApi::class)
 class DesktopComposeTestRule private constructor(
@@ -41,6 +52,11 @@
 
     constructor() : this(DesktopComposeUiTest())
 
+    @ExperimentalTestApi
+    constructor(
+        effectContext: CoroutineContext = EmptyCoroutineContext
+    ) : this(DesktopComposeUiTest(effectContext))
+
     var scene: ComposeScene
         get() = composeTest.scene
         set(value) {
@@ -77,6 +93,22 @@
     override fun waitUntil(timeoutMillis: Long, condition: () -> Boolean) =
         composeTest.waitUntil(timeoutMillis, condition)
 
+    @ExperimentalTestApi
+    override fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeoutMillis: Long) =
+        composeTest.waitUntilNodeCount(matcher, count, timeoutMillis)
+
+    @ExperimentalTestApi
+    override fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeoutMillis: Long) =
+        composeTest.waitUntilAtLeastOneExists(matcher, timeoutMillis)
+
+    @ExperimentalTestApi
+    override fun waitUntilExactlyOneExists(matcher: SemanticsMatcher, timeoutMillis: Long) =
+        composeTest.waitUntilExactlyOneExists(matcher, timeoutMillis)
+
+    @ExperimentalTestApi
+    override fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeoutMillis: Long) =
+        composeTest.waitUntilDoesNotExist(matcher, timeoutMillis)
+
     override fun registerIdlingResource(idlingResource: IdlingResource) =
         composeTest.registerIdlingResource(idlingResource)
 
diff --git a/compose/ui/ui-test-junit4/src/desktopTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt b/compose/ui/ui-test-junit4/src/desktopTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt
new file mode 100644
index 0000000..aef2e84
--- /dev/null
+++ b/compose/ui/ui-test-junit4/src/desktopTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test
+
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.MotionDurationScale
+import androidx.compose.ui.test.junit4.createComposeRule
+import com.google.common.truth.Truth.assertThat
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.model.Statement
+
+@RunWith(JUnit4::class)
+@OptIn(ExperimentalTestApi::class)
+class ComposeUiTestTest {
+
+    private lateinit var testDescription: Description
+
+    /**
+     * Records the current [testDescription] for tests that need to invoke the compose test rule
+     * directly.
+     */
+    @get:Rule
+    val testWatcher = object : TestWatcher() {
+        override fun starting(description: Description) {
+            testDescription = description
+        }
+    }
+
+    @Test
+    fun effectContextPropagatedToComposition_runComposeUiTest() {
+        val testElement = TestCoroutineContextElement()
+        runComposeUiTest(effectContext = testElement) {
+            lateinit var compositionScope: CoroutineScope
+            setContent {
+                compositionScope = rememberCoroutineScope()
+            }
+
+            runOnIdle {
+                val elementFromComposition =
+                    compositionScope.coroutineContext[TestCoroutineContextElement]
+                assertThat(elementFromComposition).isSameInstanceAs(testElement)
+            }
+        }
+    }
+
+    @Test
+    fun effectContextPropagatedToComposition_createComposeRule() {
+        val testElement = TestCoroutineContextElement()
+        lateinit var compositionScope: CoroutineScope
+        val rule = createComposeRule(testElement)
+        val baseStatement = object : Statement() {
+            override fun evaluate() {
+                rule.setContent {
+                    compositionScope = rememberCoroutineScope()
+                }
+                rule.waitForIdle()
+            }
+        }
+        rule.apply(baseStatement, testDescription)
+            .evaluate()
+
+        val elementFromComposition =
+            compositionScope.coroutineContext[TestCoroutineContextElement]
+        assertThat(elementFromComposition).isSameInstanceAs(testElement)
+    }
+
+    @Test
+    fun motionDurationScale_defaultValue() = runComposeUiTest {
+        var lastRecordedMotionDurationScale: Float? = null
+        setContent {
+            val context = rememberCoroutineScope().coroutineContext
+            lastRecordedMotionDurationScale = context[MotionDurationScale]?.scaleFactor
+        }
+
+        runOnIdle {
+            assertThat(lastRecordedMotionDurationScale).isNull()
+        }
+    }
+
+    @Test
+    fun motionDurationScale_propagatedToCoroutines() {
+        val motionDurationScale = object : MotionDurationScale {
+            override val scaleFactor: Float get() = 0f
+        }
+        runComposeUiTest(effectContext = motionDurationScale) {
+            var lastRecordedMotionDurationScale: Float? = null
+            setContent {
+                val context = rememberCoroutineScope().coroutineContext
+                lastRecordedMotionDurationScale = context[MotionDurationScale]?.scaleFactor
+            }
+
+            runOnIdle {
+                assertThat(lastRecordedMotionDurationScale).isEqualTo(0f)
+            }
+        }
+    }
+
+    private class TestCoroutineContextElement : CoroutineContext.Element {
+        override val key: CoroutineContext.Key<*> get() = Key
+
+        companion object Key : CoroutineContext.Key<TestCoroutineContextElement>
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt
index 54e9aa7..08dadac 100644
--- a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt
@@ -18,12 +18,15 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.test.ComposeTimeoutException
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.IdlingResource
 import androidx.compose.ui.test.MainTestClock
+import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
 import androidx.compose.ui.unit.Density
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
 import org.junit.rules.TestRule
-import kotlin.jvm.JvmDefaultWithCompatibility
 
 /**
  * A [TestRule] that allows you to test and control composables and applications using Compose.
@@ -125,11 +128,73 @@
      * @param condition Condition that must be satisfied in order for this method to successfully
      * finish.
      *
-     * @throws ComposeTimeoutException If the condition is not satisfied after [timeoutMillis].
+     * @throws androidx.compose.ui.test.ComposeTimeoutException If the condition is not satisfied
+     * after [timeoutMillis].
      */
     fun waitUntil(timeoutMillis: Long = 1_000, condition: () -> Boolean)
 
     /**
+     * Blocks until the number of nodes matching the given [matcher] is equal to the given [count].
+     *
+     * @see ComposeTestRule.waitUntil
+     *
+     * @param matcher The matcher that will be used to filter nodes.
+     * @param count The number of nodes that are expected to
+     * @param timeoutMillis The time after which this method throws an exception if the number of
+     * nodes that match the [matcher] is not [count]. This observes wall clock time, not frame time.
+     *
+     * @throws androidx.compose.ui.test.ComposeTimeoutException If the number of nodes that match
+     * the [matcher] is not [count] after [timeoutMillis] (in wall clock time).
+     */
+    @ExperimentalTestApi
+    fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeoutMillis: Long = 1_000L)
+
+    /**
+     * Blocks until at least one node matches the given [matcher].
+     *
+     * @see ComposeTestRule.waitUntil
+     *
+     * @param matcher The matcher that will be used to filter nodes.
+     * @param timeoutMillis The time after which this method throws an exception if no nodes match
+     * the given [matcher]. This observes wall clock time, not frame time.
+     *
+     * @throws androidx.compose.ui.test.ComposeTimeoutException If no nodes match the given
+     * [matcher] after [timeoutMillis] (in wall clock time).
+     */
+    @ExperimentalTestApi
+    fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeoutMillis: Long = 1_000L)
+
+    /**
+     * Blocks until exactly one node matches the given [matcher].
+     *
+     * @see ComposeTestRule.waitUntil
+     *
+     * @param matcher The matcher that will be used to filter nodes.
+     * @param timeoutMillis The time after which this method throws an exception if exactly one node
+     * does not match the given [matcher]. This observes wall clock time, not frame time.
+     *
+     * @throws androidx.compose.ui.test.ComposeTimeoutException If exactly one node does not match
+     * the given [matcher] after [timeoutMillis] (in wall clock time).
+     */
+    @ExperimentalTestApi
+    fun waitUntilExactlyOneExists(matcher: SemanticsMatcher, timeoutMillis: Long = 1_000L)
+
+    /**
+     * Blocks until no nodes match the given [matcher].
+     *
+     * @see ComposeTestRule.waitUntil
+     *
+     * @param matcher The matcher that will be used to filter nodes.
+     * @param timeoutMillis The time after which this method throws an exception if any nodes match
+     * the given [matcher]. This observes wall clock time, not frame time.
+     *
+     * @throws androidx.compose.ui.test.ComposeTimeoutException If any nodes match the given
+     * [matcher] after [timeoutMillis] (in wall clock time).
+     */
+    @ExperimentalTestApi
+    fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeoutMillis: Long = 1_000L)
+
+    /**
      * Registers an [IdlingResource] in this test.
      */
     fun registerIdlingResource(idlingResource: IdlingResource)
@@ -179,3 +244,24 @@
  * launched, see [createAndroidComposeRule].
  */
 expect fun createComposeRule(): ComposeContentTestRule
+
+/**
+ * Factory method to provide an implementation of [ComposeContentTestRule].
+ *
+ * This method is useful for tests in compose libraries where it is irrelevant where the compose
+ * content is hosted (e.g. an Activity on Android). Such tests typically set compose content
+ * themselves via [setContent][ComposeContentTestRule.setContent] and only instrument and assert
+ * that content.
+ *
+ * For Android this will use the default Activity (android.app.Activity). You need to add a
+ * reference to this activity into the manifest file of the corresponding tests (usually in
+ * androidTest/AndroidManifest.xml). If your Android test requires a specific Activity to be
+ * launched, see [createAndroidComposeRule].
+ *
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ */
+@ExperimentalTestApi
+expect fun createComposeRule(
+    effectContext: CoroutineContext = EmptyCoroutineContext
+): ComposeContentTestRule
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-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/BitmapCapturingTest.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/BitmapCapturingTest.kt
index 9f03269..df7537a 100644
--- a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/BitmapCapturingTest.kt
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/BitmapCapturingTest.kt
@@ -47,6 +47,7 @@
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
 import kotlin.math.roundToInt
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -138,6 +139,7 @@
             .assertContainsColor(Color.Red)
     }
 
+    @Ignore // b/266737024
     @Test
     fun capturePopup_verifyBackground() {
         setContent {
diff --git a/compose/ui/ui-text-google-fonts/api/current.txt b/compose/ui/ui-text-google-fonts/api/current.txt
index 7ac97ea..5bcabac 100644
--- a/compose/ui/ui-text-google-fonts/api/current.txt
+++ b/compose/ui/ui-text-google-fonts/api/current.txt
@@ -15,12 +15,6 @@
   public static final class GoogleFont.Provider {
     ctor public GoogleFont.Provider(String providerAuthority, String providerPackage, java.util.List<? extends java.util.List<byte[]>> certificates);
     ctor public GoogleFont.Provider(String providerAuthority, String providerPackage, @ArrayRes int certificates);
-    field public static final androidx.compose.ui.text.googlefonts.GoogleFont.Provider.Companion Companion;
-  }
-
-  public static final class GoogleFont.Provider.Companion {
-    method public android.net.Uri getAllFontsListUri();
-    property public final android.net.Uri AllFontsListUri;
   }
 
   public final class GoogleFontKt {
diff --git a/compose/ui/ui-text-google-fonts/api/public_plus_experimental_current.txt b/compose/ui/ui-text-google-fonts/api/public_plus_experimental_current.txt
index 7ac97ea..5bcabac 100644
--- a/compose/ui/ui-text-google-fonts/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text-google-fonts/api/public_plus_experimental_current.txt
@@ -15,12 +15,6 @@
   public static final class GoogleFont.Provider {
     ctor public GoogleFont.Provider(String providerAuthority, String providerPackage, java.util.List<? extends java.util.List<byte[]>> certificates);
     ctor public GoogleFont.Provider(String providerAuthority, String providerPackage, @ArrayRes int certificates);
-    field public static final androidx.compose.ui.text.googlefonts.GoogleFont.Provider.Companion Companion;
-  }
-
-  public static final class GoogleFont.Provider.Companion {
-    method public android.net.Uri getAllFontsListUri();
-    property public final android.net.Uri AllFontsListUri;
   }
 
   public final class GoogleFontKt {
diff --git a/compose/ui/ui-text-google-fonts/api/restricted_current.txt b/compose/ui/ui-text-google-fonts/api/restricted_current.txt
index 7ac97ea..5bcabac 100644
--- a/compose/ui/ui-text-google-fonts/api/restricted_current.txt
+++ b/compose/ui/ui-text-google-fonts/api/restricted_current.txt
@@ -15,12 +15,6 @@
   public static final class GoogleFont.Provider {
     ctor public GoogleFont.Provider(String providerAuthority, String providerPackage, java.util.List<? extends java.util.List<byte[]>> certificates);
     ctor public GoogleFont.Provider(String providerAuthority, String providerPackage, @ArrayRes int certificates);
-    field public static final androidx.compose.ui.text.googlefonts.GoogleFont.Provider.Companion Companion;
-  }
-
-  public static final class GoogleFont.Provider.Companion {
-    method public android.net.Uri getAllFontsListUri();
-    property public final android.net.Uri AllFontsListUri;
   }
 
   public final class GoogleFontKt {
diff --git a/compose/ui/ui-text-google-fonts/src/main/java/androidx/compose/ui/text/googlefonts/GoogleFont.kt b/compose/ui/ui-text-google-fonts/src/main/java/androidx/compose/ui/text/googlefonts/GoogleFont.kt
index 36d7450..5af4b91 100644
--- a/compose/ui/ui-text-google-fonts/src/main/java/androidx/compose/ui/text/googlefonts/GoogleFont.kt
+++ b/compose/ui/ui-text-google-fonts/src/main/java/androidx/compose/ui/text/googlefonts/GoogleFont.kt
@@ -21,7 +21,6 @@
 
 import android.content.Context
 import android.graphics.Typeface
-import android.net.Uri
 import android.os.Handler
 import android.os.Looper
 import androidx.annotation.ArrayRes
@@ -81,6 +80,9 @@
  * To learn more about the features supported by Google Fonts, see
  * [Get Started with the Google Fonts for Android](https://developers.google.com/fonts/docs/android)
  *
+ * For a full list of fonts available on Android, see the
+ * [Google Fonts Directory For Android XML](https://fonts.gstatic.com/s/a/directory.xml).
+ *
  * @param name Name of a font on Google fonts, such as "Roboto" or "Open Sans"
  * @param bestEffort If besteffort is true and your query specifies a valid family name but the
  * requested width/weight/italic value is not supported Google Fonts will return the best match it
@@ -166,6 +168,7 @@
             if (providerAuthority != other.providerAuthority) return false
             if (providerPackage != other.providerPackage) return false
             if (certificates != other.certificates) return false
+            if (certificatesRes != other.certificatesRes) return false
 
             return true
         }
@@ -173,17 +176,10 @@
         override fun hashCode(): Int {
             var result = providerAuthority.hashCode()
             result = 31 * result + providerPackage.hashCode()
-            result = 31 * result + certificates.hashCode()
+            result = 31 * result + (certificates?.hashCode() ?: 0)
+            result = 31 * result + certificatesRes
             return result
         }
-
-        companion object {
-            /**
-             * Url with a canonical list of all Google Fonts that are currently supported on
-             * Android.
-             */
-            val AllFontsListUri: Uri = Uri.parse("https://fonts.gstatic.com/s/a/directory.xml")
-        }
     }
 }
 
@@ -368,7 +364,7 @@
         FAIL_REASON_FONT_LOAD_ERROR -> "Generic error loading font, for example variation " +
             "settings were not parsable"
         FAIL_REASON_FONT_NOT_FOUND -> "Font not found, please check availability on " +
-            "GoogleFont.Provider.AllFontsList: ${GoogleFont.Provider.AllFontsListUri}"
+            "GoogleFont.Provider.AllFontsList: https://fonts.gstatic.com/s/a/directory.xml"
         FAIL_REASON_FONT_UNAVAILABLE -> "The provider found the queried font, but it is " +
             "currently unavailable."
         FAIL_REASON_MALFORMED_QUERY -> "The given query was not supported by this provider."
diff --git a/compose/ui/ui-text/api/current.ignore b/compose/ui/ui-text/api/current.ignore
index a4b016c..24501fb 100644
--- a/compose/ui/ui-text/api/current.ignore
+++ b/compose/ui/ui-text/api/current.ignore
@@ -1,13 +1,3 @@
 // Baseline format: 1.0
 ChangedType: androidx.compose.ui.text.AnnotatedString.Builder#append(char):
     Method androidx.compose.ui.text.AnnotatedString.Builder.append has changed return type from void to androidx.compose.ui.text.AnnotatedString.Builder
-
-
-InvalidNullConversion: androidx.compose.ui.text.TextStyle#TextStyle(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.text.style.TextAlign, androidx.compose.ui.text.style.TextDirection, long, androidx.compose.ui.text.style.TextIndent) parameter #6:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter fontFeatureSettings in androidx.compose.ui.text.TextStyle(long color, long fontSize, androidx.compose.ui.text.font.FontWeight fontWeight, androidx.compose.ui.text.font.FontStyle fontStyle, androidx.compose.ui.text.font.FontSynthesis fontSynthesis, androidx.compose.ui.text.font.FontFamily fontFamily, String fontFeatureSettings, long letterSpacing, androidx.compose.ui.text.style.BaselineShift baselineShift, androidx.compose.ui.text.style.TextGeometricTransform textGeometricTransform, androidx.compose.ui.text.intl.LocaleList localeList, long background, androidx.compose.ui.text.style.TextDecoration textDecoration, androidx.compose.ui.graphics.Shadow shadow, androidx.compose.ui.text.style.TextAlign textAlign, androidx.compose.ui.text.style.TextDirection textDirection, long lineHeight, androidx.compose.ui.text.style.TextIndent textIndent)
-InvalidNullConversion: androidx.compose.ui.text.TextStyle#TextStyle(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.text.style.TextAlign, androidx.compose.ui.text.style.TextDirection, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle) parameter #6:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter fontFeatureSettings in androidx.compose.ui.text.TextStyle(long color, long fontSize, androidx.compose.ui.text.font.FontWeight fontWeight, androidx.compose.ui.text.font.FontStyle fontStyle, androidx.compose.ui.text.font.FontSynthesis fontSynthesis, androidx.compose.ui.text.font.FontFamily fontFamily, String fontFeatureSettings, long letterSpacing, androidx.compose.ui.text.style.BaselineShift baselineShift, androidx.compose.ui.text.style.TextGeometricTransform textGeometricTransform, androidx.compose.ui.text.intl.LocaleList localeList, long background, androidx.compose.ui.text.style.TextDecoration textDecoration, androidx.compose.ui.graphics.Shadow shadow, androidx.compose.ui.text.style.TextAlign textAlign, androidx.compose.ui.text.style.TextDirection textDirection, long lineHeight, androidx.compose.ui.text.style.TextIndent textIndent, androidx.compose.ui.text.PlatformTextStyle platformStyle, androidx.compose.ui.text.style.LineHeightStyle lineHeightStyle)
-InvalidNullConversion: androidx.compose.ui.text.TextStyle#copy(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.text.style.TextAlign, androidx.compose.ui.text.style.TextDirection, long, androidx.compose.ui.text.style.TextIndent) parameter #6:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter fontFeatureSettings in androidx.compose.ui.text.TextStyle.copy(long color, long fontSize, androidx.compose.ui.text.font.FontWeight fontWeight, androidx.compose.ui.text.font.FontStyle fontStyle, androidx.compose.ui.text.font.FontSynthesis fontSynthesis, androidx.compose.ui.text.font.FontFamily fontFamily, String fontFeatureSettings, long letterSpacing, androidx.compose.ui.text.style.BaselineShift baselineShift, androidx.compose.ui.text.style.TextGeometricTransform textGeometricTransform, androidx.compose.ui.text.intl.LocaleList localeList, long background, androidx.compose.ui.text.style.TextDecoration textDecoration, androidx.compose.ui.graphics.Shadow shadow, androidx.compose.ui.text.style.TextAlign textAlign, androidx.compose.ui.text.style.TextDirection textDirection, long lineHeight, androidx.compose.ui.text.style.TextIndent textIndent)
-InvalidNullConversion: androidx.compose.ui.text.TextStyle#copy(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.text.style.TextAlign, androidx.compose.ui.text.style.TextDirection, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle) parameter #6:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter fontFeatureSettings in androidx.compose.ui.text.TextStyle.copy(long color, long fontSize, androidx.compose.ui.text.font.FontWeight fontWeight, androidx.compose.ui.text.font.FontStyle fontStyle, androidx.compose.ui.text.font.FontSynthesis fontSynthesis, androidx.compose.ui.text.font.FontFamily fontFamily, String fontFeatureSettings, long letterSpacing, androidx.compose.ui.text.style.BaselineShift baselineShift, androidx.compose.ui.text.style.TextGeometricTransform textGeometricTransform, androidx.compose.ui.text.intl.LocaleList localeList, long background, androidx.compose.ui.text.style.TextDecoration textDecoration, androidx.compose.ui.graphics.Shadow shadow, androidx.compose.ui.text.style.TextAlign textAlign, androidx.compose.ui.text.style.TextDirection textDirection, long lineHeight, androidx.compose.ui.text.style.TextIndent textIndent, androidx.compose.ui.text.PlatformTextStyle platformStyle, androidx.compose.ui.text.style.LineHeightStyle lineHeightStyle)
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 2c75232..d2dd899 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -307,7 +307,6 @@
     method public int getEmojiSupportMatch();
     method @Deprecated public boolean getIncludeFontPadding();
     method public androidx.compose.ui.text.PlatformParagraphStyle merge(androidx.compose.ui.text.PlatformParagraphStyle? other);
-    method public void setEmojiSupportMatch(int);
     property public final int emojiSupportMatch;
     property @Deprecated public final boolean includeFontPadding;
     field public static final androidx.compose.ui.text.PlatformParagraphStyle.Companion Companion;
@@ -514,11 +513,11 @@
 
   @androidx.compose.runtime.Immutable public final class TextStyle {
     ctor public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak, optional androidx.compose.ui.text.style.Hyphens? hyphens);
-    ctor @Deprecated public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
-    ctor @Deprecated public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    ctor @Deprecated public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
+    ctor @Deprecated public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     method public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak, optional androidx.compose.ui.text.style.Hyphens? hyphens);
-    method @Deprecated public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
-    method @Deprecated public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    method @Deprecated public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
+    method @Deprecated public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     method public long getBackground();
     method public androidx.compose.ui.text.style.BaselineShift? getBaselineShift();
     method public long getColor();
@@ -711,11 +710,11 @@
     property public abstract int style;
     property public abstract androidx.compose.ui.text.font.FontWeight weight;
     field public static final androidx.compose.ui.text.font.Font.Companion Companion;
-    field public static final long MaximumAsyncTimeout = 15000L; // 0x3a98L
+    field public static final long MaximumAsyncTimeoutMillis = 15000L; // 0x3a98L
   }
 
   public static final class Font.Companion {
-    field public static final long MaximumAsyncTimeout = 15000L; // 0x3a98L
+    field public static final long MaximumAsyncTimeoutMillis = 15000L; // 0x3a98L
   }
 
   @Deprecated public static interface Font.ResourceLoader {
@@ -1358,15 +1357,15 @@
     method @androidx.compose.runtime.Stable public static float lerp(float start, float stop, float fraction);
   }
 
-  public final class Hyphens {
+  @kotlin.jvm.JvmInline public final value class Hyphens {
     field public static final androidx.compose.ui.text.style.Hyphens.Companion Companion;
   }
 
   public static final class Hyphens.Companion {
-    method public androidx.compose.ui.text.style.Hyphens getAuto();
-    method public androidx.compose.ui.text.style.Hyphens getNone();
-    property public final androidx.compose.ui.text.style.Hyphens Auto;
-    property public final androidx.compose.ui.text.style.Hyphens None;
+    method public int getAuto();
+    method public int getNone();
+    property public final int Auto;
+    property public final int None;
   }
 
   @androidx.compose.runtime.Immutable public final class LineBreak {
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index 5c39454..c3c178e 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -328,7 +328,6 @@
     method public int getEmojiSupportMatch();
     method @Deprecated public boolean getIncludeFontPadding();
     method public androidx.compose.ui.text.PlatformParagraphStyle merge(androidx.compose.ui.text.PlatformParagraphStyle? other);
-    method public void setEmojiSupportMatch(int);
     property public final int emojiSupportMatch;
     property @Deprecated public final boolean includeFontPadding;
     field public static final androidx.compose.ui.text.PlatformParagraphStyle.Companion Companion;
@@ -557,13 +556,13 @@
     ctor public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak, optional androidx.compose.ui.text.style.Hyphens? hyphens);
     ctor @androidx.compose.ui.text.ExperimentalTextApi public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.graphics.drawscope.DrawStyle? drawStyle, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak, optional androidx.compose.ui.text.style.Hyphens? hyphens, optional androidx.compose.ui.text.style.TextMotion? textMotion);
     ctor @androidx.compose.ui.text.ExperimentalTextApi public TextStyle(androidx.compose.ui.graphics.Brush? brush, optional float alpha, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.graphics.drawscope.DrawStyle? drawStyle, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak, optional androidx.compose.ui.text.style.Hyphens? hyphens, optional androidx.compose.ui.text.style.TextMotion? textMotion);
-    ctor @Deprecated public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
-    ctor @Deprecated public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    ctor @Deprecated public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
+    ctor @Deprecated public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     method public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak, optional androidx.compose.ui.text.style.Hyphens? hyphens);
     method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.graphics.drawscope.DrawStyle? drawStyle, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak, optional androidx.compose.ui.text.style.Hyphens? hyphens, optional androidx.compose.ui.text.style.TextMotion? textMotion);
     method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.TextStyle copy(androidx.compose.ui.graphics.Brush? brush, optional float alpha, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.graphics.drawscope.DrawStyle? drawStyle, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak, optional androidx.compose.ui.text.style.Hyphens? hyphens, optional androidx.compose.ui.text.style.TextMotion? textMotion);
-    method @Deprecated public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
-    method @Deprecated public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    method @Deprecated public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
+    method @Deprecated public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     method @androidx.compose.ui.text.ExperimentalTextApi public float getAlpha();
     method public long getBackground();
     method public androidx.compose.ui.text.style.BaselineShift? getBaselineShift();
@@ -773,11 +772,11 @@
     property public abstract int style;
     property public abstract androidx.compose.ui.text.font.FontWeight weight;
     field public static final androidx.compose.ui.text.font.Font.Companion Companion;
-    field public static final long MaximumAsyncTimeout = 15000L; // 0x3a98L
+    field public static final long MaximumAsyncTimeoutMillis = 15000L; // 0x3a98L
   }
 
   public static final class Font.Companion {
-    field public static final long MaximumAsyncTimeout = 15000L; // 0x3a98L
+    field public static final long MaximumAsyncTimeoutMillis = 15000L; // 0x3a98L
   }
 
   @Deprecated public static interface Font.ResourceLoader {
@@ -1426,15 +1425,15 @@
     method @androidx.compose.runtime.Stable public static float lerp(float start, float stop, float fraction);
   }
 
-  public final class Hyphens {
+  @kotlin.jvm.JvmInline public final value class Hyphens {
     field public static final androidx.compose.ui.text.style.Hyphens.Companion Companion;
   }
 
   public static final class Hyphens.Companion {
-    method public androidx.compose.ui.text.style.Hyphens getAuto();
-    method public androidx.compose.ui.text.style.Hyphens getNone();
-    property public final androidx.compose.ui.text.style.Hyphens Auto;
-    property public final androidx.compose.ui.text.style.Hyphens None;
+    method public int getAuto();
+    method public int getNone();
+    property public final int Auto;
+    property public final int None;
   }
 
   @androidx.compose.runtime.Immutable public final class LineBreak {
diff --git a/compose/ui/ui-text/api/restricted_current.ignore b/compose/ui/ui-text/api/restricted_current.ignore
index a4b016c..24501fb 100644
--- a/compose/ui/ui-text/api/restricted_current.ignore
+++ b/compose/ui/ui-text/api/restricted_current.ignore
@@ -1,13 +1,3 @@
 // Baseline format: 1.0
 ChangedType: androidx.compose.ui.text.AnnotatedString.Builder#append(char):
     Method androidx.compose.ui.text.AnnotatedString.Builder.append has changed return type from void to androidx.compose.ui.text.AnnotatedString.Builder
-
-
-InvalidNullConversion: androidx.compose.ui.text.TextStyle#TextStyle(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.text.style.TextAlign, androidx.compose.ui.text.style.TextDirection, long, androidx.compose.ui.text.style.TextIndent) parameter #6:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter fontFeatureSettings in androidx.compose.ui.text.TextStyle(long color, long fontSize, androidx.compose.ui.text.font.FontWeight fontWeight, androidx.compose.ui.text.font.FontStyle fontStyle, androidx.compose.ui.text.font.FontSynthesis fontSynthesis, androidx.compose.ui.text.font.FontFamily fontFamily, String fontFeatureSettings, long letterSpacing, androidx.compose.ui.text.style.BaselineShift baselineShift, androidx.compose.ui.text.style.TextGeometricTransform textGeometricTransform, androidx.compose.ui.text.intl.LocaleList localeList, long background, androidx.compose.ui.text.style.TextDecoration textDecoration, androidx.compose.ui.graphics.Shadow shadow, androidx.compose.ui.text.style.TextAlign textAlign, androidx.compose.ui.text.style.TextDirection textDirection, long lineHeight, androidx.compose.ui.text.style.TextIndent textIndent)
-InvalidNullConversion: androidx.compose.ui.text.TextStyle#TextStyle(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.text.style.TextAlign, androidx.compose.ui.text.style.TextDirection, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle) parameter #6:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter fontFeatureSettings in androidx.compose.ui.text.TextStyle(long color, long fontSize, androidx.compose.ui.text.font.FontWeight fontWeight, androidx.compose.ui.text.font.FontStyle fontStyle, androidx.compose.ui.text.font.FontSynthesis fontSynthesis, androidx.compose.ui.text.font.FontFamily fontFamily, String fontFeatureSettings, long letterSpacing, androidx.compose.ui.text.style.BaselineShift baselineShift, androidx.compose.ui.text.style.TextGeometricTransform textGeometricTransform, androidx.compose.ui.text.intl.LocaleList localeList, long background, androidx.compose.ui.text.style.TextDecoration textDecoration, androidx.compose.ui.graphics.Shadow shadow, androidx.compose.ui.text.style.TextAlign textAlign, androidx.compose.ui.text.style.TextDirection textDirection, long lineHeight, androidx.compose.ui.text.style.TextIndent textIndent, androidx.compose.ui.text.PlatformTextStyle platformStyle, androidx.compose.ui.text.style.LineHeightStyle lineHeightStyle)
-InvalidNullConversion: androidx.compose.ui.text.TextStyle#copy(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.text.style.TextAlign, androidx.compose.ui.text.style.TextDirection, long, androidx.compose.ui.text.style.TextIndent) parameter #6:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter fontFeatureSettings in androidx.compose.ui.text.TextStyle.copy(long color, long fontSize, androidx.compose.ui.text.font.FontWeight fontWeight, androidx.compose.ui.text.font.FontStyle fontStyle, androidx.compose.ui.text.font.FontSynthesis fontSynthesis, androidx.compose.ui.text.font.FontFamily fontFamily, String fontFeatureSettings, long letterSpacing, androidx.compose.ui.text.style.BaselineShift baselineShift, androidx.compose.ui.text.style.TextGeometricTransform textGeometricTransform, androidx.compose.ui.text.intl.LocaleList localeList, long background, androidx.compose.ui.text.style.TextDecoration textDecoration, androidx.compose.ui.graphics.Shadow shadow, androidx.compose.ui.text.style.TextAlign textAlign, androidx.compose.ui.text.style.TextDirection textDirection, long lineHeight, androidx.compose.ui.text.style.TextIndent textIndent)
-InvalidNullConversion: androidx.compose.ui.text.TextStyle#copy(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.text.style.TextAlign, androidx.compose.ui.text.style.TextDirection, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle) parameter #6:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter fontFeatureSettings in androidx.compose.ui.text.TextStyle.copy(long color, long fontSize, androidx.compose.ui.text.font.FontWeight fontWeight, androidx.compose.ui.text.font.FontStyle fontStyle, androidx.compose.ui.text.font.FontSynthesis fontSynthesis, androidx.compose.ui.text.font.FontFamily fontFamily, String fontFeatureSettings, long letterSpacing, androidx.compose.ui.text.style.BaselineShift baselineShift, androidx.compose.ui.text.style.TextGeometricTransform textGeometricTransform, androidx.compose.ui.text.intl.LocaleList localeList, long background, androidx.compose.ui.text.style.TextDecoration textDecoration, androidx.compose.ui.graphics.Shadow shadow, androidx.compose.ui.text.style.TextAlign textAlign, androidx.compose.ui.text.style.TextDirection textDirection, long lineHeight, androidx.compose.ui.text.style.TextIndent textIndent, androidx.compose.ui.text.PlatformTextStyle platformStyle, androidx.compose.ui.text.style.LineHeightStyle lineHeightStyle)
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 2c75232..d2dd899 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -307,7 +307,6 @@
     method public int getEmojiSupportMatch();
     method @Deprecated public boolean getIncludeFontPadding();
     method public androidx.compose.ui.text.PlatformParagraphStyle merge(androidx.compose.ui.text.PlatformParagraphStyle? other);
-    method public void setEmojiSupportMatch(int);
     property public final int emojiSupportMatch;
     property @Deprecated public final boolean includeFontPadding;
     field public static final androidx.compose.ui.text.PlatformParagraphStyle.Companion Companion;
@@ -514,11 +513,11 @@
 
   @androidx.compose.runtime.Immutable public final class TextStyle {
     ctor public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak, optional androidx.compose.ui.text.style.Hyphens? hyphens);
-    ctor @Deprecated public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
-    ctor @Deprecated public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    ctor @Deprecated public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
+    ctor @Deprecated public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     method public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak, optional androidx.compose.ui.text.style.Hyphens? hyphens);
-    method @Deprecated public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
-    method @Deprecated public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    method @Deprecated public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
+    method @Deprecated public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     method public long getBackground();
     method public androidx.compose.ui.text.style.BaselineShift? getBaselineShift();
     method public long getColor();
@@ -711,11 +710,11 @@
     property public abstract int style;
     property public abstract androidx.compose.ui.text.font.FontWeight weight;
     field public static final androidx.compose.ui.text.font.Font.Companion Companion;
-    field public static final long MaximumAsyncTimeout = 15000L; // 0x3a98L
+    field public static final long MaximumAsyncTimeoutMillis = 15000L; // 0x3a98L
   }
 
   public static final class Font.Companion {
-    field public static final long MaximumAsyncTimeout = 15000L; // 0x3a98L
+    field public static final long MaximumAsyncTimeoutMillis = 15000L; // 0x3a98L
   }
 
   @Deprecated public static interface Font.ResourceLoader {
@@ -1358,15 +1357,15 @@
     method @androidx.compose.runtime.Stable public static float lerp(float start, float stop, float fraction);
   }
 
-  public final class Hyphens {
+  @kotlin.jvm.JvmInline public final value class Hyphens {
     field public static final androidx.compose.ui.text.style.Hyphens.Companion Companion;
   }
 
   public static final class Hyphens.Companion {
-    method public androidx.compose.ui.text.style.Hyphens getAuto();
-    method public androidx.compose.ui.text.style.Hyphens getNone();
-    property public final androidx.compose.ui.text.style.Hyphens Auto;
-    property public final androidx.compose.ui.text.style.Hyphens None;
+    method public int getAuto();
+    method public int getNone();
+    property public final int Auto;
+    property public final int None;
   }
 
   @androidx.compose.runtime.Immutable public final class LineBreak {
diff --git a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/HyphensLineBreakBenchmark.kt b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/HyphensLineBreakBenchmark.kt
index 03466cd..7183f6c 100644
--- a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/HyphensLineBreakBenchmark.kt
+++ b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/HyphensLineBreakBenchmark.kt
@@ -39,7 +39,7 @@
 @OptIn(ExperimentalTextApi::class, InternalPlatformTextApi::class)
 class HyphensLineBreakBenchmark(
     private val textLength: Int,
-    private val hyphens: Hyphens,
+    private val hyphensWrapper: HyphensWrapper,
     private val lineBreak: LineBreak
 ) {
     companion object {
@@ -48,7 +48,7 @@
         fun initParameters(): List<Array<Any?>> {
             return cartesian(
                 arrayOf(32, 128, 512),
-                arrayOf(Hyphens.None, Hyphens.Auto),
+                arrayOf(Hyphens.None.wrap, Hyphens.Auto.wrap),
                 arrayOf(LineBreak.Paragraph, LineBreak.Simple, LineBreak.Heading)
             )
         }
@@ -62,7 +62,7 @@
 
     private val width = 100
     private val textSize: Float = 10F
-    private val hyphenationFrequency = toLayoutHyphenationFrequency(hyphens)
+    private val hyphenationFrequency = toLayoutHyphenationFrequency(hyphensWrapper.hyphens)
     private val lineBreakStyle = toLayoutLineBreakStyle(lineBreak.strictness)
     private val breakStrategy = toLayoutBreakStrategy(lineBreak.strategy)
     private val lineBreakWordStyle = toLayoutLineBreakWordStyle(lineBreak.wordBreak)
@@ -138,4 +138,13 @@
             LineBreak.WordBreak.Phrase -> LayoutCompat.LINE_BREAK_WORD_STYLE_PHRASE
             else -> LayoutCompat.LINE_BREAK_WORD_STYLE_NONE
         }
-}
\ No newline at end of file
+}
+
+/**
+ * Required to make this test work due to a bug with value classes and Parameterized JUnit tests.
+ * https://youtrack.jetbrains.com/issue/KT-35523
+ */
+data class HyphensWrapper(val hyphens: Hyphens)
+
+val Hyphens.wrap: HyphensWrapper
+    get() = HyphensWrapper(this)
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
index 20dafd0..7e76c38 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
@@ -1380,20 +1380,38 @@
     @Test
     fun testTextStyle_letterSpacingInSp_appliedAsSpan() {
         val letterSpacing = 5f
-        val text = "abc"
+        val annotatedText = buildAnnotatedString {
+            pushStyle(SpanStyle(fontWeight = FontWeight.W800))
+            append("abc")
+            pop()
+        }
         val paragraph = simpleParagraph(
-            text = text,
+            text = annotatedText.text,
+            spanStyles = annotatedText.spanStyles,
             style = TextStyle(letterSpacing = letterSpacing.sp),
             width = 0.0f
         )
 
         assertThat(paragraph.charSequence)
-            .hasSpan(LetterSpacingSpanPx::class, 0, text.length) {
+            .hasSpan(LetterSpacingSpanPx::class, 0, annotatedText.length) {
                 it.letterSpacing == letterSpacing
             }
     }
 
     @Test
+    fun testTextStyle_letterSpacingInSp_noSpan_whenNoAnnoattions() {
+        val letterSpacing = 5f
+        val annotatedText = "abc"
+        val paragraph = simpleParagraph(
+            text = annotatedText,
+            style = TextStyle(letterSpacing = letterSpacing.sp),
+            width = 0.0f
+        )
+
+        assertThat(paragraph.charSequence).doesNotHaveSpan(LetterSpacingSpanPx::class)
+    }
+
+    @Test
     fun testSpanStyle_fontFeatureSettings_appliedOnTextPaint() {
         val fontFeatureSettings = "\"kern\" 0"
         val paragraph = simpleParagraph(
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt
index ec23878..5b47875 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt
@@ -133,13 +133,7 @@
         for (line in 0 until paragraph.lineCount) {
             assertThat(paragraph.getLineRight(line)).isEqualTo(paragraph.width)
 
-            val expectedLeft = if (line == paragraph.lineCount - 1) {
-                // ellipsize does not include letter spacing
-                letterSpacing
-            } else {
-                0f
-            }
-            assertThat(paragraph.getLineLeft(line)).isEqualTo(expectedLeft)
+            assertThat(paragraph.getLineLeft(line)).isEqualTo(0f)
         }
     }
 
@@ -149,13 +143,7 @@
         for (line in 0 until paragraph.lineCount) {
             assertThat(paragraph.getLineLeft(line)).isEqualTo(0)
 
-            val expectedRight = if (line == paragraph.lineCount - 1) {
-                // ellipsize does not include letter spacing
-                paragraph.width - letterSpacing
-            } else {
-                paragraph.width
-            }
-            assertThat(paragraph.getLineRight(line)).isEqualTo(expectedRight)
+            assertThat(paragraph.getLineRight(line)).isEqualTo(paragraph.width)
         }
     }
 
@@ -170,7 +158,7 @@
             assertThat(paragraph.getLineRight(line)).isEqualTo(paragraph.width)
 
             val expectedLeft = if (line == paragraph.lineCount - 1) {
-                -fontSize
+                -fontSize - letterSpacing
             } else {
                 0f
             }
@@ -182,18 +170,13 @@
     fun getHorizontalPosition_Ltr_sp_letterspacing() {
         val paragraph = paragraph(ltrChar.repeat(repeatCount), letterSpacing = letterSpacing.sp)
         lineStartOffsets.forEach { offset ->
-            val expectedPosition = if (offset == paragraph.getLineStart(paragraph.lineCount - 1)) {
-                letterSpacing
-            } else {
-                0f
-            }
             assertThat(
                 paragraph.getHorizontalPosition(offset, usePrimaryDirection = true)
-            ).isEqualTo(expectedPosition)
+            ).isEqualTo(0f)
 
             assertThat(
                 paragraph.getHorizontalPosition(offset, usePrimaryDirection = false)
-            ).isEqualTo(expectedPosition)
+            ).isEqualTo(0f)
         }
     }
 
@@ -201,18 +184,13 @@
     fun getHorizontalPosition_Rtl_sp_letterspacing() {
         val paragraph = paragraph(rtlChar.repeat(repeatCount), letterSpacing = letterSpacing.sp)
         lineStartOffsets.forEach { offset ->
-            val expectedPosition = if (offset == paragraph.getLineStart(paragraph.lineCount - 1)) {
-                paragraph.width - letterSpacing
-            } else {
-                paragraph.width
-            }
             assertThat(
                 paragraph.getHorizontalPosition(offset, usePrimaryDirection = true)
-            ).isEqualTo(expectedPosition)
+            ).isEqualTo(paragraph.width)
 
             assertThat(
                 paragraph.getHorizontalPosition(offset, usePrimaryDirection = false)
-            ).isEqualTo(expectedPosition)
+            ).isEqualTo(paragraph.width)
         }
     }
 
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverImplPreloadTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverImplPreloadTest.kt
index fe9f249..ec16989 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverImplPreloadTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontFamilyResolverImplPreloadTest.kt
@@ -197,7 +197,7 @@
             fallbackFont
         )
         val deferred = testScope.async { subject.preload(fontFamily) }
-        testScope.advanceTimeBy(Font.MaximumAsyncTimeout)
+        testScope.advanceTimeBy(Font.MaximumAsyncTimeoutMillis)
         assertThat(deferred.isCompleted).isTrue()
         testScope.runBlockingTest {
             deferred.await() // actually throw here
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapterPreloadTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapterPreloadTest.kt
index c86dd18..f477e95 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapterPreloadTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapterPreloadTest.kt
@@ -196,7 +196,7 @@
             subject.preload(fontFamily, fontLoader)
         }
         assertThat(typefaceLoader.pendingRequests()).containsExactly(asyncFont)
-        scope.advanceTimeBy(Font.MaximumAsyncTimeout)
+        scope.advanceTimeBy(Font.MaximumAsyncTimeoutMillis)
         scope.runBlockingTest {
             preloadJob.await()
         }
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapterTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapterTest.kt
index 761092c..26eda8c 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapterTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapterTest.kt
@@ -294,7 +294,7 @@
                 assertThat(it).currentAsyncTypefaceValue(Typeface.DEFAULT)
             },
             doCompleteAsync = {
-                scope.advanceTimeBy(Font.MaximumAsyncTimeout)
+                scope.advanceTimeBy(Font.MaximumAsyncTimeoutMillis)
                 scope.runCurrent()
                 typefaceLoader.completeOne(asyncFontFallback, expected)
             }
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/matchers/CharSequenceSubject.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/matchers/CharSequenceSubject.kt
index e8a3fce..92fb82d 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/matchers/CharSequenceSubject.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/matchers/CharSequenceSubject.kt
@@ -68,6 +68,12 @@
         spans(spanClazz).has(start, end, predicate)
     }
 
+    fun <T : Any> doesNotHaveSpan(
+        spanClazz: KClass<out T>,
+    ) {
+        spans(spanClazz).isEmpty()
+    }
+
     /**
      * Similar to [hasSpan], and the returned matcher will also check that the span is not covered
      * by other spans.
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/TextPaintExtensionsTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/TextPaintExtensionsTest.kt
index 4bfa91b..70890ed 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/TextPaintExtensionsTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/TextPaintExtensionsTest.kt
@@ -40,7 +40,6 @@
 import androidx.compose.ui.unit.sp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import androidx.test.platform.app.InstrumentationRegistry
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -49,8 +48,7 @@
 @SmallTest
 class TextPaintExtensionsTest {
 
-    private val context = InstrumentationRegistry.getInstrumentation().targetContext!!
-    private val density = Density(context)
+    private val density = Density(1f) /* don't use density since it changes letterSpacing*/
     private val resolveTypeface: (FontFamily?, FontWeight, FontStyle, FontSynthesis) -> Typeface =
         { _, _, _, _ ->
             Typeface.DEFAULT
@@ -65,7 +63,7 @@
         val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
 
         assertThat(tp.textSize).isEqualTo(with(density) { fontSize.toPx() })
-        assertThat(notApplied.fontSize).isEqualTo(TextUnit.Unspecified)
+        assertThat(notApplied).isNull()
     }
 
     @Test
@@ -78,7 +76,7 @@
         val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
 
         assertThat(tp.textSize).isEqualTo(60f)
-        assertThat(notApplied.fontSize).isEqualTo(TextUnit.Unspecified)
+        assertThat(notApplied).isNull()
     }
 
     @Test
@@ -96,11 +94,24 @@
 
         assertThat(tp.textSkewX).isEqualTo(originalSkew + textGeometricTransform.skewX)
         assertThat(tp.textScaleX).isEqualTo(originalScale * textGeometricTransform.scaleX)
-        assertThat(notApplied.textGeometricTransform).isNull()
+        assertThat(notApplied?.textGeometricTransform).isNull()
     }
 
     @Test
-    fun letterSpacingSp_shouldBeLeftAsSpan() {
+    fun letterSpacingSp_shouldBeLeftAsSpan_whenSpans() {
+        val letterSpacing = 10.sp
+        val spanStyle = SpanStyle(letterSpacing = letterSpacing)
+        val tp = AndroidTextPaint(0, density.density)
+        tp.letterSpacing = 4f
+
+        val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density, true)
+
+        assertThat(tp.letterSpacing).isWithin(0.01f).of(0.8333333f)
+        assertThat(notApplied?.letterSpacing).isEqualTo(letterSpacing)
+    }
+
+    @Test
+    fun letterSpacingSp_makesNoSpan_whenNoSpans() {
         val letterSpacing = 10.sp
         val spanStyle = SpanStyle(letterSpacing = letterSpacing)
         val tp = AndroidTextPaint(0, density.density)
@@ -108,12 +119,12 @@
 
         val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
 
-        assertThat(tp.letterSpacing).isEqualTo(4f)
-        assertThat(notApplied.letterSpacing).isEqualTo(letterSpacing)
+        assertThat(tp.letterSpacing).isWithin(0.01f).of(0.8333333f)
+        assertThat(notApplied).isNull()
     }
 
     @Test
-    fun letterSpacingEm_shouldBeAppliedTo_letterSpacing() {
+    fun letterSpacingEm_shouldNotBeAppliedTo_letterSpacing() {
         val letterSpacing = 1.5.em
         val spanStyle = SpanStyle(letterSpacing = letterSpacing)
         val tp = AndroidTextPaint(0, density.density)
@@ -122,20 +133,33 @@
         val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
 
         assertThat(tp.letterSpacing).isEqualTo(1.5f)
-        assertThat(notApplied.letterSpacing).isEqualTo(TextUnit.Unspecified)
+        assertThat(notApplied).isNull()
+    }
+
+    @Test
+    fun letterSpacingEm_shouldBeAppliedTo_letterSpacing_whenSpans() {
+        val letterSpacing = 1.5.em
+        val spanStyle = SpanStyle(letterSpacing = letterSpacing)
+        val tp = AndroidTextPaint(0, density.density)
+        tp.letterSpacing = 4f
+
+        val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density, true)
+
+        assertThat(tp.letterSpacing).isEqualTo(1.5f)
+        assertThat(notApplied?.letterSpacing).isEqualTo(null)
     }
 
     @Test
     fun letterSpacingUnspecified_shouldBeNoOp() {
         val letterSpacing = TextUnit.Unspecified
-        val spanStyle = SpanStyle(letterSpacing = letterSpacing)
+        val spanStyle = SpanStyle(letterSpacing = letterSpacing, background = Color.Black)
         val tp = AndroidTextPaint(0, density.density)
         tp.letterSpacing = 4f
 
         val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
 
         assertThat(tp.letterSpacing).isEqualTo(4f)
-        assertThat(notApplied.letterSpacing).isEqualTo(TextUnit.Unspecified)
+        assertThat(notApplied?.letterSpacing).isEqualTo(TextUnit.Unspecified)
     }
 
     @Test
@@ -148,7 +172,7 @@
         val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
 
         assertThat(tp.fontFeatureSettings).isEqualTo(fontFeatureSettings)
-        assertThat(notApplied.fontFeatureSettings).isNull()
+        assertThat(notApplied?.fontFeatureSettings).isNull()
     }
 
     @Test
@@ -161,7 +185,7 @@
         val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
 
         assertThat(tp.fontFeatureSettings).isEqualTo("\"kern\" 0")
-        assertThat(notApplied.fontFeatureSettings).isNull()
+        assertThat(notApplied?.fontFeatureSettings).isNull()
     }
 
     @Test
@@ -204,10 +228,10 @@
         assertThat(calledFontStyle).isEqualTo(fontStyle)
         assertThat(calledFontSynthesis).isEqualTo(fontSynthesis)
 
-        assertThat(notApplied.fontFamily).isNull()
-        assertThat(notApplied.fontWeight).isNull()
-        assertThat(notApplied.fontStyle).isNull()
-        assertThat(notApplied.fontSynthesis).isNull()
+        assertThat(notApplied?.fontFamily).isNull()
+        assertThat(notApplied?.fontWeight).isNull()
+        assertThat(notApplied?.fontStyle).isNull()
+        assertThat(notApplied?.fontSynthesis).isNull()
     }
 
     @Test
@@ -220,7 +244,7 @@
         val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
 
         assertThat(tp.baselineShift).isEqualTo(0)
-        assertThat(notApplied.baselineShift).isEqualTo(baselineShift)
+        assertThat(notApplied?.baselineShift).isEqualTo(baselineShift)
     }
 
     @Test
@@ -233,7 +257,7 @@
         val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
 
         assertThat(tp.baselineShift).isEqualTo(0)
-        assertThat(notApplied.baselineShift).isNull()
+        assertThat(notApplied?.baselineShift).isNull()
     }
 
     @Test
@@ -246,20 +270,20 @@
         val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
 
         assertThat(tp.color).isEqualTo(Color.Black.toArgb())
-        assertThat(notApplied.background).isEqualTo(background)
+        assertThat(notApplied?.background).isEqualTo(background)
     }
 
     @Test
     fun backgroundTransparent_shouldNotBeLeftAsSpan() {
         val background = Color.Transparent
-        val spanStyle = SpanStyle(background = background)
+        val spanStyle = SpanStyle(background = background, baselineShift = BaselineShift.Subscript)
         val tp = AndroidTextPaint(0, density.density)
         tp.color = Color.Black.toArgb()
 
         val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
 
         assertThat(tp.color).isEqualTo(Color.Black.toArgb())
-        assertThat(notApplied.background).isEqualTo(Color.Unspecified)
+        assertThat(notApplied?.background).isEqualTo(Color.Unspecified)
     }
 
     @Test
@@ -272,7 +296,7 @@
         val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
 
         assertThat(tp.isUnderlineText).isEqualTo(true)
-        assertThat(notApplied.textDecoration).isEqualTo(null)
+        assertThat(notApplied?.textDecoration).isEqualTo(null)
     }
 
     @Test
@@ -285,7 +309,7 @@
         val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
 
         assertThat(tp.isStrikeThruText).isEqualTo(true)
-        assertThat(notApplied.textDecoration).isEqualTo(null)
+        assertThat(notApplied?.textDecoration).isEqualTo(null)
     }
 
     @Test
@@ -301,7 +325,7 @@
 
         assertThat(tp.isUnderlineText).isEqualTo(true)
         assertThat(tp.isStrikeThruText).isEqualTo(true)
-        assertThat(notApplied.textDecoration).isEqualTo(null)
+        assertThat(notApplied?.textDecoration).isEqualTo(null)
     }
 
     @Test
@@ -314,20 +338,21 @@
         val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
 
         assertThat(tp.shadow).isEqualTo(shadow)
-        assertThat(notApplied.shadow).isNull()
+        assertThat(notApplied?.shadow).isNull()
     }
 
     @Test
     fun color_shouldBeAppliedTo_color() {
         val color = Color.Red
-        val spanStyle = SpanStyle(color = color)
+        val spanStyle = SpanStyle(color = color, background = Color.Green)
         val tp = AndroidTextPaint(0, density.density)
         tp.color = Color.Black.toArgb()
 
         val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
 
         assertThat(tp.color).isEqualTo(Color.Red.toArgb())
-        assertThat(notApplied.color).isEqualTo(Color.Unspecified)
+        assertThat(notApplied?.background).isEqualTo(Color.Green)
+        assertThat(notApplied?.color).isEqualTo(Color.Unspecified)
     }
 
     @OptIn(ExperimentalTextApi::class)
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidTextStyle.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidTextStyle.android.kt
index 9e35c4f..88efda3 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidTextStyle.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidTextStyle.android.kt
@@ -32,6 +32,12 @@
      */
     actual val paragraphStyle: PlatformParagraphStyle?
 
+    /**
+     * Convenience constructor for when you already have a [spanStyle] and [paragraphStyle].
+     *
+     * @param spanStyle platform specific span styling
+     * @param paragraphStyle platform specific paragraph styling
+     */
     constructor(
         spanStyle: PlatformSpanStyle?,
         paragraphStyle: PlatformParagraphStyle?
@@ -70,7 +76,11 @@
     )
 
     /**
-     * Set EmojiMatchSupport on [PlatformParagraphStyle]
+     * [EmojiSupportMatch] allows you to control emoji support replacement behavior.
+     *
+     * You can disable emoji support matches by passing [EmojiSupportMatch.None]
+     *
+     * @param emojiSupportMatch configuration for emoji support match and replacement
      */
     constructor(
         emojiSupportMatch: EmojiSupportMatch
@@ -118,17 +128,46 @@
             PlatformParagraphStyle()
     }
 
+    /**
+     * Include extra space beyond font ascent and descent.
+     *
+     * Enables turning on and off for Android [includeFontPadding](https://developer.android.com/reference/android/text/StaticLayout.Builder#setIncludePad(boolean)).
+     *
+     * includeFontPadding was added to Android in order to prevent clipping issues on tall scripts.
+     * However that issue has been fixed since Android 28. Jetpack Compose backports the fix for
+     * Android versions prior to Android 28. Therefore the original reason why includeFontPadding
+     * was needed in invalid on Compose.
+     *
+     * This configuration was added for migration of the apps in case some code or design  was
+     * relying includeFontPadding=true behavior and will be removed.
+     */
     @Deprecated("Sets includeFontPadding parameter for transitioning. Will be removed.")
     val includeFontPadding: Boolean
 
-    var emojiSupportMatch: EmojiSupportMatch
+    /**
+     * When to replace emoji with support emoji using androidx.emoji2.
+     *
+     * This is only available on Android.
+     */
+    val emojiSupportMatch: EmojiSupportMatch
 
+    /**
+     * Represents platform specific text flags
+     *
+     * @param includeFontPadding Set whether to include extra space beyond font ascent and descent.
+     */
     @Deprecated("Provides configuration options for behavior compatibility.")
     constructor(includeFontPadding: Boolean = DefaultIncludeFontPadding) {
         this.includeFontPadding = includeFontPadding
         this.emojiSupportMatch = EmojiSupportMatch.Default
     }
 
+    /**
+     * Represents platform specific text flags
+     *
+     * @param emojiSupportMatch control emoji support matches on Android
+     * @param includeFontPadding Set whether to include extra space beyond font ascent and descent.
+     */
     @Deprecated("Provides configuration options for behavior compatibility.")
     constructor(
         emojiSupportMatch: EmojiSupportMatch = EmojiSupportMatch.Default,
@@ -138,11 +177,19 @@
         this.emojiSupportMatch = emojiSupportMatch
     }
 
+    /**
+     * Represents platform specific text flags.
+     *
+     * @param emojiSupportMatch control emoji support matches on Android
+     */
     constructor(emojiSupportMatch: EmojiSupportMatch = EmojiSupportMatch.Default) {
         this.includeFontPadding = DefaultIncludeFontPadding
         this.emojiSupportMatch = emojiSupportMatch
     }
 
+    /**
+     * Default platform paragraph style
+     */
     constructor() : this(
         includeFontPadding = DefaultIncludeFontPadding,
         emojiSupportMatch = EmojiSupportMatch.Default
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidFont.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidFont.kt
index 01033ed..4a790ed 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidFont.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidFont.kt
@@ -141,6 +141,17 @@
         typefaceLoader: TypefaceLoader,
     ) : this(loadingStrategy, typefaceLoader, FontVariation.Settings())
 
+    /**
+     * The settings that will be applied to this font, if supported by the font.
+     *
+     * If the font does not support a [FontVariation.Setting], it has no effect.
+     *
+     * Subclasses are required to apply these variation settings during font loading path on
+     * appropriate API levels, for example by using [Typeface.Builder.setFontVariationSettings].
+     *
+     * Subclasses may safely apply all variation settings without querying the font file. Android
+     * will ignore any unsupported axis.
+     */
     val variationSettings: FontVariation.Settings = variationSettings
 
     /**
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/intl/AndroidLocaleDelegate.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/intl/AndroidLocaleDelegate.android.kt
index a192a45d..0f87b74 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/intl/AndroidLocaleDelegate.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/intl/AndroidLocaleDelegate.android.kt
@@ -19,6 +19,7 @@
 import androidx.annotation.RequiresApi
 import android.os.LocaleList as AndroidLocaleList
 import java.util.Locale as JavaLocale
+import androidx.compose.ui.text.platform.createSynchronizedObject
 
 /**
  * An Android implementation of Locale object
@@ -41,8 +42,8 @@
  */
 internal class AndroidLocaleDelegateAPI23 : PlatformLocaleDelegate {
 
-    override val current: List<PlatformLocale>
-        get() = listOf(AndroidLocale(JavaLocale.getDefault()))
+    override val current: LocaleList
+        get() = LocaleList(listOf(Locale(AndroidLocale(JavaLocale.getDefault()))))
 
     override fun parseLanguageTag(languageTag: String): PlatformLocale =
         AndroidLocale(JavaLocale.forLanguageTag(languageTag))
@@ -53,15 +54,29 @@
  */
 @RequiresApi(api = 24)
 internal class AndroidLocaleDelegateAPI24 : PlatformLocaleDelegate {
+    private var lastPlatformLocaleList: AndroidLocaleList? = null
+    private var lastLocaleList: LocaleList? = null
+    private val lock = createSynchronizedObject()
 
-    override val current: List<PlatformLocale>
+    override val current: LocaleList
         get() {
-            val localeList = AndroidLocaleList.getDefault()
-            val result = mutableListOf<PlatformLocale>()
-            for (i in 0 until localeList.size()) {
-                result.add(AndroidLocale(localeList[i]))
+            val platformLocaleList = AndroidLocaleList.getDefault()
+            return synchronized(lock) {
+                // try to avoid any more allocs
+                lastLocaleList?.let {
+                    if (platformLocaleList === lastPlatformLocaleList) return it
+                }
+                // this is faster than adding to an empty mutableList
+                val localeList = LocaleList(
+                    List(platformLocaleList.size()) { position ->
+                        Locale(AndroidLocale(platformLocaleList[position]))
+                    }
+                )
+                // cache the platform result and compose result
+                lastPlatformLocaleList = platformLocaleList
+                lastLocaleList = localeList
+                localeList
             }
-            return result
         }
 
     override fun parseLanguageTag(languageTag: String): PlatformLocale =
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt
index c3e09fa..cd3a8f2 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt
@@ -72,7 +72,7 @@
     fontFamilyResolver: FontFamily.Resolver
 ): SpannableString {
     val spannableString = SpannableString(text)
-    spanStyles.fastForEach { (style, start, end) ->
+    spanStylesOrNull?.fastForEach { (style, start, end) ->
         // b/232238615 looking up fonts inside of accessibility does not honor overwritten
         // FontFamilyResolver. This is not safe until Font.ResourceLoader is fully removed.
         val noFontStyle = style.copy(fontFamily = null)
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsics.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsics.android.kt
index 8ed7467..8dfa683 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsics.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsics.android.kt
@@ -33,13 +33,13 @@
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontSynthesis
 import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.font.TypefaceResult
 import androidx.compose.ui.text.intl.AndroidLocale
 import androidx.compose.ui.text.intl.LocaleList
 import androidx.compose.ui.text.platform.extensions.applySpanStyle
 import androidx.compose.ui.text.platform.extensions.setTextMotion
 import androidx.compose.ui.text.style.TextDirection
 import androidx.compose.ui.unit.Density
-import androidx.compose.ui.util.fastAny
 import androidx.core.text.TextUtilsCompat
 import androidx.core.view.ViewCompat
 import java.util.Locale
@@ -66,7 +66,7 @@
     override val minIntrinsicWidth: Float
         get() = layoutIntrinsics.minIntrinsicWidth
 
-    private val resolvedTypefaces: MutableList<TypefaceDirtyTracker> = mutableListOf()
+    private var resolvedTypefaces: TypefaceDirtyTrackerLinkedList? = null
 
     /**
      * If emojiCompat is used in the making of this Paragraph
@@ -77,7 +77,7 @@
         if (!style.hasEmojiCompat) { false } else { EmojiCompatStatus.fontLoaded.value }
 
     override val hasStaleResolvedFonts: Boolean
-        get() = resolvedTypefaces.fastAny { it.isStaleResolvedFont } ||
+        get() = (resolvedTypefaces?.isStaleResolvedFont ?: false) ||
             (!emojiCompatProcessed && style.hasEmojiCompat &&
                 /* short-circuit this state read */ EmojiCompatStatus.fontLoaded.value)
 
@@ -95,9 +95,13 @@
                     fontStyle,
                     fontSynthesis
                 )
-                val holder = TypefaceDirtyTracker(result)
-                resolvedTypefaces.add(holder)
-                holder.typeface
+                if (result !is TypefaceResult.Immutable) {
+                    val newHead = TypefaceDirtyTrackerLinkedList(result, resolvedTypefaces)
+                    resolvedTypefaces = newHead
+                    newHead.typeface
+                } else {
+                    result.value as Typeface
+                }
             }
 
         textPaint.setTextMotion(style.textMotion)
@@ -106,21 +110,31 @@
             style = style.toSpanStyle(),
             resolveTypeface = resolveTypeface,
             density = density,
+            requiresLetterSpacing = spanStyles.isNotEmpty(),
         )
 
+        val finalSpanStyles = if (notAppliedStyle != null) {
+            // This is just a prepend operation, written in a lower alloc way
+            // equivalent to: `AnnotatedString.Range(...) + spanStyles`
+            List(spanStyles.size + 1) { position ->
+                when (position) {
+                    0 -> AnnotatedString.Range(
+                        item = notAppliedStyle,
+                        start = 0,
+                        end = text.length
+                    )
+
+                    else -> spanStyles[position - 1]
+                }
+            }
+        } else {
+            spanStyles
+        }
         charSequence = createCharSequence(
             text = text,
             contextFontSize = textPaint.textSize,
             contextTextStyle = style,
-            // NOTE(text-perf-review): this is sabotaging the optimization that
-            // createCharSequence makes where it just uses `text` if there are no spanStyles!
-            spanStyles = listOf(
-                AnnotatedString.Range(
-                    item = notAppliedStyle,
-                    start = 0,
-                    end = text.length
-                )
-            ) + spanStyles,
+            spanStyles = finalSpanStyles,
             placeholders = placeholders,
             density = density,
             resolveTypeface = resolveTypeface,
@@ -176,13 +190,16 @@
     density = density
 )
 
-private class TypefaceDirtyTracker(val resolveResult: State<Any>) {
+private class TypefaceDirtyTrackerLinkedList(
+    private val resolveResult: State<Any>,
+    private val next: TypefaceDirtyTrackerLinkedList? = null
+) {
     val initial = resolveResult.value
     val typeface: Typeface
         get() = initial as Typeface
 
     val isStaleResolvedFont: Boolean
-        get() = resolveResult.value !== initial
+        get() = resolveResult.value !== initial || (next != null && next.isStaleResolvedFont)
 }
 
 private val TextStyle.hasEmojiCompat: Boolean
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
index 20dbff0..3b316c8 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
@@ -77,12 +77,6 @@
 import kotlin.math.ceil
 import kotlin.math.roundToInt
 
-private data class SpanRange(
-    val span: Any,
-    val start: Int,
-    val end: Int
-)
-
 internal fun Spannable.setSpan(span: Any, start: Int, end: Int) {
     setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
 }
@@ -182,12 +176,7 @@
 ) {
 
     setFontAttributes(contextTextStyle, spanStyles, resolveTypeface)
-
-    // LetterSpacingSpanPx/LetterSpacingSpanSP has lower priority than normal spans. Because
-    // letterSpacing relies on the fontSize on [Paint] to compute Px/Sp from Em. So it must be
-    // applied after all spans that changes the fontSize.
-    val lowPrioritySpans = ArrayList<SpanRange>()
-
+    var hasLetterSpacing = false
     for (i in spanStyles.indices) {
         val spanStyleRange = spanStyles[i]
         val start = spanStyleRange.start
@@ -197,21 +186,39 @@
 
         setSpanStyle(
             spanStyleRange,
-            density,
-            lowPrioritySpans
+            density
         )
+
+        if (spanStyleRange.item.needsLetterSpacingSpan) {
+            hasLetterSpacing = true
+        }
     }
 
-    lowPrioritySpans.fastForEach { (span, start, end) ->
-        setSpan(span, start, end)
+    if (hasLetterSpacing) {
+
+        // LetterSpacingSpanPx/LetterSpacingSpanSP has lower priority than normal spans. Because
+        // letterSpacing relies on the fontSize on [Paint] to compute Px/Sp from Em. So it must be
+        // applied after all spans that changes the fontSize.
+
+        for (i in spanStyles.indices) {
+            val spanStyleRange = spanStyles[i]
+            val start = spanStyleRange.start
+            val end = spanStyleRange.end
+            val style = spanStyleRange.item
+
+            if (start < 0 || start >= length || end <= start || end > length) continue
+
+            createLetterSpacingSpan(style.letterSpacing, density)?.let {
+                setSpan(it, start, end)
+            }
+        }
     }
 }
 
 @OptIn(ExperimentalTextApi::class)
 private fun Spannable.setSpanStyle(
     spanStyleRange: AnnotatedString.Range<SpanStyle>,
-    density: Density,
-    lowPrioritySpans: ArrayList<SpanRange>
+    density: Density
 ) {
     val start = spanStyleRange.start
     val end = spanStyleRange.end
@@ -240,12 +247,6 @@
     setShadow(style.shadow, start, end)
 
     setDrawStyle(style.drawStyle, start, end)
-
-    createLetterSpacingSpan(style.letterSpacing, density)?.let {
-        lowPrioritySpans.add(
-            SpanRange(it, start, end)
-        )
-    }
 }
 
 /**
@@ -403,6 +404,9 @@
     }
 }
 
+private val SpanStyle.needsLetterSpacingSpan: Boolean
+    get() = letterSpacing.type == TextUnitType.Sp || letterSpacing.type == TextUnitType.Em
+
 @OptIn(InternalPlatformTextApi::class)
 private fun Spannable.setShadow(shadow: Shadow?, start: Int, end: Int) {
     shadow?.let {
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/TextPaintExtensions.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/TextPaintExtensions.android.kt
index 9d9bb33..b92b9c55 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/TextPaintExtensions.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/TextPaintExtensions.android.kt
@@ -49,7 +49,8 @@
     style: SpanStyle,
     resolveTypeface: (FontFamily?, FontWeight, FontStyle, FontSynthesis) -> Typeface,
     density: Density,
-): SpanStyle {
+    requiresLetterSpacing: Boolean = false,
+): SpanStyle? {
     when (style.fontSize.type) {
         TextUnitType.Sp -> with(density) {
             textSize = style.fontSize.toPx()
@@ -82,12 +83,6 @@
         }
     }
 
-    when (style.letterSpacing.type) {
-        TextUnitType.Em -> { letterSpacing = style.letterSpacing.value }
-        TextUnitType.Sp -> {} // Sp will be handled by applying a span
-        else -> {} // Do nothing
-    }
-
     if (style.fontFeatureSettings != null && style.fontFeatureSettings != "") {
         fontFeatureSettings = style.fontFeatureSettings
     }
@@ -110,28 +105,53 @@
     setTextDecoration(style.textDecoration)
     setDrawStyle(style.drawStyle)
 
-    // letterSpacing with unit Sp needs to be handled by span.
+    // apply para level leterspacing
+    if (style.letterSpacing.type == TextUnitType.Sp && style.letterSpacing.value != 0.0f) {
+        val emWidth = textSize * textScaleX
+        val letterSpacingPx = with(density) {
+            style.letterSpacing.toPx()
+        }
+        // Do nothing if emWidth is 0.0f.
+        if (emWidth != 0.0f) {
+            letterSpacing = letterSpacingPx / emWidth
+        }
+    } else if (style.letterSpacing.type == TextUnitType.Em) {
+        letterSpacing = style.letterSpacing.value
+    }
+
+    return generateFallbackSpanStyle(
+        style.letterSpacing,
+        requiresLetterSpacing,
+        style.background,
+        style.baselineShift
+    )
+}
+
+private fun generateFallbackSpanStyle(
+    letterSpacing: TextUnit,
+    requiresLetterSpacing: Boolean,
+    background: Color,
+    baselineShift: BaselineShift?
+): SpanStyle? {
+    // letterSpacing needs to be reset at every metricsEffectingSpan transition - so generate
+    // a span for it only if there are other spans
+    val hasLetterSpacing = requiresLetterSpacing &&
+        (letterSpacing.type == TextUnitType.Sp && letterSpacing.value != 0f)
+
     // baselineShift and bgColor is reset in the Android Layout constructor,
     // therefore we cannot apply them on paint, have to use spans.
-    return SpanStyle(
-        letterSpacing = if (style.letterSpacing.type == TextUnitType.Sp &&
-            style.letterSpacing.value != 0f
-        ) {
-            style.letterSpacing
-        } else {
-            TextUnit.Unspecified
-        },
-        background = if (style.background == Color.Transparent) {
-            Color.Unspecified // No need to add transparent background for default text style.
-        } else {
-            style.background
-        },
-        baselineShift = if (style.baselineShift == BaselineShift.None) {
-            null
-        } else {
-            style.baselineShift
-        }
-    )
+    val hasBackgroundColor = background != Color.Unspecified && background != Color.Transparent
+    val hasBaselineShift = baselineShift != null && baselineShift != BaselineShift.None
+
+    return if (!hasLetterSpacing && !hasBackgroundColor && !hasBaselineShift) {
+        null
+    } else {
+        SpanStyle(
+            letterSpacing = if (hasLetterSpacing) { letterSpacing } else { TextUnit.Unspecified },
+            background = if (hasBackgroundColor) { background } else { Color.Unspecified },
+            baselineShift = if (hasBaselineShift) { baselineShift } else { null }
+        )
+    }
 }
 
 @OptIn(ExperimentalTextApi::class)
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt
index 2d7c207..d447d62 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt
@@ -32,10 +32,23 @@
 @Immutable
 class AnnotatedString internal constructor(
     val text: String,
-    val spanStyles: List<Range<SpanStyle>> = emptyList(),
-    val paragraphStyles: List<Range<ParagraphStyle>> = emptyList(),
-    internal val annotations: List<Range<out Any>> = emptyList()
+    internal val spanStylesOrNull: List<Range<SpanStyle>>? = null,
+    internal val paragraphStylesOrNull: List<Range<ParagraphStyle>>? = null,
+    internal val annotations: List<Range<out Any>>? = null
 ) : CharSequence {
+
+    /**
+     * All [SpanStyle] that have been applied to a range of this String
+     */
+    val spanStyles: List<Range<SpanStyle>>
+        get() = spanStylesOrNull ?: emptyList()
+
+    /**
+     * All [ParagraphStyle] that have been applied to a range of this String
+     */
+    val paragraphStyles: List<Range<ParagraphStyle>>
+        get() = paragraphStylesOrNull ?: emptyList()
+
     /**
      * The basic data structure of text with multiple styles. To construct an [AnnotatedString]
      * you can use [Builder].
@@ -60,11 +73,17 @@
         text: String,
         spanStyles: List<Range<SpanStyle>> = listOf(),
         paragraphStyles: List<Range<ParagraphStyle>> = listOf()
-    ) : this(text, spanStyles, paragraphStyles, listOf())
+    ) : this(
+        text,
+        spanStyles.ifEmpty { null },
+        paragraphStyles.ifEmpty { null },
+        null
+    )
 
     init {
         var lastStyleEnd = -1
-        paragraphStyles.sortedBy { it.start }.fastForEach { paragraphStyle ->
+        @Suppress("ListIterator")
+        paragraphStylesOrNull?.sortedBy { it.start }?.fastForEach { paragraphStyle ->
             require(paragraphStyle.start >= lastStyleEnd) {
                 "ParagraphStyle should not overlap"
             }
@@ -96,8 +115,8 @@
         val text = text.substring(startIndex, endIndex)
         return AnnotatedString(
             text = text,
-            spanStyles = filterRanges(spanStyles, startIndex, endIndex),
-            paragraphStyles = filterRanges(paragraphStyles, startIndex, endIndex),
+            spanStylesOrNull = filterRanges(spanStylesOrNull, startIndex, endIndex),
+            paragraphStylesOrNull = filterRanges(paragraphStylesOrNull, startIndex, endIndex),
             annotations = filterRanges(annotations, startIndex, endIndex)
         )
     }
@@ -137,16 +156,17 @@
      */
     @Suppress("UNCHECKED_CAST")
     fun getStringAnnotations(tag: String, start: Int, end: Int): List<Range<String>> =
-        annotations.fastFilter {
+        (annotations?.fastFilter {
             it.item is String && tag == it.tag && intersect(start, end, it.start, it.end)
-        } as List<Range<String>>
+        } ?: emptyList()) as List<Range<String>>
 
     /**
      * Returns true if [getStringAnnotations] with the same parameters would return a non-empty list
      */
-    fun hasStringAnnotations(tag: String, start: Int, end: Int): Boolean = annotations.fastAny {
-        it.item is String && tag == it.tag && intersect(start, end, it.start, it.end)
-    }
+    fun hasStringAnnotations(tag: String, start: Int, end: Int): Boolean =
+        annotations?.fastAny {
+            it.item is String && tag == it.tag && intersect(start, end, it.start, it.end)
+        } ?: false
 
     /**
      * Query all of the string annotations attached on this AnnotatedString.
@@ -159,9 +179,9 @@
      */
     @Suppress("UNCHECKED_CAST")
     fun getStringAnnotations(start: Int, end: Int): List<Range<String>> =
-        annotations.fastFilter {
+        (annotations?.fastFilter {
             it.item is String && intersect(start, end, it.start, it.end)
-        } as List<Range<String>>
+        } ?: emptyList()) as List<Range<String>>
 
     /**
      * Query all of the [TtsAnnotation]s attached on this [AnnotatedString].
@@ -174,9 +194,9 @@
      */
     @Suppress("UNCHECKED_CAST")
     fun getTtsAnnotations(start: Int, end: Int): List<Range<TtsAnnotation>> =
-        annotations.fastFilter {
+        ((annotations?.fastFilter {
             it.item is TtsAnnotation && intersect(start, end, it.start, it.end)
-        } as List<Range<TtsAnnotation>>
+        } ?: emptyList()) as List<Range<TtsAnnotation>>)
 
     /**
      * Query all of the [UrlAnnotation]s attached on this [AnnotatedString].
@@ -190,25 +210,25 @@
     @ExperimentalTextApi
     @Suppress("UNCHECKED_CAST")
     fun getUrlAnnotations(start: Int, end: Int): List<Range<UrlAnnotation>> =
-        annotations.fastFilter {
+        ((annotations?.fastFilter {
             it.item is UrlAnnotation && intersect(start, end, it.start, it.end)
-        } as List<Range<UrlAnnotation>>
+        } ?: emptyList()) as List<Range<UrlAnnotation>>)
 
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is AnnotatedString) return false
         if (text != other.text) return false
-        if (spanStyles != other.spanStyles) return false
-        if (paragraphStyles != other.paragraphStyles) return false
+        if (spanStylesOrNull != other.spanStylesOrNull) return false
+        if (paragraphStylesOrNull != other.paragraphStylesOrNull) return false
         if (annotations != other.annotations) return false
         return true
     }
 
     override fun hashCode(): Int {
         var result = text.hashCode()
-        result = 31 * result + spanStyles.hashCode()
-        result = 31 * result + paragraphStyles.hashCode()
-        result = 31 * result + annotations.hashCode()
+        result = 31 * result + (spanStylesOrNull?.hashCode() ?: 0)
+        result = 31 * result + (paragraphStylesOrNull?.hashCode() ?: 0)
+        result = 31 * result + (annotations?.hashCode() ?: 0)
         return result
     }
 
@@ -373,14 +393,14 @@
             val start = this.text.length
             this.text.append(text.text)
             // offset every style with start and add to the builder
-            text.spanStyles.fastForEach {
+            text.spanStylesOrNull?.fastForEach {
                 addStyle(it.item, start + it.start, start + it.end)
             }
-            text.paragraphStyles.fastForEach {
+            text.paragraphStylesOrNull?.fastForEach {
                 addStyle(it.item, start + it.start, start + it.end)
             }
 
-            text.annotations.fastForEach {
+            text.annotations?.fastForEach {
                 annotations.add(
                     MutableRange(it.item, start + it.start, start + it.end, it.tag)
                 )
@@ -400,14 +420,14 @@
             val insertionStart = this.text.length
             this.text.append(text.text, start, end)
             // offset every style with insertionStart and add to the builder
-            text.getLocalSpanStyles(start, end).fastForEach {
+            text.getLocalSpanStyles(start, end)?.fastForEach {
                 addStyle(it.item, insertionStart + it.start, insertionStart + it.end)
             }
-            text.getLocalParagraphStyles(start, end).fastForEach {
+            text.getLocalParagraphStyles(start, end)?.fastForEach {
                 addStyle(it.item, insertionStart + it.start, insertionStart + it.end)
             }
 
-            text.getLocalAnnotations(start, end).fastForEach {
+            text.getLocalAnnotations(start, end)?.fastForEach {
                 annotations.add(
                     MutableRange(
                         it.item,
@@ -616,9 +636,15 @@
         fun toAnnotatedString(): AnnotatedString {
             return AnnotatedString(
                 text = text.toString(),
-                spanStyles = spanStyles.fastMap { it.toRange(text.length) },
-                paragraphStyles = paragraphStyles.fastMap { it.toRange(text.length) },
-                annotations = annotations.fastMap { it.toRange(text.length) }
+                spanStylesOrNull = spanStyles
+                    .fastMap { it.toRange(text.length) }
+                    .ifEmpty { null },
+                paragraphStylesOrNull = paragraphStyles
+                    .fastMap { it.toRange(text.length) }
+                    .ifEmpty { null },
+                annotations = annotations
+                    .fastMap { it.toRange(text.length) }
+                    .ifEmpty { null }
             )
         }
     }
@@ -643,7 +669,7 @@
     defaultParagraphStyle: ParagraphStyle
 ): List<Range<ParagraphStyle>> {
     val length = text.length
-    val paragraphStyles = paragraphStyles
+    val paragraphStyles = paragraphStylesOrNull ?: emptyList()
 
     var lastOffset = 0
     val result = mutableListOf<Range<ParagraphStyle>>()
@@ -676,8 +702,9 @@
 private fun AnnotatedString.getLocalSpanStyles(
     start: Int,
     end: Int
-): List<Range<SpanStyle>> {
-    if (start == end) return listOf()
+): List<Range<SpanStyle>>? {
+    if (start == end) return null
+    val spanStyles = spanStylesOrNull ?: return null
     // If the given range covers the whole AnnotatedString, return SpanStyles without conversion.
     if (start == 0 && end >= this.text.length) {
         return spanStyles
@@ -702,8 +729,9 @@
 private fun AnnotatedString.getLocalParagraphStyles(
     start: Int,
     end: Int
-): List<Range<ParagraphStyle>> {
-    if (start == end) return listOf()
+): List<Range<ParagraphStyle>>? {
+    if (start == end) return null
+    val paragraphStyles = paragraphStylesOrNull ?: return null
     // If the given range covers the whole AnnotatedString, return SpanStyles without conversion.
     if (start == 0 && end >= this.text.length) {
         return paragraphStyles
@@ -728,8 +756,9 @@
 private fun AnnotatedString.getLocalAnnotations(
     start: Int,
     end: Int
-): List<Range<out Any>> {
-    if (start == end) return listOf()
+): List<Range<out Any>>? {
+    if (start == end) return null
+    val annotations = annotations ?: return null
     // If the given range covers the whole AnnotatedString, return SpanStyles without conversion.
     if (start == 0 && end >= this.text.length) {
         return annotations
@@ -760,7 +789,7 @@
 ): AnnotatedString {
     return AnnotatedString(
         text = if (start != end) text.substring(start, end) else "",
-        spanStyles = getLocalSpanStyles(start, end)
+        spanStylesOrNull = getLocalSpanStyles(start, end)
     )
 }
 
@@ -1013,16 +1042,18 @@
  * @param start the inclusive start offset of the text range
  * @param end the exclusive end offset of the text range
  */
-private fun <T> filterRanges(ranges: List<Range<out T>>, start: Int, end: Int): List<Range<T>> {
+private fun <T> filterRanges(ranges: List<Range<out T>>?, start: Int, end: Int): List<Range<T>>? {
     require(start <= end) { "start ($start) should be less than or equal to end ($end)" }
-    return ranges.fastFilter { intersect(start, end, it.start, it.end) }.fastMap {
+    val nonNullRange = ranges ?: return null
+
+    return nonNullRange.fastFilter { intersect(start, end, it.start, it.end) }.fastMap {
         Range(
             item = it.item,
             start = maxOf(start, it.start) - start,
             end = minOf(end, it.end) - start,
             tag = it.tag
         )
-    }
+    }.ifEmpty { null }
 }
 
 /**
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
index b3cd8cd..00de43db 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
@@ -246,8 +246,6 @@
      * because we reached `maxLines` lines of text or because the `maxLines` was
      * null, `ellipsis` was not null, and one of the lines exceeded the width
      * constraint.
-     *
-     * See the discussion of the `maxLines` and `ellipsis` arguments at [ParagraphStyle].
      */
     val didExceedMaxLines: Boolean
 
@@ -779,7 +777,7 @@
 
     private fun requireLineIndexInRange(lineIndex: Int) {
         require(lineIndex in 0 until lineCount) {
-            "lineIndex($lineIndex) is out of bounds [0, $lineIndex)"
+            "lineIndex($lineIndex) is out of bounds [0, $lineCount)"
         }
     }
 }
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Savers.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Savers.kt
index e23240e..c5ad4ce 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Savers.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Savers.kt
@@ -81,11 +81,16 @@
     },
     restore = {
         val list = it as List<Any?>
+        // lift these to make types work
+        val spanStylesOrNull: List<AnnotatedString.Range<SpanStyle>>? =
+            restore(list[1], AnnotationRangeListSaver)
+        val paragraphStylesOrNull: List<AnnotatedString.Range<ParagraphStyle>>? =
+            restore(list[2], AnnotationRangeListSaver)
         AnnotatedString(
             text = restore(list[0])!!,
-            spanStyles = restore(list[1], AnnotationRangeListSaver)!!,
-            paragraphStyles = restore(list[2], AnnotationRangeListSaver)!!,
-            annotations = restore(list[3], AnnotationRangeListSaver)!!,
+            spanStylesOrNull = spanStylesOrNull?.ifEmpty { null },
+            paragraphStylesOrNull = paragraphStylesOrNull?.ifEmpty { null },
+            annotations = restore(list[3], AnnotationRangeListSaver),
         )
     }
 )
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/Font.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/Font.kt
index 79f18a2..66e5205 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/Font.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/Font.kt
@@ -95,7 +95,7 @@
          *
          * This timeout is not configurable, and timers are maintained globally.
          */
-        const val MaximumAsyncTimeout = 15_000L
+        const val MaximumAsyncTimeoutMillis = 15_000L
     }
 }
 
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontFamily.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontFamily.kt
index 399974d..d928ccf 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontFamily.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontFamily.kt
@@ -196,7 +196,7 @@
 }
 
 /**
- * Defines a font family with an generic font family name.
+ * Defines a font family with a generic font family name.
  *
  * If the platform cannot find the passed generic font family, use the platform default one.
  *
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapter.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapter.kt
index ab77242..10b9df2 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapter.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapter.kt
@@ -102,7 +102,7 @@
                     async {
                         asyncTypefaceCache.runCached(font, resourceLoader, true) {
                             try {
-                                withTimeout(Font.MaximumAsyncTimeout) {
+                                withTimeout(Font.MaximumAsyncTimeoutMillis) {
                                     resourceLoader.awaitLoad(font)
                                 }
                             } catch (cause: Exception) {
@@ -297,7 +297,7 @@
         return try {
             // case 0: load completes - success (non-null)
             // case 1: we timeout - permanent failure (null)
-            withTimeoutOrNull(Font.MaximumAsyncTimeout) {
+            withTimeoutOrNull(Font.MaximumAsyncTimeoutMillis) {
                 platformFontLoader.awaitLoad(this@loadWithTimeoutOrNull)
             }
         } catch (cancel: CancellationException) {
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/intl/Locale.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/intl/Locale.kt
index f9ab3f5..797adea 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/intl/Locale.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/intl/Locale.kt
@@ -36,7 +36,7 @@
         /**
          * Returns a [Locale] object which represents current locale
          */
-        val current: Locale get() = Locale(platformLocaleDelegate.current[0])
+        val current: Locale get() = platformLocaleDelegate.current[0]
     }
 
     /**
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/intl/LocaleList.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/intl/LocaleList.kt
index 634f8bb..987e8ef 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/intl/LocaleList.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/intl/LocaleList.kt
@@ -30,11 +30,12 @@
 @Immutable
 class LocaleList constructor(val localeList: List<Locale>) : Collection<Locale> {
     companion object {
+
         /**
          * Returns Locale object which represents current locale
          */
         val current: LocaleList
-            get() = LocaleList(platformLocaleDelegate.current.fastMap { Locale(it) })
+            get() = platformLocaleDelegate.current
     }
 
     /**
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.kt
index 0c99171..de5c613 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.kt
@@ -52,7 +52,7 @@
      *
      * The implementation must return at least one locale.
      */
-    val current: List<PlatformLocale>
+    val current: LocaleList
 
     /**
      * Parse the IETF BCP47 compliant language tag.
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/Hyphens.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/Hyphens.kt
index dadedd9..5d8db44 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/Hyphens.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/Hyphens.kt
@@ -41,7 +41,8 @@
  * The default configuration for [Hyphens] = [Hyphens.None]
  *
  */
-class Hyphens private constructor() {
+@JvmInline
+value class Hyphens private constructor(internal val value: Int) {
     companion object {
         /**
          *  Lines will break with no hyphenation.
@@ -58,7 +59,7 @@
          * +---------+
          * </pre>
          */
-        val None = Hyphens()
+        val None = Hyphens(1)
 
         /**
          * The words will be automatically broken at appropriate hyphenation points.
@@ -73,7 +74,7 @@
          * +---------+
          * </pre>
          */
-        val Auto = Hyphens()
+        val Auto = Hyphens(2)
     }
 
     override fun toString() = when (this) {
diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/intl/DesktopPlatformLocale.desktop.kt b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/intl/DesktopPlatformLocale.desktop.kt
index 7fe8f8f..69ddebd 100644
--- a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/intl/DesktopPlatformLocale.desktop.kt
+++ b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/intl/DesktopPlatformLocale.desktop.kt
@@ -32,8 +32,8 @@
 }
 
 internal actual fun createPlatformLocaleDelegate() = object : PlatformLocaleDelegate {
-    override val current: List<PlatformLocale>
-        get() = listOf(DesktopLocale(Locale.getDefault()))
+    override val current: LocaleList
+        get() = LocaleList(listOf(Locale(DesktopLocale(Locale.getDefault()))))
 
     override fun parseLanguageTag(languageTag: String): PlatformLocale =
         DesktopLocale(
diff --git a/compose/ui/ui-text/src/jvmMain/kotlin/androidx/compose/ui/text/JvmAnnotatedString.jvm.kt b/compose/ui/ui-text/src/jvmMain/kotlin/androidx/compose/ui/text/JvmAnnotatedString.jvm.kt
index a2f6c8d..09a611f 100644
--- a/compose/ui/ui-text/src/jvmMain/kotlin/androidx/compose/ui/text/JvmAnnotatedString.jvm.kt
+++ b/compose/ui/ui-text/src/jvmMain/kotlin/androidx/compose/ui/text/JvmAnnotatedString.jvm.kt
@@ -31,8 +31,8 @@
     transform: (String, Int, Int) -> String
 ): AnnotatedString {
     val transitions = sortedSetOf(0, text.length)
-    collectRangeTransitions(spanStyles, transitions)
-    collectRangeTransitions(paragraphStyles, transitions)
+    collectRangeTransitions(spanStylesOrNull, transitions)
+    collectRangeTransitions(paragraphStylesOrNull, transitions)
     collectRangeTransitions(annotations, transitions)
 
     var resultStr = ""
@@ -42,21 +42,21 @@
         offsetMap.put(end, resultStr.length)
     }
 
-    val newSpanStyles = spanStyles.fastMap {
+    val newSpanStyles = spanStylesOrNull?.fastMap {
         // The offset map must have mapping entry from all style start, end position.
         Range(it.item, offsetMap[it.start]!!, offsetMap[it.end]!!)
     }
-    val newParaStyles = paragraphStyles.fastMap {
+    val newParaStyles = paragraphStylesOrNull?.fastMap {
         Range(it.item, offsetMap[it.start]!!, offsetMap[it.end]!!)
     }
-    val newAnnotations = annotations.fastMap {
+    val newAnnotations = annotations?.fastMap {
         Range(it.item, offsetMap[it.start]!!, offsetMap[it.end]!!)
     }
 
     return AnnotatedString(
         text = resultStr,
-        spanStyles = newSpanStyles,
-        paragraphStyles = newParaStyles,
+        spanStylesOrNull = newSpanStyles,
+        paragraphStylesOrNull = newParaStyles,
         annotations = newAnnotations
     )
 }
@@ -68,10 +68,10 @@
  * @param target The output list
  */
 private fun collectRangeTransitions(
-    ranges: List<Range<*>>,
+    ranges: List<Range<*>>?,
     target: SortedSet<Int>
 ) {
-    ranges.fastFold(target) { acc, range ->
+    ranges?.fastFold(target) { acc, range ->
         acc.apply {
             add(range.start)
             add(range.end)
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt
index d69b2f3..16c64c8 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt
@@ -184,10 +184,10 @@
         val text = "a"
         val annotatedString = AnnotatedString(
             text = text,
-            spanStyles = listOf(
+            spanStylesOrNull = listOf(
                 text.inclusiveRangeOf('a', 'a', item = SpanStyle(color = Color.Red))
             ),
-            paragraphStyles = listOf(
+            paragraphStylesOrNull = listOf(
                 text.inclusiveRangeOf('a', 'a', item = ParagraphStyle(lineHeight = 20.sp))
             ),
             annotations = listOf(
@@ -222,8 +222,8 @@
         )
         val appendedAnnotatedString = AnnotatedString(
             text = appendedText,
-            spanStyles = appendedSpanStyles,
-            paragraphStyles = appendedParagraphStyles,
+            spanStylesOrNull = appendedSpanStyles,
+            paragraphStylesOrNull = appendedParagraphStyles,
             annotations = appendedAnnotations
         )
 
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTest.kt
index e73d220..05297b1 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTest.kt
@@ -113,8 +113,8 @@
         )
         val annotatedString1 = AnnotatedString(
             text = text1,
-            spanStyles = spanStyles1,
-            paragraphStyles = paragraphStyles1,
+            spanStylesOrNull = spanStyles1,
+            paragraphStylesOrNull = paragraphStyles1,
             annotations = annotations1
         )
 
@@ -123,8 +123,8 @@
         val paragraphStyle = ParagraphStyle(lineHeight = 10.sp)
         val annotatedString2 = AnnotatedString(
             text = text2,
-            spanStyles = listOf(Range(spanStyle, 0, text2.length)),
-            paragraphStyles = listOf(Range(paragraphStyle, 0, text2.length)),
+            spanStylesOrNull = listOf(Range(spanStyle, 0, text2.length)),
+            paragraphStylesOrNull = listOf(Range(paragraphStyle, 0, text2.length)),
             annotations = listOf(Range("annotation2", 0, text2.length, "scope2"))
         )
 
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index 0cae782..80af0795 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -38,9 +38,11 @@
         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")
+        implementation(project(":activity:activity-compose"))
+        implementation(project(":lifecycle:lifecycle-common"))
 
         // kotlin-reflect and animation-tooling-internal are provided by Studio at runtime
         compileOnly(project(":compose:animation:animation-tooling-internal"))
@@ -87,7 +89,8 @@
                 implementation("androidx.savedstate:savedstate-ktx:1.2.0")
 
                 implementation(project(":compose:material:material"))
-                implementation("androidx.activity:activity-compose:1.3.0")
+                implementation(project(":activity:activity-compose"))
+                implementation(project(":lifecycle:lifecycle-common"))
 
                 // kotlin-reflect and tooling-animation-internal are provided by Studio at runtime
                 compileOnly(project(":compose:animation:animation-tooling-internal"))
@@ -107,8 +110,8 @@
                 // Outside of androidx this is resolved via constraint added to lifecycle-common,
                 // but it doesn't work in androidx.
                 // See aosp/1804059
-                implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
-                implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
+                implementation(project(":lifecycle:lifecycle-common-java8"))
+                implementation(project(":lifecycle:lifecycle-viewmodel-savedstate"))
 
                 implementation(libs.junit)
                 implementation(libs.testRunner)
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClockTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClockTest.kt
index 323824b..f58d39d 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClockTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClockTest.kt
@@ -77,7 +77,7 @@
         checkUpdatedState(clock, label = "DpAnimation",
             newInitialValue = 1.dp, newTargetValue = 2.dp,
             composeState = { state!!.value })
-        rule.runOnUiThread { clock.setStateParameters(listOf(3.dp), listOf(4.dp)) }
+        rule.runOnUiThread { clock.setStateParameters(3f, 4.0) }
         checkUpdatedState(clock, label = "DpAnimation",
             newInitialValue = 3.dp, newTargetValue = 4.dp,
             composeState = { state!!.value })
@@ -89,7 +89,7 @@
             composeState = { state!!.value })
         // Invalid parameters are ignored.
         rule.runOnUiThread {
-            clock.setStateParameters(111.dp, 111)
+            clock.setStateParameters(111.dp, "")
             clock.setStateParameters(111.dp, null)
             clock.setStateParameters(listOf(111.dp), listOf(111L))
             clock.setStateParameters(listOf(null), listOf(null))
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/UtilsTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/UtilsTest.kt
new file mode 100644
index 0000000..a5cf31a
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/UtilsTest.kt
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling.animation.clock
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.animation.states.TargetState
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class UtilsTest {
+
+    @Test
+    fun currentValueIsNull() {
+        val value = parseParametersToValue<Int?>(null, 20, 30)
+        assertNull(value)
+    }
+
+    @Test
+    fun par2IsNull() {
+        val value = parseParametersToValue(10, 20, null)
+        assertNull(value)
+    }
+
+    @Test
+    fun currentValueHasDifferentType() {
+        val value = parseParametersToValue(10f, 20, 30)
+        assertNull(value)
+    }
+
+    @Test
+    fun par1HasDifferentType() {
+        val value = parseParametersToValue(10, 20f, 30)
+        assertNull(value)
+    }
+
+    @Test
+    fun par2HasDifferentType() {
+        val value = parseParametersToValue(10, 20, 30f)
+        assertNull(value)
+    }
+
+    @Test
+    fun listsHasNull() {
+        val value = parseParametersToValue(IntSize(10, 20), listOf(10, null), listOf(20, 30))
+        assertNull(value)
+    }
+
+    @Test
+    fun listsAreEmpty() {
+        val value = parseParametersToValue(IntSize(10, 20), emptyList<Int>(), emptyList<Int>())
+        assertNull(value)
+    }
+
+    @Test
+    fun offsetOutOfBounds() {
+        val value = parseParametersToValue(
+            Offset(10f, 20f),
+            listOf(30f),
+            listOf(50f, 60f)
+        )
+        assertNull(value)
+    }
+
+    @Test
+    fun offsetIncorrectType() {
+        val value = parseParametersToValue(
+            Offset(10f, 20f),
+            listOf("a", "b"),
+            listOf(50f, 60f)
+        )
+        assertNull(value)
+    }
+
+    @Test
+    fun intIsParsed() {
+        val value = parseParametersToValue(10, 20, 30)
+        assertEquals(TargetState(20, 30), value)
+    }
+
+    @Test
+    fun intIsParsedAsList() {
+        val value = parseParametersToValue(10, listOf(20), listOf(30))
+        assertEquals(TargetState(20, 30), value)
+    }
+
+    @Test
+    fun stringIsParsed() {
+        val value = parseParametersToValue("a", "b", "c")
+        assertEquals(TargetState("b", "c"), value)
+    }
+
+    @Test
+    fun stringIsParsedAsList() {
+        val value = parseParametersToValue("a", listOf("b"), listOf("c"))
+        assertEquals(TargetState("b", "c"), value)
+    }
+
+    @Test
+    fun booleanIsParsed() {
+        val value = parseParametersToValue(currentValue = false, par1 = true, par2 = false)
+        assertEquals(TargetState(initial = true, target = false), value)
+    }
+
+    @Test
+    fun booleanIsParsedAsList() {
+        val value = parseParametersToValue(false, listOf(true), listOf(false))
+        assertEquals(TargetState(initial = true, target = false), value)
+    }
+
+    @Test
+    fun dpIsParsed() {
+        val value = parseParametersToValue(10.dp, 20.dp, 30.dp)
+        assertEquals(TargetState(20.dp, 30.dp), value)
+    }
+
+    @Test
+    fun dpIsParsedAsDoubleAndFloat() {
+        val value = parseParametersToValue(10.dp, 20.0, 30f)
+        assertEquals(TargetState(20.dp, 30.dp), value)
+    }
+
+    @Test
+    fun dpIsParsedAsList() {
+        val value = parseParametersToValue(10.dp, listOf(20f), listOf(30f))
+        assertEquals(TargetState(20.dp, 30.dp), value)
+    }
+
+    @Test
+    fun dpIsParsedAsDoubleAndFloatList() {
+        val value = parseParametersToValue(10.dp, listOf(20.0), listOf(30f))
+        assertEquals(TargetState(20.dp, 30.dp), value)
+    }
+
+    @Test
+    fun intSizeIsParsed() {
+        val value = parseParametersToValue(
+            IntSize(10, 20),
+            IntSize(30, 40),
+            IntSize(50, 60)
+        )
+        assertEquals(TargetState(IntSize(30, 40), IntSize(50, 60)), value)
+    }
+
+    @Test
+    fun intSizeIsParsedAsList() {
+        val value = parseParametersToValue(
+            IntSize(10, 20),
+            listOf(30, 40),
+            listOf(50, 60)
+        )
+        assertEquals(TargetState(IntSize(30, 40), IntSize(50, 60)), value)
+    }
+
+    @Test
+    fun intOffsetIsParsed() {
+        val value = parseParametersToValue(
+            IntOffset(10, 20),
+            IntOffset(30, 40),
+            IntOffset(50, 60)
+        )
+        assertEquals(TargetState(IntOffset(30, 40), IntOffset(50, 60)), value)
+    }
+
+    @Test
+    fun intOffsetIsParsedAsList() {
+        val value = parseParametersToValue(
+            IntOffset(10, 20),
+            listOf(30, 40),
+            listOf(50, 60)
+        )
+        assertEquals(TargetState(IntOffset(30, 40), IntOffset(50, 60)), value)
+    }
+
+    @Test
+    fun sizeIsParsed() {
+        val value = parseParametersToValue(
+            Size(10f, 20f),
+            Size(30f, 40f),
+            Size(50f, 60f)
+        )
+        assertEquals(TargetState(Size(30f, 40f), Size(50f, 60f)), value)
+    }
+
+    @Test
+    fun sizeIsParsedAsList() {
+        val value = parseParametersToValue(
+            Size(10f, 20f),
+            listOf(30f, 40f),
+            listOf(50f, 60f)
+        )
+        assertEquals(TargetState(Size(30f, 40f), Size(50f, 60f)), value)
+    }
+
+    @Test
+    fun offsetIsParsed() {
+        val value = parseParametersToValue(
+            Offset(10f, 20f),
+            Offset(30f, 40f),
+            Offset(50f, 60f)
+        )
+        assertEquals(TargetState(Offset(30f, 40f), Offset(50f, 60f)), value)
+    }
+
+    @Test
+    fun offsetIsParsedAsList() {
+        val value = parseParametersToValue(
+            Offset(10f, 20f),
+            listOf(30f, 40f),
+            listOf(50f, 60f)
+        )
+        assertEquals(TargetState(Offset(30f, 40f), Offset(50f, 60f)), value)
+    }
+
+    @Test
+    fun rectIsParsed() {
+        val value = parseParametersToValue(
+            Rect(10f, 20f, 30f, 40f),
+            Rect(50f, 60f, 70f, 80f),
+            Rect(90f, 100f, 110f, 120f)
+        )
+        assertEquals(
+            TargetState(
+                Rect(50f, 60f, 70f, 80f),
+                Rect(90f, 100f, 110f, 120f)
+            ), value
+        )
+    }
+
+    @Test
+    fun rectIsParsedAsList() {
+        val value = parseParametersToValue(
+            Rect(10f, 20f, 30f, 40f),
+            listOf(50f, 60f, 70f, 80f),
+            listOf(90f, 100f, 110f, 120f)
+        )
+        assertEquals(
+            TargetState(
+                Rect(50f, 60f, 70f, 80f),
+                Rect(90f, 100f, 110f, 120f)
+            ), value
+        )
+    }
+
+    @Test
+    fun colorIsParsed() {
+        val value = parseParametersToValue(
+            Color(0.1f, 0.2f, 0.3f, 0.4f),
+            Color(0.5f, 0.6f, 0.7f, 0.8f),
+            Color(0.55f, 0.65f, 0.75f, 0.85f)
+        )
+        assertEquals(
+            TargetState(
+                Color(0.5f, 0.6f, 0.7f, 0.8f),
+                Color(0.55f, 0.65f, 0.75f, 0.85f)
+            ), value
+        )
+    }
+
+    @Test
+    fun colorIsParsedAsList() {
+        val value = parseParametersToValue(
+            Color(0.1f, 0.2f, 0.3f, 0.4f),
+            listOf(0.5f, 0.6f, 0.7f, 0.8f),
+            listOf(0.55f, 0.65f, 0.75f, 0.85f)
+        )
+        assertEquals(
+            TargetState(
+                Color(0.5f, 0.6f, 0.7f, 0.8f),
+                Color(0.55f, 0.65f, 0.75f, 0.85f)
+            ), value
+        )
+    }
+}
\ No newline at end of file
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..aea52a6 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
@@ -642,24 +642,25 @@
         override val savedStateRegistry: SavedStateRegistry
             get() = controller.savedStateRegistry
 
-        override fun getLifecycle(): Lifecycle = lifecycleRegistry
+        override val lifecycle: LifecycleRegistry
+            get() = lifecycleRegistry
     }
 
     private val FakeViewModelStoreOwner = object : ViewModelStoreOwner {
-        private val viewModelStore = ViewModelStore()
+        private val vmStore = ViewModelStore()
 
-        override fun getViewModelStore() = viewModelStore
+        override val viewModelStore = vmStore
     }
 
     private val FakeOnBackPressedDispatcherOwner = object : OnBackPressedDispatcherOwner {
-        private val onBackPressedDispatcher = OnBackPressedDispatcher()
+        override val onBackPressedDispatcher = OnBackPressedDispatcher()
 
-        override fun getOnBackPressedDispatcher() = onBackPressedDispatcher
-        override fun getLifecycle() = FakeSavedStateRegistryOwner.lifecycleRegistry
+        override val lifecycle: LifecycleRegistry
+            get() = FakeSavedStateRegistryOwner.lifecycleRegistry
     }
 
     private val FakeActivityResultRegistryOwner = object : ActivityResultRegistryOwner {
-        private val activityResultRegistry = object : ActivityResultRegistry() {
+        override val activityResultRegistry = object : ActivityResultRegistry() {
             override fun <I : Any?, O : Any?> onLaunch(
                 requestCode: Int,
                 contract: ActivityResultContract<I, O>,
@@ -669,7 +670,5 @@
                 throw IllegalStateException("Calling launch() is not supported in Preview")
             }
         }
-
-        override fun getActivityResultRegistry(): ActivityResultRegistry = activityResultRegistry
     }
 }
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt
index 85fa93c..74f9023 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt
@@ -38,6 +38,7 @@
 private const val ANIMATED_VISIBILITY = "AnimatedVisibility"
 private const val ANIMATE_VALUE_AS_STATE = "animateValueAsState"
 private const val REMEMBER = "remember"
+private const val REMEMBER_INFINITE_TRANSITION = "rememberInfiniteTransition"
 private const val REMEMBER_UPDATED_STATE = "rememberUpdatedState"
 private const val SIZE_ANIMATION_MODIFIER = "androidx.compose.animation.SizeAnimationModifier"
 
@@ -58,9 +59,12 @@
 }
 
 @OptIn(UiToolingDataApi::class)
-private inline fun <reified T> Group.findData(): T? {
+private inline fun <reified T> Group.findData(includeGrandchildren: Boolean = false): T? {
     // Search in self data and children data
-    return (data + children.flatMap { it.data }).firstOrNull { data ->
+    val dataToSearch = data + children.let {
+        if (includeGrandchildren) (it + it.flatMap { child -> child.children }) else it
+    }.flatMap { it.data }
+    return dataToSearch.firstOrNull { data ->
         data is T
     } as? T
 }
@@ -201,11 +205,13 @@
 
         private fun findAnimations(groupsWithLocation: Collection<Group>):
             List<InfiniteTransitionSearchInfo> {
-            val groups = groupsWithLocation.filter { group -> group.name == "run" }
-                .filterIsInstance<CallGroup>()
+            val groups =
+                groupsWithLocation.filter { group -> group.name == REMEMBER_INFINITE_TRANSITION }
+                    .filterIsInstance<CallGroup>()
+
             return groups.mapNotNull {
                 val infiniteTransition = it.findData<InfiniteTransition>()
-                val toolingOverride = it.findData<MutableState<State<Long>?>>()
+                val toolingOverride = it.findData<MutableState<State<Long>?>>(true)
                 if (infiniteTransition != null && toolingOverride != null) {
                     if (toolingOverride.value == null) {
                         toolingOverride.value = ToolingState(0L)
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/Utils.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/Utils.kt
index fad8a64..ae1f769 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/Utils.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/Utils.kt
@@ -98,11 +98,13 @@
                     animationSpec.initialStartOffset.offsetMillis
                 else 0L
             }
+
             is InfiniteRepeatableSpec<*> -> {
                 if (animationSpec.initialStartOffset.offsetType == StartOffsetType.Delay)
                     animationSpec.initialStartOffset.offsetMillis
                 else 0L
             }
+
             is VectorizedDurationBasedAnimationSpec<*> -> animationSpec.delayMillis
             else -> 0L
         }.toLong()
@@ -154,19 +156,42 @@
     )
 }
 
+/**
+ * [parseParametersToValue] makes sure what [currentValue], [par1], [par2] have the same types and
+ * returned [TargetState] always has correct and the same type as [currentValue].
+ */
 @Suppress("UNCHECKED_CAST")
 internal fun <T> parseParametersToValue(currentValue: T, par1: Any, par2: Any?): TargetState<T>? {
 
     currentValue ?: return null
 
+    /** Check if [par1] and [par2] are not null and have the same type. */
     fun parametersAreValid(par1: Any?, par2: Any?): Boolean {
         return par1 != null && par2 != null && par1::class == par2::class
     }
 
+    /** Check if all parameters have the same type. */
     fun parametersHasTheSameType(value: Any, par1: Any, par2: Any): Boolean {
         return value::class == par1::class && value::class == par2::class
     }
 
+    fun getDp(par: Any): Dp? {
+        return (par as? Dp) ?: (par as? Float)?.dp ?: (par as? Double)?.dp ?: (par as? Int)?.dp
+    }
+
+    fun parseDp(par1: Any, par2: Any?): TargetState<Dp>? {
+        if (currentValue !is Dp || par2 == null) return null
+        return if (par1 is Dp && par2 is Dp)
+            TargetState(par1, par2) else {
+            val dp1 = getDp(par1)
+            val dp2 = getDp(par2)
+            if (dp1 != null && dp2 != null)
+                TargetState(dp1, dp2) else null
+        }
+    }
+    // Dp could be presented as Float/Double/Int - try to parse it.
+    parseDp(par1, par2)?.let { return it as TargetState<T> }
+
     if (!parametersAreValid(par1, par2)) return null
 
     if (parametersHasTheSameType(currentValue, par1, par2!!)) {
@@ -227,12 +252,8 @@
                     ),
                 )
 
-                is Dp -> {
-                    if (parametersHasTheSameType(currentValue, par1[0]!!, par2[0]!!))
-                        TargetState(par1[0], par2[0]) else TargetState(
-                        (par1[0] as Float).dp, (par2[0] as Float).dp
-                    )
-                }
+                is Dp ->
+                    parseDp(par1[0]!!, par2[0]!!)
 
                 else -> {
                     if (parametersAreValid(par1[0], par2[0]) &&
diff --git a/compose/ui/ui-unit/api/current.txt b/compose/ui/ui-unit/api/current.txt
index 353e98d..8c0257c 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 roundToIntRect(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..cdfac57 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 roundToIntRect(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..c5ff9fa 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 roundToIntRect(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..92e9791 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.roundToIntRect(): 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..d1f93a7 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).roundToIntRect(),
+        )
+    }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index bbb411c..4eea595 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -1761,6 +1761,15 @@
     method public void resetTracking();
   }
 
+  public final class VelocityTracker1D {
+    ctor public VelocityTracker1D(boolean isDataDifferential);
+    method public void addDataPoint(long timeMillis, float dataPoint);
+    method public float calculateVelocity();
+    method public boolean isDataDifferential();
+    method public void resetTracking();
+    property public final boolean isDataDifferential;
+  }
+
   public final class VelocityTrackerKt {
     method public static void addPointerInputChange(androidx.compose.ui.input.pointer.util.VelocityTracker, androidx.compose.ui.input.pointer.PointerInputChange event);
   }
@@ -2080,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 004340e..d167cda 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -1937,6 +1937,15 @@
     method public void resetTracking();
   }
 
+  public final class VelocityTracker1D {
+    ctor public VelocityTracker1D(boolean isDataDifferential);
+    method public void addDataPoint(long timeMillis, float dataPoint);
+    method public float calculateVelocity();
+    method public boolean isDataDifferential();
+    method public void resetTracking();
+    property public final boolean isDataDifferential;
+  }
+
   public final class VelocityTrackerKt {
     method public static void addPointerInputChange(androidx.compose.ui.input.pointer.util.VelocityTracker, androidx.compose.ui.input.pointer.PointerInputChange event);
   }
@@ -2271,7 +2280,7 @@
   }
 
   public static fun interface PinnableContainer.PinnedHandle {
-    method public void unpin();
+    method public void release();
   }
 
   public final class PinnableContainerKt {
@@ -2585,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 a480734..6165576 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -1761,6 +1761,15 @@
     method public void resetTracking();
   }
 
+  public final class VelocityTracker1D {
+    ctor public VelocityTracker1D(boolean isDataDifferential);
+    method public void addDataPoint(long timeMillis, float dataPoint);
+    method public float calculateVelocity();
+    method public boolean isDataDifferential();
+    method public void resetTracking();
+    property public final boolean isDataDifferential;
+  }
+
   public final class VelocityTrackerKt {
     method public static void addPointerInputChange(androidx.compose.ui.input.pointer.util.VelocityTracker, androidx.compose.ui.input.pointer.PointerInputChange event);
   }
@@ -2083,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/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/ModifiersBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/ModifiersBenchmark.kt
index 6df4dd7..226b652 100644
--- a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/ModifiersBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/ModifiersBenchmark.kt
@@ -47,7 +47,9 @@
 import androidx.compose.ui.focus.focusTarget
 import androidx.compose.ui.focus.onFocusEvent
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.semantics
@@ -132,6 +134,12 @@
                     CircleShape
                 )
             },
+            *modifier("graphicsLayer") {
+                Modifier.graphicsLayer(
+                    translationX = if (it) 1f else 2f,
+                    shape = if (it) RectangleShape else CircleShape
+                )
+            }
         )
 
         private val focusRequester = FocusRequester()
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index db53f5c..d7dd5c0 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -71,6 +71,7 @@
         implementation("androidx.autofill:autofill:1.0.0")
         implementation(libs.kotlinCoroutinesAndroid)
 
+        implementation(project(":activity:activity"))
         implementation("androidx.activity:activity-ktx:1.5.1")
         implementation("androidx.core:core:1.9.0")
         implementation('androidx.collection:collection:1.0.0')
@@ -78,7 +79,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")
 
@@ -168,6 +169,7 @@
                 implementation("androidx.autofill:autofill:1.0.0")
                 implementation(libs.kotlinCoroutinesAndroid)
 
+                implementation(project(":activity:activity"))
                 implementation("androidx.activity:activity-ktx:1.5.1")
                 implementation("androidx.core:core:1.9.0")
                 implementation('androidx.collection:collection:1.0.0')
@@ -175,7 +177,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")
             }
 
@@ -243,6 +245,7 @@
                 implementation("androidx.recyclerview:recyclerview:1.3.0-alpha02")
                 implementation("androidx.core:core-ktx:1.2.0")
                 implementation("androidx.activity:activity-compose:1.5.1")
+                implementation(project(":lifecycle:lifecycle-common"))
             }
 
             desktopTest.dependencies {
@@ -291,6 +294,9 @@
     sourceSets.androidTest.assets.srcDirs +=
             project.rootDir.absolutePath + "/../../golden/compose/ui/ui"
     namespace "androidx.compose.ui"
+    // namespace has to be unique, but default androidx.compose.ui.test package is taken by
+    // the androidx.compose.ui:ui-test library
+    testNamespace "androidx.compose.ui.tests"
 }
 
 // Diagnostics for b/188565660
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/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index c2ea1fd..1e844e4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
@@ -86,8 +86,8 @@
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.ImageBitmap
-import androidx.compose.ui.graphics.toAndroidRect
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.toComposeRect
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.platform.AndroidComposeView
 import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat
@@ -639,7 +639,7 @@
     }
 
     @Test
-    fun testCreateAccessibilityNodeInfo_forTraversalOrder_layout() {
+    fun testCreateAccessibilityNodeInfo_forTraversalBefore_layout() {
         val overlaidText = "Overlaid node text"
         val text1 = "Lorem1 ipsum dolor sit amet, consectetur adipiscing elit.\n"
         val text2 = "Lorem2 ipsum dolor sit amet, consectetur adipiscing elit.\n"
@@ -669,13 +669,46 @@
         // comparison (like SemanticsSort), the third text node should come before the overlaid node
         // — OverlaidNode should be read last
         assertNotEquals(ani3TraversalBeforeVal, 0)
-        if (ani3TraversalBeforeVal != null) {
-            assertEquals(ani3TraversalBeforeVal, overlaidNode.id)
-        }
+        assertEquals(ani3TraversalBeforeVal!!, overlaidNode.id)
     }
 
     @Test
-    fun testCreateAccessibilityNodeInfo_forTraversalOrder_layoutTestTags() {
+    fun testCreateAccessibilityNodeInfo_forTraversalAfter_layout() {
+        val overlaidText = "Overlaid node text"
+        val text1 = "Lorem1 ipsum dolor sit amet, consectetur adipiscing elit.\n"
+        val text2 = "Lorem2 ipsum dolor sit amet, consectetur adipiscing elit.\n"
+        val text3 = "Lorem3 ipsum dolor sit amet, consectetur adipiscing elit.\n"
+        container.setContent {
+            LastElementOverLaidColumn(modifier = Modifier.padding(8.dp)) {
+                Row {
+                    Column {
+                        Row { Text(text1) }
+                        Row { Text(text2) }
+                        Row { Text(text3) }
+                    }
+                }
+                Row {
+                    Text(overlaidText)
+                }
+            }
+        }
+
+        val node3 = rule.onNodeWithText(text3).fetchSemanticsNode()
+        val overlaidNode = rule.onNodeWithText(overlaidText).fetchSemanticsNode()
+
+        val overlaidANI = provider.createAccessibilityNodeInfo(overlaidNode.id)
+        val overlaidTraversalAfterValue =
+            overlaidANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALAFTER_VAL)
+
+        // Nodes 1, 2, and 3 are all children of a larger column; this means with a hierarchy
+        // comparison (like SemanticsSort), the third text node should come before the overlaid node
+        // — OverlaidNode should be read last
+        assertNotEquals(overlaidTraversalAfterValue, 0)
+        assertEquals(overlaidTraversalAfterValue!!, node3.id)
+    }
+
+    @Test
+    fun testCreateAccessibilityNodeInfo_forTraversalBefore_layoutTestTags() {
         val overlaidText = "Overlaid node text"
         val text1 = "Lorem1 ipsum dolor sit amet, consectetur adipiscing elit.\n"
         val text2 = "Lorem2 ipsum dolor sit amet, consectetur adipiscing elit.\n"
@@ -711,14 +744,15 @@
         // comparison (like SemanticsSort), the third text node should come before the overlaid node
         // — OverlaidNode and its row should be read last
         assertNotEquals(ani3TraversalBeforeVal, 0)
-        if (ani3TraversalBeforeVal != null) {
-            assertTrue(ani3TraversalBeforeVal < row2.id)
-        }
+        assertTrue(ani3TraversalBeforeVal!! < row2.id)
     }
 
     companion object {
         private const val EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL =
             "android.view.accessibility.extra.EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL"
+
+        private const val EXTRA_DATA_TEST_TRAVERSALAFTER_VAL =
+            "android.view.accessibility.extra.EXTRA_DATA_TEST_TRAVERSALAFTER_VAL"
     }
 
     @Test
@@ -1116,7 +1150,7 @@
             textFieldNode.positionInWindow
         )
         val expectedTopLeftInScreenCoords = androidComposeView.localToScreen(
-            expectedRectInLocalCoords.toAndroidRect().topLeftToOffset()
+            expectedRectInLocalCoords.topLeft
         )
         assertEquals(expectedTopLeftInScreenCoords.x, rectF.left)
         assertEquals(expectedTopLeftInScreenCoords.y, rectF.top)
@@ -2315,6 +2349,72 @@
     }
 
     @Test
+    fun testMultiPanesDisappear() {
+        val firstPaneTag = "Pane 1"
+        val secondPaneTag = "Pane 2"
+        var isPaneVisible by mutableStateOf(false)
+        val firstPaneTestTitle by mutableStateOf("first pane title")
+        val secondPaneTestTitle by mutableStateOf("second pane title")
+
+        container.setContent {
+            if (isPaneVisible) {
+                Column {
+                    with(LocalDensity.current) {
+                        Box(
+                            Modifier
+                                .size(100.toDp())
+                                .testTag(firstPaneTag)
+                                .semantics { paneTitle = firstPaneTestTitle }) {}
+                        Box(
+                            Modifier
+                                .size(100.toDp())
+                                .testTag(secondPaneTag)
+                                .semantics { paneTitle = secondPaneTestTitle }) {}
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstPaneTag).assertDoesNotExist()
+        rule.onNodeWithTag(secondPaneTag).assertDoesNotExist()
+
+        isPaneVisible = true
+        rule.onNodeWithTag(firstPaneTag)
+            .assert(
+                SemanticsMatcher.expectValue(
+                    SemanticsProperties.PaneTitle,
+                    "first pane title"
+                )
+            )
+            .assertIsDisplayed()
+        rule.onNodeWithTag(secondPaneTag)
+            .assert(
+                SemanticsMatcher.expectValue(
+                    SemanticsProperties.PaneTitle,
+                    "second pane title"
+                )
+            )
+            .assertIsDisplayed()
+        waitForSubtreeEventToSend()
+
+        isPaneVisible = false
+        rule.onNodeWithTag(firstPaneTag).assertDoesNotExist()
+        rule.onNodeWithTag(secondPaneTag).assertDoesNotExist()
+        rule.runOnIdle {
+            verify(container, times(2)).requestSendAccessibilityEvent(
+                eq(androidComposeView),
+                argThat(
+                    ArgumentMatcher {
+                        it.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED &&
+                            it.contentChangeTypes ==
+                            AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED
+                    }
+                )
+            )
+        }
+    }
+
+    @Test
     fun testEventForPasswordTextField() {
         val tag = "TextField"
         container.setContent {
@@ -2832,7 +2932,7 @@
         accessibilityNodeInfo.getBoundsInScreen(rect)
         val resultWidth = rect.right - rect.left
         val resultHeight = rect.bottom - rect.top
-        val resultInLocalCoords = androidComposeView.screenToLocal(rect.topLeftToOffset())
+        val resultInLocalCoords = androidComposeView.screenToLocal(rect.toComposeRect().topLeft)
 
         assertEquals(size, resultWidth)
         assertEquals(size, resultHeight)
@@ -3366,5 +3466,3 @@
             )
     }
 }
-
-private fun Rect.topLeftToOffset() = Offset(this.left.toFloat(), this.top.toFloat())
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/AndroidLayoutDrawTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
index 3dc330f..e27cd97 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
@@ -29,7 +29,6 @@
 import android.view.ViewGroup
 import android.view.ViewTreeObserver
 import android.widget.FrameLayout
-import android.widget.LinearLayout
 import androidx.activity.compose.setContent
 import androidx.annotation.RequiresApi
 import androidx.compose.animation.core.animateFloatAsState
@@ -125,8 +124,8 @@
 import org.junit.Assert.assertSame
 import org.junit.Assert.assertTrue
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -3099,60 +3098,6 @@
         validateSquareColors(outerColor = Color.Blue, innerColor = Color.White, size = 10)
     }
 
-    @Ignore // b/173806298
-    @Test
-    fun makingItemLarger() {
-        var height by mutableStateOf(30)
-        var latch = CountDownLatch(1)
-        var composeView: View? = null
-        activityTestRule.runOnUiThread {
-            val linearLayout = LinearLayout(activity)
-            linearLayout.orientation = LinearLayout.VERTICAL
-            val child = ComposeView(activity)
-            activity.setContentView(linearLayout)
-            linearLayout.addView(
-                child,
-                LinearLayout.LayoutParams(
-                    ViewGroup.LayoutParams.MATCH_PARENT,
-                    ViewGroup.LayoutParams.WRAP_CONTENT,
-                    1f
-                )
-            )
-            linearLayout.addView(
-                View(activity),
-                LinearLayout.LayoutParams(
-                    ViewGroup.LayoutParams.MATCH_PARENT,
-                    0,
-                    10000f
-                )
-            )
-            child.setContent {
-                Layout(
-                    {},
-                    Modifier.onGloballyPositioned {
-                        latch.countDown()
-                    }
-                ) { _, constraints ->
-                    layout(constraints.maxWidth, height.coerceAtMost(constraints.maxHeight)) {}
-                }
-            }
-            composeView = child
-        }
-
-        assertTrue(latch.await(1, TimeUnit.SECONDS))
-        latch = CountDownLatch(1)
-
-        activityTestRule.runOnUiThread {
-            assertEquals(height, composeView!!.measuredHeight)
-            height = 60
-        }
-
-        assertTrue(latch.await(1, TimeUnit.SECONDS))
-        activityTestRule.runOnUiThread {
-            assertEquals(height, composeView!!.measuredHeight)
-        }
-    }
-
     // Make sure that when the child of a layer changes that the drawing changes to match.
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     @Test
@@ -3328,6 +3273,7 @@
         }
     }
 
+    @Ignore // b/266748671
     // Tests that an invalidation on a detached view will draw correctly when attached.
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     @Test
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ShadowTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ShadowTest.kt
index 53b352a..f9d9136d 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ShadowTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ShadowTest.kt
@@ -56,6 +56,7 @@
 import org.junit.Assert.assertNotEquals
 import org.junit.Assert.assertTrue
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -123,6 +124,7 @@
         }
     }
 
+    @Ignore // b/266748959
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     @Test
     fun switchFromShadowToNoShadow() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorInvalidationTestCase.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorInvalidationTestCase.kt
index b4aa669..6696cb1 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorInvalidationTestCase.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorInvalidationTestCase.kt
@@ -22,12 +22,12 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.AtLeastSize
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.paint
 import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.draw.paint
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.test.R
+import androidx.compose.ui.tests.R
 
 class VectorInvalidationTestCase() {
 
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
index 8208a50..eff49cc 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
@@ -48,10 +48,10 @@
 import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.graphics.asAndroidBitmap
-import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.graphics.toPixelMap
 import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
 import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.graphics.toPixelMap
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
@@ -59,20 +59,21 @@
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.res.ImageVectorCache
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.LayoutDirection
-
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onRoot
 import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.R
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.tests.R
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
 import org.junit.Assert
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertTrue
@@ -80,8 +81,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParserTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParserTest.kt
index 01c9518..4a724a3 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParserTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParserTest.kt
@@ -26,7 +26,7 @@
 import androidx.compose.ui.graphics.vector.VectorNode
 import androidx.compose.ui.graphics.vector.VectorPath
 import androidx.compose.ui.res.vectorResource
-import androidx.compose.ui.test.R
+import androidx.compose.ui.tests.R
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/InputModeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/InputModeTest.kt
index 1c940a9..9a5fc62 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/InputModeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/InputModeTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.input
 
+import android.os.Build
 import android.view.View
 import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.Composable
@@ -56,6 +57,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun initialInputMode() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         // Arrange.
         rule.setContentWithInputManager {
             Box {}
@@ -68,6 +73,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun switchToTouchModeProgrammatically() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         // Arrange.
         rule.setContentWithInputManager {
             Box {}
@@ -97,6 +106,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun switchToKeyboardModeProgrammatically() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         // Arrange.
         val testTag = "Box"
         rule.setContentWithInputManager {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
index 6845b6a..30f35ea 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
@@ -42,6 +42,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -182,6 +183,7 @@
         }
     }
 
+    @Ignore("b/265319988")
     @Test
     fun onFocusAwareEvent_isTriggered() {
         // Arrange.
@@ -203,6 +205,7 @@
         rule.runOnIdle { assertThat(sentEvent).isEqualTo(receivedEvent) }
     }
 
+    @Ignore("b/264466323")
     @Test
     fun onPreFocusAwareEvent_triggered() {
         // Arrange.
@@ -276,6 +279,7 @@
         }
     }
 
+    @Ignore // b/266984867
     @Test
     fun onPreFocusAwareEvent_triggeredBefore_onFocusAwareEvent_1() {
         // Arrange.
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/IntrinsicsMeasurementTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/IntrinsicsMeasurementTest.kt
index 4e867ba4..8629e64 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/IntrinsicsMeasurementTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/IntrinsicsMeasurementTest.kt
@@ -279,8 +279,8 @@
                         placeable.place(0, 0)
                     }
                 }
+                val placeable = measurables[0].measure(constraints)
                 return layout(100, 100) {
-                    val placeable = measurables[0].measure(constraints)
                     placeable.place(0, 0)
                 }
             }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/ResizingComposeViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/ResizingComposeViewTest.kt
new file mode 100644
index 0000000..3e4c2be
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/ResizingComposeViewTest.kt
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.layout
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.test.TestActivity
+import androidx.compose.ui.unit.Constraints
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class ResizingComposeViewTest {
+
+    private var drawLatch = CountDownLatch(1)
+    private lateinit var composeView: ComposeView
+
+    @Before
+    fun setup() {
+        composeView = ComposeView(rule.activity)
+    }
+
+    @Suppress("DEPRECATION")
+    @get:Rule
+    val rule = androidx.test.rule.ActivityTestRule(
+        TestActivity::class.java
+    )
+
+    @Test
+    fun whenParentIsMeasuringTwiceWithDifferentConstraints() {
+        var height by mutableStateOf(10)
+        rule.runOnUiThread {
+            val linearLayout = LinearLayout(rule.activity)
+            linearLayout.orientation = LinearLayout.VERTICAL
+            rule.activity.setContentView(linearLayout)
+            linearLayout.addView(
+                composeView,
+                LinearLayout.LayoutParams(
+                    ViewGroup.LayoutParams.MATCH_PARENT,
+                    ViewGroup.LayoutParams.WRAP_CONTENT,
+                    1f
+                )
+            )
+            linearLayout.addView(
+                View(rule.activity),
+                LinearLayout.LayoutParams(
+                    ViewGroup.LayoutParams.MATCH_PARENT,
+                    0,
+                    10000f
+                )
+            )
+            composeView.setContent {
+                ResizingChild(layoutHeight = { height })
+            }
+        }
+
+        awaitDrawAndAssertSizes()
+        rule.runOnUiThread {
+            height = 20
+            drawLatch = CountDownLatch(1)
+        }
+
+        awaitDrawAndAssertSizes()
+    }
+
+    @Test
+    fun whenMeasuredWithWrapContent() {
+        var height by mutableStateOf(10)
+
+        rule.runOnUiThread {
+            rule.activity.setContentView(
+                composeView, WrapContentLayoutParams
+            )
+            composeView.setContent {
+                ResizingChild(layoutHeight = { height })
+            }
+        }
+
+        awaitDrawAndAssertSizes()
+        rule.runOnUiThread {
+            height = 20
+            drawLatch = CountDownLatch(1)
+        }
+
+        awaitDrawAndAssertSizes()
+    }
+
+    @Test
+    fun whenMeasuredWithFixedConstraints() {
+        var childHeight by mutableStateOf(10)
+        val viewSize = 30
+        val parent = RequestLayoutTrackingFrameLayout(rule.activity)
+
+        rule.runOnUiThread {
+            parent.addView(composeView, ViewGroup.LayoutParams(viewSize, viewSize))
+            rule.activity.setContentView(parent, WrapContentLayoutParams)
+            composeView.setContent {
+                ResizingChild(layoutHeight = { childHeight }, viewHeight = { viewSize })
+            }
+        }
+
+        awaitDrawAndAssertSizes()
+        rule.runOnUiThread {
+            childHeight = 20
+            drawLatch = CountDownLatch(1)
+            parent.requestLayoutCalled = false
+        }
+
+        awaitDrawAndAssertSizes()
+        // as the ComposeView is measured with fixed size parent shouldn't be remeasured
+        assertThat(parent.requestLayoutCalled).isFalse()
+    }
+
+    @Test
+    fun whenInsideComposableParentWithFixedSize() {
+        var childHeight by mutableStateOf(10)
+        val parentSize = 30
+        val parent = RequestLayoutTrackingFrameLayout(rule.activity)
+
+        rule.runOnUiThread {
+            parent.addView(composeView, WrapContentLayoutParams)
+            rule.activity.setContentView(parent, WrapContentLayoutParams)
+            composeView.setContent {
+                Layout(
+                    modifier = Modifier.layout { measurable, _ ->
+                        // this modifier sets a fixed size on a parent similarly to how
+                        // Modifier.fillMaxSize() or Modifier.size(foo) would do
+                        val placeable =
+                            measurable.measure(Constraints.fixed(parentSize, parentSize))
+                        layout(placeable.width, placeable.height) {
+                            placeable.place(0, 0)
+                        }
+                    },
+                    content = {
+                        ResizingChild(layoutHeight = { childHeight }, viewHeight = { parentSize })
+                    }
+                ) { measurables, constraints ->
+                    val placeable = measurables[0].measure(constraints)
+                    layout(placeable.width, placeable.height) {
+                        placeable.place(0, 0)
+                    }
+                }
+            }
+        }
+
+        awaitDrawAndAssertSizes()
+        rule.runOnUiThread {
+            childHeight = 20
+            drawLatch = CountDownLatch(1)
+            parent.requestLayoutCalled = false
+        }
+
+        awaitDrawAndAssertSizes()
+        // as the child is not affecting size parent view shouldn't be remeasured
+        assertThat(parent.requestLayoutCalled).isFalse()
+    }
+
+    @Test
+    fun whenParentIsMeasuringInLayoutBlock() {
+        var childHeight by mutableStateOf(10)
+        val parentSize = 30
+        val parent = RequestLayoutTrackingFrameLayout(rule.activity)
+
+        rule.runOnUiThread {
+            parent.addView(composeView, WrapContentLayoutParams)
+            rule.activity.setContentView(parent, WrapContentLayoutParams)
+            composeView.setContent {
+                Layout(
+                    content = {
+                        ResizingChild(layoutHeight = { childHeight }, viewHeight = { parentSize })
+                    }
+                ) { measurables, _ ->
+                    layout(parentSize, parentSize) {
+                        val placeable =
+                            measurables[0].measure(Constraints.fixed(parentSize, parentSize))
+                        placeable.place(0, 0)
+                    }
+                }
+            }
+        }
+
+        awaitDrawAndAssertSizes()
+        rule.runOnUiThread {
+            childHeight = 20
+            drawLatch = CountDownLatch(1)
+            parent.requestLayoutCalled = false
+        }
+
+        awaitDrawAndAssertSizes()
+        // as the child is not affecting size parent view shouldn't be remeasured
+        assertThat(parent.requestLayoutCalled).isFalse()
+    }
+
+    @Test
+    fun whenParentIsSettingFixedIntrinsicsSize() {
+        var intrinsicsHeight by mutableStateOf(10)
+        val parent = RequestLayoutTrackingFrameLayout(rule.activity)
+
+        rule.runOnUiThread {
+            parent.addView(composeView, WrapContentLayoutParams)
+            rule.activity.setContentView(parent, WrapContentLayoutParams)
+            composeView.setContent {
+                Layout(
+                    modifier = Modifier.layout { measurable, _ ->
+                        val intrinsicsSize = measurable.minIntrinsicHeight(Int.MAX_VALUE)
+                        val placeable =
+                            measurable.measure(Constraints.fixed(intrinsicsSize, intrinsicsSize))
+                        layout(placeable.width, placeable.height) {
+                            placeable.place(0, 0)
+                        }
+                    },
+                    content = {
+                        IntrinsicsChild(intrinsicsHeight = { intrinsicsHeight })
+                    }
+                ) { measurables, constraints ->
+                    val placeable = measurables[0].measure(constraints)
+                    layout(placeable.width, placeable.height) {
+                        placeable.place(0, 0)
+                    }
+                }
+            }
+        }
+
+        awaitDrawAndAssertSizes()
+        rule.runOnUiThread {
+            intrinsicsHeight = 20
+            drawLatch = CountDownLatch(1)
+        }
+
+        awaitDrawAndAssertSizes()
+    }
+
+    private fun awaitDrawAndAssertSizes() {
+        Assert.assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
+        // size assertion is done inside Modifier.drawBehind() which calls countDown() on the latch
+    }
+
+    @Composable
+    private fun ResizingChild(
+        layoutHeight: () -> Int,
+        viewHeight: () -> Int = layoutHeight,
+    ) {
+        Layout(
+            {},
+            Modifier.drawBehind {
+                val expectedLayoutHeight = Snapshot.withoutReadObservation { layoutHeight() }
+                assertWithMessage("Layout size is wrong")
+                    .that(size.height.roundToInt()).isEqualTo(expectedLayoutHeight)
+                val expectedViewHeight = Snapshot.withoutReadObservation { viewHeight() }
+                assertWithMessage("ComposeView size is wrong")
+                    .that(composeView.measuredHeight).isEqualTo(expectedViewHeight)
+                drawLatch.countDown()
+            }
+        ) { _, constraints ->
+            layout(constraints.maxWidth, layoutHeight()) {}
+        }
+    }
+
+    @Composable
+    private fun IntrinsicsChild(
+        intrinsicsHeight: () -> Int
+    ) {
+        Layout(
+            {},
+            Modifier.drawBehind {
+                val expectedHeight = Snapshot.withoutReadObservation { intrinsicsHeight() }
+                assertWithMessage("Layout size is wrong")
+                    .that(size.height.roundToInt()).isEqualTo(expectedHeight)
+                assertWithMessage("ComposeView size is wrong")
+                    .that(composeView.measuredHeight).isEqualTo(expectedHeight)
+                drawLatch.countDown()
+            },
+            object : MeasurePolicy {
+                override fun MeasureScope.measure(
+                    measurables: List<Measurable>,
+                    constraints: Constraints
+                ): MeasureResult {
+                    return layout(constraints.maxWidth, constraints.maxHeight) {}
+                }
+
+                override fun IntrinsicMeasureScope.minIntrinsicHeight(
+                    measurables: List<IntrinsicMeasurable>,
+                    width: Int
+                ): Int = intrinsicsHeight()
+            }
+        )
+    }
+}
+
+private class RequestLayoutTrackingFrameLayout(context: Context) : FrameLayout(context) {
+
+    var requestLayoutCalled = false
+
+    override fun requestLayout() {
+        super.requestLayout()
+        requestLayoutCalled = true
+    }
+}
+
+private val WrapContentLayoutParams = ViewGroup.LayoutParams(
+    ViewGroup.LayoutParams.WRAP_CONTENT,
+    ViewGroup.LayoutParams.WRAP_CONTENT
+)
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 83d6962..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
@@ -20,6 +20,7 @@
 import android.os.Build
 import android.widget.FrameLayout
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
@@ -50,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
@@ -104,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) {
@@ -141,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)
                 }
@@ -251,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) }
 
@@ -296,7 +311,10 @@
 
         rule.runOnIdle {
             content.value = {
-                Spacer(Modifier.requiredSize(10.dp).testTag(updatedTag))
+                Spacer(
+                    Modifier
+                        .requiredSize(10.dp)
+                        .testTag(updatedTag))
             }
         }
 
@@ -361,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)
@@ -391,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)
@@ -424,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)
@@ -490,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) {}
                 }
@@ -509,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) {
@@ -1299,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)
@@ -1735,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)
@@ -1788,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)
@@ -2040,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"))
             }
         }
 
@@ -2173,6 +2227,34 @@
     }
 
     @Test
+    fun subcomposeLayout_movedToDifferentGroup() {
+        var wrapped by mutableStateOf(false)
+        rule.setContent {
+            val content = remember {
+                movableContentOf {
+                    BoxWithConstraints {
+                        Spacer(
+                            modifier = Modifier.testTag(wrapped.toString()),
+                        )
+                    }
+                }
+            }
+
+            if (wrapped) {
+                Box { content() }
+            } else {
+                content()
+            }
+        }
+
+        rule.runOnIdle {
+            wrapped = !wrapped
+        }
+
+        rule.waitForIdle()
+    }
+
+    @Test
     @Ignore("b/188320755")
     fun forceMeasureOfInactiveElementFromLaunchedEffect() {
         var isActive by mutableStateOf(true)
@@ -2219,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>>
@@ -2239,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/androidAndroidTest/kotlin/androidx/compose/ui/layout/TestRuleExecutesLayoutPassesWhenWaitingForIdleTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/TestRuleExecutesLayoutPassesWhenWaitingForIdleTest.kt
index acf4e1a..ec23d95 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/TestRuleExecutesLayoutPassesWhenWaitingForIdleTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/TestRuleExecutesLayoutPassesWhenWaitingForIdleTest.kt
@@ -32,6 +32,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -104,6 +105,7 @@
         }
     }
 
+    @Ignore("b/265281787")
     @Test
     fun child_AndroidView() {
         val numUpdates = 5
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/ComposeViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/ComposeViewTest.kt
index 2779885..1b702f2 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/ComposeViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/ComposeViewTest.kt
@@ -17,9 +17,9 @@
 package androidx.compose.ui.platform
 
 import android.view.ViewGroup
-import androidx.compose.ui.test.R
 import androidx.compose.ui.test.TestActivity
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.tests.R
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import org.junit.Assert.assertFalse
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/DisposableSaveableStateRegistryTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/DisposableSaveableStateRegistryTest.kt
index 6d919b5..e6e19a8 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/DisposableSaveableStateRegistryTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/DisposableSaveableStateRegistryTest.kt
@@ -25,7 +25,9 @@
 import android.util.SizeF
 import android.util.SparseArray
 import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.LifecycleRegistry
+import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.savedstate.SavedStateRegistry
 import androidx.savedstate.SavedStateRegistryController
 import androidx.savedstate.SavedStateRegistryOwner
@@ -252,19 +254,18 @@
 
 private class TestOwner(
     restoredBundle: Bundle? = null
-) : SavedStateRegistryOwner {
+) : SavedStateRegistryOwner, LifecycleOwner by TestLifecycleOwner(Lifecycle.State.INITIALIZED) {
 
-    private val lifecycle = LifecycleRegistry(this)
     private val controller = SavedStateRegistryController.create(this).apply {
         performRestore(restoredBundle ?: Bundle())
     }
+
     init {
-        lifecycle.currentState = Lifecycle.State.RESUMED
+        (lifecycle as LifecycleRegistry).currentState = Lifecycle.State.RESUMED
     }
 
     override val savedStateRegistry: SavedStateRegistry
         get() = controller.savedStateRegistry
-    override fun getLifecycle(): Lifecycle = lifecycle
 
     fun save() = Bundle().apply {
         controller.performSave(this)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowRecomposerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowRecomposerTest.kt
index 2ec7077..b4d9bbe 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowRecomposerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowRecomposerTest.kt
@@ -38,8 +38,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.core.view.get
 import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.LifecycleRegistry
+import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.test.core.app.ActivityScenario
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
@@ -226,13 +225,9 @@
     fun lifecycleAwareWindowRecomposerJoinsAfterDetach(): Unit = runBlocking {
         ActivityScenario.launch(ComponentActivity::class.java).use { scenario ->
             lateinit var recomposer: Recomposer
-            val lifecycleOwner = object : LifecycleOwner {
-                val lifecycle = LifecycleRegistry(this)
-                override fun getLifecycle(): Lifecycle = lifecycle
-            }
+            val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.RESUMED)
             scenario.onActivity { activity ->
                 val view = View(activity)
-                lifecycleOwner.lifecycle.currentState = Lifecycle.State.RESUMED
                 recomposer = view.createLifecycleAwareWindowRecomposer(
                     lifecycle = lifecycleOwner.lifecycle
                 )
@@ -265,11 +260,7 @@
             lateinit var recomposer: Recomposer
             scenario.onActivity { activity ->
                 val view = View(activity)
-                val lifecycleOwner = object : LifecycleOwner {
-                    val lifecycle = LifecycleRegistry(this)
-                    override fun getLifecycle(): Lifecycle = lifecycle
-                }
-                lifecycleOwner.lifecycle.currentState = Lifecycle.State.RESUMED
+                val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.RESUMED)
                 recomposer = view.createLifecycleAwareWindowRecomposer(
                     lifecycle = lifecycleOwner.lifecycle
                 )
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/ColorResourcesTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/ColorResourcesTest.kt
index e0ec921..cca5c26 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/ColorResourcesTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/ColorResourcesTest.kt
@@ -19,8 +19,8 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.test.R
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tests.R
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/PrimitiveResourcesTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/PrimitiveResourcesTest.kt
index 3703ff2..a503656 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/PrimitiveResourcesTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/PrimitiveResourcesTest.kt
@@ -18,8 +18,8 @@
 
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.test.R
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tests.R
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/StringResourcesTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/StringResourcesTest.kt
index 4b67de4e..9751449 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/StringResourcesTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/StringResourcesTest.kt
@@ -19,8 +19,8 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.test.R
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tests.R
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.platform.app.InstrumentationRegistry
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
index 98804c8..22b0751 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
@@ -61,19 +61,19 @@
 import androidx.compose.ui.platform.ViewCompositionStrategy
 import androidx.compose.ui.platform.findViewTreeCompositionContext
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.R
 import androidx.compose.ui.test.TestActivity
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.tests.R
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
-import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.savedstate.SavedStateRegistry
 import androidx.savedstate.SavedStateRegistryOwner
 import androidx.savedstate.findViewTreeSavedStateRegistryOwner
@@ -458,8 +458,7 @@
     @Test
     fun androidView_propagatesLocalLifecycleOwnerAsViewTreeOwner() {
         lateinit var parentLifecycleOwner: LifecycleOwner
-        // We don't actually need to ever get the actual lifecycle.
-        val compositionLifecycleOwner = LifecycleOwner { throw UnsupportedOperationException() }
+        val compositionLifecycleOwner = TestLifecycleOwner()
         var childViewTreeLifecycleOwner: LifecycleOwner? = null
 
         rule.setContent {
@@ -492,13 +491,12 @@
     @Test
     fun androidView_propagatesLocalSavedStateRegistryOwnerAsViewTreeOwner() {
         lateinit var parentSavedStateRegistryOwner: SavedStateRegistryOwner
-        val compositionSavedStateRegistryOwner = object : SavedStateRegistryOwner {
-            // We don't actually need to ever get actual instances.
-            override fun getLifecycle(): Lifecycle = throw UnsupportedOperationException()
-
-            override val savedStateRegistry: SavedStateRegistry
-                get() = throw UnsupportedOperationException()
-        }
+        val compositionSavedStateRegistryOwner =
+            object : SavedStateRegistryOwner, LifecycleOwner by TestLifecycleOwner() {
+                // We don't actually need to ever get actual instance.
+                override val savedStateRegistry: SavedStateRegistry
+                    get() = throw UnsupportedOperationException()
+            }
         var childViewTreeSavedStateRegistryOwner: SavedStateRegistryOwner? = null
 
         rule.setContent {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
index b203206..0fe1ac4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
@@ -53,12 +53,12 @@
 import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.platform.ViewCompositionStrategy
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.R
 import androidx.compose.ui.test.assertTextEquals
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performScrollTo
 import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.tests.R
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntRect
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt
index 73c9f5e..ea089f4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt
@@ -33,7 +33,6 @@
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.modifier.modifierLocalProvider
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.R
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -41,6 +40,7 @@
 import androidx.compose.ui.test.swipeDown
 import androidx.compose.ui.test.swipeUp
 import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.tests.R
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.round
 import androidx.test.espresso.Espresso.onView
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropTestHelper.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropTestHelper.kt
index 755df89..5392884 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropTestHelper.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropTestHelper.kt
@@ -44,7 +44,7 @@
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.R
+import androidx.compose.ui.tests.R
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
 import androidx.coordinatorlayout.widget.CoordinatorLayout
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
index 2ea1689..67a83e3f 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
@@ -24,11 +24,11 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.test.R
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.tests.R
 import androidx.compose.ui.unit.round
 import androidx.test.espresso.Espresso.onView
 import androidx.test.espresso.assertion.ViewAssertions.matches
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropViewHolderTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropViewHolderTest.kt
index 78bd15c..40f2199 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropViewHolderTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropViewHolderTest.kt
@@ -25,8 +25,8 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.test.R
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tests.R
 import androidx.compose.ui.unit.Velocity
 import androidx.test.espresso.Espresso
 import androidx.test.espresso.Espresso.onView
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/PoolingContainerComposeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/PoolingContainerComposeTest.kt
index 878ccca..a9b7904 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/PoolingContainerComposeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/PoolingContainerComposeTest.kt
@@ -23,7 +23,7 @@
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.ui.platform.AbstractComposeView
 import androidx.compose.ui.platform.ViewCompositionStrategy
-import androidx.compose.ui.test.R
+import androidx.compose.ui.tests.R
 import androidx.customview.poolingcontainer.callPoolingContainerOnRelease
 import androidx.customview.poolingcontainer.isPoolingContainer
 import androidx.test.ext.junit.rules.ActivityScenarioRule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogTest.kt
index 1f5e56a..b6579e5 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogTest.kt
@@ -15,6 +15,7 @@
  */
 package androidx.compose.ui.window
 
+import android.os.Build
 import androidx.activity.compose.BackHandler
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
@@ -266,9 +267,14 @@
         rule.onNodeWithText(defaultText).assertIsDisplayed()
     }
 
+    @Ignore // b/266613263
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun dialogTest_backHandler_isCalled_backButtonPressed() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         val clickCountPrefix = "Click: "
 
         rule.setContent {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt
index a1b44c7..3ef53ac 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt
@@ -27,8 +27,8 @@
 import androidx.annotation.DoNotInline
 import androidx.annotation.RequiresApi
 import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.graphics.toAndroidRect
 import androidx.compose.ui.util.fastMap
+import kotlin.math.roundToInt
 
 /**
  * Autofill implementation for Android.
@@ -46,13 +46,20 @@
     init { view.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_YES }
 
     override fun requestAutofillForNode(autofillNode: AutofillNode) {
+        val boundingBox = autofillNode.boundingBox
+            ?: error("requestAutofill called before onChildPositioned()")
+
         // TODO(b/138731416): Find out what happens when notifyViewEntered() is called multiple times
         // before calling notifyViewExited().
         autofillManager.notifyViewEntered(
             view,
             autofillNode.id,
-            autofillNode.boundingBox?.toAndroidRect()
-                ?: error("requestAutofill called before onChildPositioned()")
+            android.graphics.Rect(
+                boundingBox.left.roundToInt(),
+                boundingBox.top.roundToInt(),
+                boundingBox.right.roundToInt(),
+                boundingBox.bottom.roundToInt()
+            )
         )
     }
 
@@ -89,7 +96,8 @@
                 autofillNode.autofillTypes.fastMap { it.androidType }.toTypedArray()
             )
 
-            if (autofillNode.boundingBox == null) {
+            val boundingBox = autofillNode.boundingBox
+            if (boundingBox == null) {
                 // Do we need an exception here? warning? silently ignore? If the boundingbox is
                 // null, the autofill overlay will not be shown.
                 Log.w(
@@ -97,9 +105,14 @@
                     """Bounding box not set.
                         Did you call perform autofillTree before the component was positioned? """
                 )
-            }
-            autofillNode.boundingBox?.toAndroidRect()?.run {
-                AutofillApi23Helper.setDimens(child, left, top, 0, 0, width(), height())
+            } else {
+                val left = boundingBox.left.roundToInt()
+                val top = boundingBox.top.roundToInt()
+                val right = boundingBox.right.roundToInt()
+                val bottom = boundingBox.bottom.roundToInt()
+                val width = right - left
+                val height = bottom - top
+                AutofillApi23Helper.setDimens(child, left, top, 0, 0, width, height)
             }
         }
         index++
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index 7357ca4..e8c3bc2 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -734,13 +734,17 @@
 
     private fun scheduleMeasureAndLayout(nodeToRemeasure: LayoutNode? = null) {
         if (!isLayoutRequested && isAttachedToWindow) {
-            if (wasMeasuredWithMultipleConstraints && nodeToRemeasure != null) {
-                // if nodeToRemeasure can potentially resize the root and the view was measured
-                // twice with different constraints last time it means the constraints we have could
-                // be not the final constraints and in fact our parent ViewGroup can remeasure us
-                // with larger constraints if we call requestLayout()
+            if (nodeToRemeasure != null) {
+                // if [nodeToRemeasure] can potentially resize the root we should call
+                // requestLayout() so our parent View can react on this change on the same frame.
+                // if instead we just call invalidate() and remeasure inside dispatchDraw()
+                // this will cause inconsistency as the Compose content will already have the
+                // new size, but the View hierarchy will react only on the next frame.
                 var node = nodeToRemeasure
-                while (node != null && node.measuredByParent == UsageByParent.InMeasureBlock) {
+                while (node != null &&
+                    node.measuredByParent == UsageByParent.InMeasureBlock &&
+                    node.childSizeCanAffectParentSize()
+                ) {
                     node = node.parent
                 }
                 if (node === root) {
@@ -757,6 +761,17 @@
         }
     }
 
+    private fun LayoutNode.childSizeCanAffectParentSize(): Boolean {
+        // if the view was measured twice with different constraints last time it means the
+        // constraints we have could be not the final constraints and in fact our parent
+        // ViewGroup can remeasure us with different constraints if we call requestLayout().
+        return wasMeasuredWithMultipleConstraints ||
+            // when parent's [hasFixedInnerContentConstraints] is true the child size change
+            // can't affect parent size as the size is fixed. for example it happens when parent
+            // has Modifier.fillMaxSize() set on it.
+            parent?.hasFixedInnerContentConstraints == false
+    }
+
     override fun measureAndLayout(sendPointerUpdate: Boolean) {
         trace("AndroidOwner:measureAndLayout") {
             val resend = if (sendPointerUpdate) resendMotionEventOnLayout else null
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 7ce8bbb..c9003f7 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
@@ -49,7 +49,6 @@
 import androidx.compose.ui.fastJoinToString
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.graphics.toAndroidRect
 import androidx.compose.ui.graphics.toComposeRect
 import androidx.compose.ui.layout.boundsInParent
 import androidx.compose.ui.layout.boundsInWindow
@@ -284,8 +283,11 @@
         }
     private var paneDisplayed = ArraySet<Int>()
     private var idToBeforeMap = HashMap<Int, Int>()
+    private var idToAfterMap = HashMap<Int, Int>()
     private val EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL =
         "android.view.accessibility.extra.EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL"
+    private val EXTRA_DATA_TEST_TRAVERSALAFTER_VAL =
+        "android.view.accessibility.extra.EXTRA_DATA_TEST_TRAVERSALAFTER_VAL"
 
     /**
      * A snapshot of the semantics node. The children here is fixed and are taken from the time
@@ -444,6 +446,7 @@
 
     private fun setTraversalValues() {
         idToBeforeMap.clear()
+        idToAfterMap.clear()
         var idToCoordinatesList = mutableListOf<Pair<Int, Rect>>()
 
         fun depthFirstSearch(currNode: SemanticsNode) {
@@ -464,10 +467,11 @@
             }
         }
 
-        currentSemanticsNodes[AccessibilityNodeProviderCompat.HOST_VIEW_ID]?.semanticsNode
-            ?.replacedChildrenSortedByBounds?.fastForEach { node ->
-                depthFirstSearch(node)
-            }
+        currentSemanticsNodes[AccessibilityNodeProviderCompat.HOST_VIEW_ID]?.semanticsNode?.let {
+            depthFirstSearch(
+                it
+            )
+        }
 
         // Iterate through our ordered list, and creating a mapping of current node to next node ID
         // We'll later read through this and set traversal order with IdToBeforeMap
@@ -475,6 +479,7 @@
             val prevId = idToCoordinatesList[i - 1].first
             val currId = idToCoordinatesList[i].first
             idToBeforeMap[prevId] = currId
+            idToAfterMap[currId] = prevId
         }
 
         return
@@ -1035,6 +1040,12 @@
             addExtraDataToAccessibilityNodeInfoHelper(
                 virtualViewId, info.unwrap(), EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL, null)
         }
+
+        if (idToAfterMap[virtualViewId] != null) {
+            idToAfterMap[virtualViewId]?.let { info.setTraversalAfter(view, it) }
+            addExtraDataToAccessibilityNodeInfoHelper(
+                virtualViewId, info.unwrap(), EXTRA_DATA_TEST_TRAVERSALAFTER_VAL, null)
+        }
     }
 
     /** Set the error text for this node */
@@ -1532,12 +1543,16 @@
         val node = currentSemanticsNodes[virtualViewId]?.semanticsNode ?: return
         val text = getIterableTextForAccessibility(node)
 
-        // This extra is just for testing: needed a way to retrieve `traversalBefore` from
-        // non-sealed instance of ANI
+        // This extra is just for testing: needed a way to retrieve `traversalBefore` and
+        // `traversalAfter` from a non-sealed instance of an ANI
         if (extraDataKey == EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL) {
             idToBeforeMap[virtualViewId]?.let {
                 info.extras.putInt(extraDataKey, it)
             }
+        } else if (extraDataKey == EXTRA_DATA_TEST_TRAVERSALAFTER_VAL) {
+            idToAfterMap[virtualViewId]?.let {
+                info.extras.putInt(extraDataKey, it)
+            }
         } else if (node.unmergedConfig.contains(SemanticsActions.GetTextLayoutResult) &&
             arguments != null && extraDataKey == EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY
         ) {
@@ -1891,10 +1906,11 @@
 
     private fun updateSemanticsNodesCopyAndPanes() {
         // TODO(b/172606324): removed this compose specific fix when talkback has a proper solution.
+        val toRemove = ArraySet<Int>()
         for (id in paneDisplayed) {
             val currentNode = currentSemanticsNodes[id]?.semanticsNode
             if (currentNode == null || !currentNode.hasPaneTitle()) {
-                paneDisplayed.remove(id)
+                toRemove.add(id)
                 sendPaneChangeEvents(
                     id,
                     AccessibilityEventCompat.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED,
@@ -1904,6 +1920,7 @@
                 )
             }
         }
+        paneDisplayed.removeAll(toRemove)
         previousSemanticsNodes.clear()
         for (entry in currentSemanticsNodes.entries) {
             if (entry.value.semanticsNode.hasPaneTitle() && paneDisplayed.add(entry.key)) {
@@ -2261,7 +2278,7 @@
     }
 
     private fun sendScrollEventIfNeeded(scrollObservationScope: ScrollObservationScope) {
-        if (!scrollObservationScope.isValid) {
+        if (!scrollObservationScope.isValidOwnerScope) {
             return
         }
         view.snapshotObserver.observeReads(scrollObservationScope, sendScrollEventIfNeededLambda) {
@@ -2779,7 +2796,18 @@
     if (!root.layoutNode.isPlaced || !root.layoutNode.isAttached) {
         return nodes
     }
-    val unaccountedSpace = Region().also { it.set(root.boundsInRoot.toAndroidRect()) }
+    val unaccountedSpace = Region().also {
+        it.set(
+            root.boundsInRoot.run {
+                android.graphics.Rect(
+                    left.roundToInt(),
+                    top.roundToInt(),
+                    right.roundToInt(),
+                    bottom.roundToInt()
+                )
+            }
+        )
+    }
 
     fun findAllSemanticNodesRecursive(currentNode: SemanticsNode) {
         val notAttachedOrPlaced =
@@ -2789,7 +2817,12 @@
         ) {
             return
         }
-        val boundsInRoot = currentNode.touchBoundsInRoot.toAndroidRect()
+        val boundsInRoot = android.graphics.Rect(
+            currentNode.touchBoundsInRoot.left.roundToInt(),
+            currentNode.touchBoundsInRoot.top.roundToInt(),
+            currentNode.touchBoundsInRoot.right.roundToInt(),
+            currentNode.touchBoundsInRoot.bottom.roundToInt(),
+        )
         val region = Region().also { it.set(boundsInRoot) }
         val virtualViewId = if (currentNode.id == root.id) {
             AccessibilityNodeProviderCompat.HOST_VIEW_ID
@@ -2818,7 +2851,12 @@
                 }
                 nodes[virtualViewId] = SemanticsNodeWithAdjustedBounds(
                     currentNode,
-                    boundsForFakeNode.toAndroidRect()
+                    android.graphics.Rect(
+                        boundsForFakeNode.left.roundToInt(),
+                        boundsForFakeNode.top.roundToInt(),
+                        boundsForFakeNode.right.roundToInt(),
+                        boundsForFakeNode.bottom.roundToInt(),
+                    )
                 )
             } else if (virtualViewId == AccessibilityNodeProviderCompat.HOST_VIEW_ID) {
                 // Root view might have WRAP_CONTENT layout params in which case it will have zero
@@ -2861,7 +2899,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/MotionDurationScale.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/MotionDurationScale.kt
index 40cfb6e..2fe23c8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/MotionDurationScale.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/MotionDurationScale.kt
@@ -24,6 +24,12 @@
  * the motion will end in the next frame callback. Otherwise, the duration [scaleFactor] will be
  * used as a multiplier to scale the duration of the motion. The larger the scale, the longer the
  * motion will take to finish, and therefore the slower it will be perceived.
+ *
+ * ## Testing
+ *
+ * To control the motion duration scale in tests, create an implementation of this interface and
+ * pass it to the `effectContext` parameter either where you call `runComposeUiTest` or where you
+ * create your test rule.
  */
 @Stable
 interface MotionDurationScale : CoroutineContext.Element {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/UiComposable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/UiComposable.kt
index 25b550b..ed1427b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/UiComposable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/UiComposable.kt
@@ -19,7 +19,7 @@
 import androidx.compose.runtime.ComposableTargetMarker
 
 /**
- * An annotation that can be used to mark an composable function as being expected to be use in a
+ * An annotation that can be used to mark a composable function as being expected to be use in a
  * composable function that is also marked or inferred to be marked as a [UiComposable].
  *
  * Using this annotation explicitly is rarely necessary as the Compose compiler plugin will infer
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
index 493756f..5fa5bdc 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
@@ -24,6 +24,7 @@
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.node.Nodes
 import androidx.compose.ui.node.modifierElementOf
 import androidx.compose.ui.node.requireCoordinator
@@ -363,48 +364,47 @@
     ambientShadowColor: Color = DefaultShadowColor,
     spotShadowColor: Color = DefaultShadowColor,
     compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
-) = this then modifierElementOf(
-    key = GraphicsLayerParameters(
-        scaleX,
-        scaleY,
-        alpha,
-        translationX,
-        translationY,
-        shadowElevation,
-        rotationX,
-        rotationY,
-        rotationZ,
-        cameraDistance,
-        transformOrigin,
-        shape,
-        clip,
-        renderEffect,
-        ambientShadowColor,
-        spotShadowColor,
-        compositingStrategy
-    ),
-    create = {
-        SimpleGraphicsLayerModifier(
-            scaleX = scaleX,
-            scaleY = scaleY,
-            alpha = alpha,
-            translationX = translationX,
-            translationY = translationY,
-            shadowElevation = shadowElevation,
-            rotationX = rotationX,
-            rotationY = rotationY,
-            rotationZ = rotationZ,
-            cameraDistance = cameraDistance,
-            transformOrigin = transformOrigin,
-            shape = shape,
-            clip = clip,
-            renderEffect = renderEffect,
-            ambientShadowColor = ambientShadowColor,
-            spotShadowColor = spotShadowColor,
-            compositingStrategy = compositingStrategy
-        )
-    },
-    definitions = debugInspectorInfo {
+) = this then GraphicsLayerModifierNodeElement(
+    scaleX,
+    scaleY,
+    alpha,
+    translationX,
+    translationY,
+    shadowElevation,
+    rotationX,
+    rotationY,
+    rotationZ,
+    cameraDistance,
+    transformOrigin,
+    shape,
+    clip,
+    renderEffect,
+    ambientShadowColor,
+    spotShadowColor,
+    compositingStrategy
+)
+
+@ExperimentalComposeUiApi
+private class GraphicsLayerModifierNodeElement(
+    val scaleX: Float,
+    val scaleY: Float,
+    val alpha: Float,
+    val translationX: Float,
+    val translationY: Float,
+    val shadowElevation: Float,
+    val rotationX: Float,
+    val rotationY: Float,
+    val rotationZ: Float,
+    val cameraDistance: Float,
+    val transformOrigin: TransformOrigin,
+    val shape: Shape,
+    val clip: Boolean,
+    val renderEffect: RenderEffect?,
+    val ambientShadowColor: Color,
+    val spotShadowColor: Color,
+    val compositingStrategy: CompositingStrategy
+) : ModifierNodeElement<SimpleGraphicsLayerModifier>(
+    inspectorInfo = debugInspectorInfo {
         name = "graphicsLayer"
         properties["scaleX"] = scaleX
         properties["scaleY"] = scaleY
@@ -423,28 +423,97 @@
         properties["ambientShadowColor"] = ambientShadowColor
         properties["spotShadowColor"] = spotShadowColor
         properties["compositingStrategy"] = compositingStrategy
-    },
-    update = {
-        it.scaleX = scaleX
-        it.scaleY = scaleY
-        it.alpha = alpha
-        it.translationX = translationX
-        it.translationY = translationY
-        it.shadowElevation = shadowElevation
-        it.rotationX = rotationX
-        it.rotationY = rotationY
-        it.rotationZ = rotationZ
-        it.cameraDistance = cameraDistance
-        it.transformOrigin = transformOrigin
-        it.shape = shape
-        it.clip = clip
-        it.renderEffect = renderEffect
-        it.ambientShadowColor = ambientShadowColor
-        it.spotShadowColor = spotShadowColor
-        it.compositingStrategy = compositingStrategy
-        it.invalidateLayerBlock()
     }
-)
+) {
+    override fun create(): SimpleGraphicsLayerModifier {
+        return SimpleGraphicsLayerModifier(
+            scaleX = scaleX,
+            scaleY = scaleY,
+            alpha = alpha,
+            translationX = translationX,
+            translationY = translationY,
+            shadowElevation = shadowElevation,
+            rotationX = rotationX,
+            rotationY = rotationY,
+            rotationZ = rotationZ,
+            cameraDistance = cameraDistance,
+            transformOrigin = transformOrigin,
+            shape = shape,
+            clip = clip,
+            renderEffect = renderEffect,
+            ambientShadowColor = ambientShadowColor,
+            spotShadowColor = spotShadowColor,
+            compositingStrategy = compositingStrategy
+        )
+    }
+
+    override fun update(node: SimpleGraphicsLayerModifier): SimpleGraphicsLayerModifier {
+        node.scaleX = scaleX
+        node.scaleY = scaleY
+        node.alpha = alpha
+        node.translationX = translationX
+        node.translationY = translationY
+        node.shadowElevation = shadowElevation
+        node.rotationX = rotationX
+        node.rotationY = rotationY
+        node.rotationZ = rotationZ
+        node.cameraDistance = cameraDistance
+        node.transformOrigin = transformOrigin
+        node.shape = shape
+        node.clip = clip
+        node.renderEffect = renderEffect
+        node.ambientShadowColor = ambientShadowColor
+        node.spotShadowColor = spotShadowColor
+        node.compositingStrategy = compositingStrategy
+        node.invalidateLayerBlock()
+
+        return node
+    }
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is ModifierNodeElement<*>) return false
+        if (other !is GraphicsLayerModifierNodeElement) return false
+
+        return this.scaleX == other.scaleX &&
+            this.scaleY == other.scaleY &&
+            this.alpha == other.alpha &&
+            this.translationX == other.translationX &&
+            this.translationY == other.translationY &&
+            this.shadowElevation == other.shadowElevation &&
+            this.rotationX == other.rotationX &&
+            this.rotationY == other.rotationY &&
+            this.rotationZ == other.rotationZ &&
+            this.cameraDistance == other.cameraDistance &&
+            this.transformOrigin == other.transformOrigin &&
+            this.shape == other.shape &&
+            this.clip == other.clip &&
+            this.renderEffect == other.renderEffect &&
+            this.ambientShadowColor == other.ambientShadowColor &&
+            this.spotShadowColor == other.spotShadowColor &&
+            this.compositingStrategy == other.compositingStrategy
+    }
+
+    override fun hashCode(): Int {
+        var result = scaleX.hashCode()
+        result = 31 * result + scaleY.hashCode()
+        result = 31 * result + alpha.hashCode()
+        result = 31 * result + translationX.hashCode()
+        result = 31 * result + translationY.hashCode()
+        result = 31 * result + shadowElevation.hashCode()
+        result = 31 * result + rotationX.hashCode()
+        result = 31 * result + rotationY.hashCode()
+        result = 31 * result + rotationZ.hashCode()
+        result = 31 * result + cameraDistance.hashCode()
+        result = 31 * result + transformOrigin.hashCode()
+        result = 31 * result + shape.hashCode()
+        result = 31 * result + clip.hashCode()
+        result = 31 * result + renderEffect.hashCode()
+        result = 31 * result + ambientShadowColor.hashCode()
+        result = 31 * result + spotShadowColor.hashCode()
+        result = 31 * result + compositingStrategy.hashCode()
+        return result
+    }
+}
 
 /**
  * A [Modifier.Node] that makes content draw into a draw layer. The draw layer can be
@@ -549,26 +618,6 @@
             "block=$layerBlock)"
 }
 
-private data class GraphicsLayerParameters(
-    val scaleX: Float,
-    val scaleY: Float,
-    val alpha: Float,
-    val translationX: Float,
-    val translationY: Float,
-    val shadowElevation: Float,
-    val rotationX: Float,
-    val rotationY: Float,
-    val rotationZ: Float,
-    val cameraDistance: Float,
-    val transformOrigin: TransformOrigin,
-    val shape: Shape,
-    val clip: Boolean,
-    val renderEffect: RenderEffect?,
-    val ambientShadowColor: Color,
-    val spotShadowColor: Color,
-    val compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
-)
-
 @OptIn(ExperimentalComposeUiApi::class)
 private class SimpleGraphicsLayerModifier(
     var scaleX: Float,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt
index 482ac9b..d24885c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt
@@ -88,22 +88,47 @@
  * A velocity tracker calculating velocity in 1 dimension.
  *
  * Add displacement data points using [addDataPoint], and obtain velocity using [calculateVelocity].
+ *
+ * Note: for calculating touch-related or other 2 dimensional/planar velocities, please use
+ * [VelocityTracker], which handles velocity tracking across both X and Y dimensions at once.
  */
-internal class VelocityTracker1D(
+class VelocityTracker1D internal constructor(
     // whether the data points added to the tracker represent differential values
     // (i.e. change in the  tracked object's displacement since the previous data point).
     // If false, it means that the data points added to the tracker will be considered as absolute
     // values (e.g. positional values).
-    private val differentialDataPoints: Boolean = false,
+    val isDataDifferential: Boolean = false,
     // The velocity tracking strategy that this instance uses for all velocity calculations.
     private val strategy: Strategy = Strategy.Lsq2,
 ) {
 
     init {
-        if (differentialDataPoints && strategy.equals(Strategy.Lsq2)) {
+        if (isDataDifferential && strategy.equals(Strategy.Lsq2)) {
             throw IllegalStateException("Lsq2 not (yet) supported for differential axes")
         }
     }
+
+    /**
+     * Constructor to create a new velocity tracker. It allows to specify whether or not the tracker
+     * should consider the data ponits provided via [addDataPoint] as differential or
+     * non-differential.
+     *
+     * Differential data ponits represent change in displacement. For instance, differential data
+     * points of [2, -1, 5] represent: the object moved by "2" units, then by "-1" units, then by
+     * "5" units. An example use case for differential data points is when tracking velocity for an
+     * object whose displacements (or change in positions) over time are known.
+     *
+     * Non-differential data ponits represent position of the object whose velocity is tracked. For
+     * instance, non-differential data points of [2, -1, 5] represent: the object was at position
+     * "2", then at position "-1", then at position "5". An example use case for non-differential
+     * data points is when tracking velocity for an object whose positions on a geometrical axis
+     * over different instances of time are known.
+     *
+     * @param isDataDifferential [true] if the data ponits provided to the constructed tracker
+     * are differential. [false] otherwise.
+     */
+    constructor(isDataDifferential: Boolean) : this(isDataDifferential, Strategy.Impulse)
+
     private val minSampleSize: Int = when (strategy) {
         Strategy.Impulse -> 2
         Strategy.Lsq2 -> 3
@@ -114,7 +139,7 @@
      * result in notably different velocities than the others, so make careful choice or change of
      * strategy whenever you want to make one.
      */
-    enum class Strategy {
+    internal enum class Strategy {
         /**
          * Least squares strategy. Polynomial fit at degree 2.
          * Note that the implementation of this strategy currently supports only non-differential
@@ -133,8 +158,11 @@
     private var index: Int = 0
 
     /**
-     * Adds a data point for velocity calculation. A data point should represent a position along
-     * the tracked axis at a given time, [timeMillis].
+     * Adds a data point for velocity calculation at a given time, [timeMillis]. The data ponit
+     * represents an amount of a change in position (for differential data points), or an absolute
+     * position (for non-differential data points). Whether or not the tracker handles differential
+     * data points is decided by [isDataDifferential], which is set once and finally during
+     * the construction of the tracker.
      *
      * Use the same units for the data points provided. For example, having some data points in `cm`
      * and some in `m` will result in incorrect velocity calculations, as this method (and the
@@ -188,7 +216,7 @@
             // Multiply by "1000" to convert from units/ms to units/s
             return when (strategy) {
                 Strategy.Impulse ->
-                    calculateImpulseVelocity(dataPoints, time, differentialDataPoints) * 1000
+                    calculateImpulseVelocity(dataPoints, time, isDataDifferential) * 1000
                 Strategy.Lsq2 -> calculateLeastSquaresVelocity(dataPoints, time) * 1000
             }
         }
@@ -199,7 +227,7 @@
     }
 
     /**
-     * Clears the tracked positions added by [addDataPoint].
+     * Clears data points added by [addDataPoint].
      */
     fun resetTracking() {
         samples.fill(element = null)
@@ -487,7 +515,7 @@
 private fun calculateImpulseVelocity(
     dataPoints: List<Float>,
     time: List<Float>,
-    differentialDataPoints: Boolean
+    isDataDifferential: Boolean
 ): Float {
     val numDataPoints = dataPoints.size
     if (numDataPoints < 2) {
@@ -501,7 +529,7 @@
             // For differential data ponits, each measurement reflects the amount of change in the
             // subject's position. However, the first sample is discarded in computation because we
             // don't know the time duration over which this change has occurred.
-            if (differentialDataPoints) dataPoints[0]
+            if (isDataDifferential) dataPoints[0]
             else dataPoints[0] - dataPoints[1]
         return dataPointsDelta / (time[0] - time[1])
     }
@@ -512,7 +540,7 @@
         }
         val vPrev = kineticEnergyToVelocity(work)
         val dataPointsDelta =
-            if (differentialDataPoints) -dataPoints[i - 1]
+            if (isDataDifferential) -dataPoints[i - 1]
             else dataPoints[i] - dataPoints[i - 1]
         val vCurr = dataPointsDelta / (time[i] - time[i - 1])
         work += (vCurr - vPrev) * abs(vCurr)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutModifier.kt
index b66fc49..abb028f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutModifier.kt
@@ -16,15 +16,15 @@
 
 package androidx.compose.ui.layout
 
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.GraphicsLayerScope
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.InspectorValueInfo
-import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.internal.JvmDefaultWithCompatibility
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.modifierElementOf
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.internal.JvmDefaultWithCompatibility
 
 /**
  * A [Modifier.Element] that changes how its wrapped content is measured and laid out.
@@ -263,37 +263,28 @@
  *
  * @see androidx.compose.ui.layout.LayoutModifier
  */
+@OptIn(ExperimentalComposeUiApi::class)
 fun Modifier.layout(
     measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
-) = this.then(
-    LayoutModifierImpl(
-        measureBlock = measure,
-        inspectorInfo = debugInspectorInfo {
-            name = "layout"
-            properties["measure"] = measure
-        }
-    )
+) = this then modifierElementOf(
+    key = measure,
+    create = { LayoutModifierImpl(measure) },
+    update = { layoutModifier -> layoutModifier.measureBlock = measure },
+    definitions = {
+        name = "layout"
+        properties["measure"] = measure
+    }
 )
 
-private class LayoutModifierImpl(
-    val measureBlock: MeasureScope.(Measurable, Constraints) -> MeasureResult,
-    inspectorInfo: InspectorInfo.() -> Unit,
-) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
+@OptIn(ExperimentalComposeUiApi::class)
+internal class LayoutModifierImpl(
+    var measureBlock: MeasureScope.(Measurable, Constraints) -> MeasureResult
+) : LayoutModifierNode, Modifier.Node() {
     override fun MeasureScope.measure(
         measurable: Measurable,
         constraints: Constraints
     ) = measureBlock(measurable, constraints)
 
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        val otherModifier = other as? LayoutModifierImpl ?: return false
-        return measureBlock == otherModifier.measureBlock
-    }
-
-    override fun hashCode(): Int {
-        return measureBlock.hashCode()
-    }
-
     override fun toString(): String {
         return "LayoutModifierImpl(measureBlock=$measureBlock)"
     }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnPlacedModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnPlacedModifier.kt
index 619deb9..860638c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnPlacedModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnPlacedModifier.kt
@@ -17,11 +17,11 @@
 package androidx.compose.ui.layout
 
 import androidx.compose.runtime.Stable
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.InspectorValueInfo
-import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.internal.JvmDefaultWithCompatibility
+import androidx.compose.ui.node.LayoutAwareModifierNode
+import androidx.compose.ui.node.modifierElementOf
 
 /**
  * Invoke [onPlaced] after the parent [LayoutModifier] and parent layout has been placed and before
@@ -30,38 +30,32 @@
  *
  * @sample androidx.compose.ui.samples.OnPlaced
  */
+@OptIn(ExperimentalComposeUiApi::class)
 @Stable
 fun Modifier.onPlaced(
     onPlaced: (LayoutCoordinates) -> Unit
-) = this.then(
-    OnPlacedModifierImpl(
-        callback = onPlaced,
-        inspectorInfo = debugInspectorInfo {
-            name = "onPlaced"
-            properties["onPlaced"] = onPlaced
-        }
-    )
+) = this then modifierElementOf(
+    key = onPlaced,
+    create = {
+        OnPlacedModifierImpl(callback = onPlaced)
+    },
+    update = {
+        it.callback = onPlaced
+    },
+    definitions = {
+        name = "onPlaced"
+        properties["onPlaced"] = onPlaced
+    }
 )
 
+@OptIn(ExperimentalComposeUiApi::class)
 private class OnPlacedModifierImpl(
-    val callback: (LayoutCoordinates) -> Unit,
-    inspectorInfo: InspectorInfo.() -> Unit
-) : OnPlacedModifier, InspectorValueInfo(inspectorInfo) {
+    var callback: (LayoutCoordinates) -> Unit
+) : LayoutAwareModifierNode, Modifier.Node() {
 
     override fun onPlaced(coordinates: LayoutCoordinates) {
         callback(coordinates)
     }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is OnPlacedModifierImpl) return false
-
-        return callback == other.callback
-    }
-
-    override fun hashCode(): Int {
-        return callback.hashCode()
-    }
 }
 
 /**
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 0674ea8..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 {
@@ -474,6 +474,16 @@
             "measurePolicy: $measurePolicy"
     }
 
+    internal val hasFixedInnerContentConstraints: Boolean
+        get() {
+            // it is the constraints we have after all the modifiers applied on this node,
+            // the one to be passed into user provided [measurePolicy.measure]. if those
+            // constraints are fixed this means the children size changes can't affect
+            // this LayoutNode size.
+            val innerContentConstraints = innerCoordinator.lastMeasurementConstraints
+            return innerContentConstraints.hasFixedWidth && innerContentConstraints.hasFixedHeight
+        }
+
     /**
      * Call this method from the debugger to see a dump of the LayoutNode tree structure
      */
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
index f3603db..e1d54e7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
@@ -510,14 +510,18 @@
                 val updated = next.updateUnsafe(node)
                 if (updated !== node) {
                     // if a new instance is returned, we want to detach the old one
-                    autoInvalidateRemovedNode(node)
-                    node.detach()
+                    if (node.isAttached) {
+                        autoInvalidateRemovedNode(node)
+                        node.detach()
+                    }
                     val result = replaceNode(node, updated)
-                    autoInvalidateInsertedNode(updated)
+                    if (node.isAttached) {
+                        autoInvalidateInsertedNode(updated)
+                    }
                     return result
                 } else {
                     // the node was updated. we are done.
-                    if (next.autoInvalidate) {
+                    if (next.autoInvalidate && updated.isAttached) {
                         // the modifier element is labeled as "auto invalidate", which means
                         // that since the node was updated, we need to invalidate everything
                         // relevant to it.
@@ -529,7 +533,9 @@
             node is BackwardsCompatNode -> {
                 node.element = next
                 // We always autoInvalidate BackwardsCompatNode.
-                autoInvalidateUpdatedNode(node)
+                if (node.isAttached) {
+                    autoInvalidateUpdatedNode(node)
+                }
                 return node
             }
             else -> error("Unknown Modifier.Node type")
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 037ef2c..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
@@ -295,6 +295,8 @@
      */
     private var layerPositionalProperties: LayerPositionalProperties? = null
 
+    internal val lastMeasurementConstraints: Constraints get() = measurementConstraints
+
     protected inline fun performingMeasure(
         constraints: Constraints,
         block: () -> Placeable
@@ -505,7 +507,7 @@
     var layer: OwnedLayer? = null
         private set
 
-    override val isValid: Boolean
+    override val isValidOwnerScope: Boolean
         get() = layer != null && isAttached
 
     val minimumTouchTargetSize: Size
@@ -1206,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/NodeKind.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
index 4698e4f..595c6b6 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
@@ -205,6 +205,7 @@
 
 @OptIn(ExperimentalComposeUiApi::class)
 private fun autoInvalidateNode(node: Modifier.Node, phase: Int) {
+    check(node.isAttached)
     if (node.isKind(Nodes.Layout) && node is LayoutModifierNode) {
         node.invalidateMeasurements()
         if (phase == Removed) {
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/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker1DTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker1DTest.kt
index 83771ee..8283e89 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker1DTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker1DTest.kt
@@ -35,7 +35,7 @@
     @Test
     fun lsq2_differentialValues_unsupported() {
         assertThrows(IllegalStateException::class.java) {
-            VelocityTracker1D(differentialDataPoints = true, Strategy.Lsq2)
+            VelocityTracker1D(isDataDifferential = true, Strategy.Lsq2)
         }
     }
     @Test
@@ -154,7 +154,7 @@
     @Test
     fun resetTracking_differentialValues_impulse() {
         // Fixed velocity at 5 points per 10 milliseconds
-        val tracker = VelocityTracker1D(differentialDataPoints = true, Strategy.Impulse)
+        val tracker = VelocityTracker1D(isDataDifferential = true, Strategy.Impulse)
         tracker.addDataPoint(0, 0f)
         tracker.addDataPoint(10, 5f)
         tracker.addDataPoint(20, 10f)
@@ -169,7 +169,7 @@
     @Test
     fun resetTracking_nonDifferentialValues_impulse() {
         // Fixed velocity at 5 points per 10 milliseconds
-        val tracker = VelocityTracker1D(differentialDataPoints = false, Strategy.Impulse)
+        val tracker = VelocityTracker1D(isDataDifferential = false, Strategy.Impulse)
         tracker.addDataPoint(0, 0f)
         tracker.addDataPoint(10, 5f)
         tracker.addDataPoint(20, 10f)
@@ -184,7 +184,7 @@
     @Test
     fun resetTracking_nonDifferentialValues_lsq2() {
         // Fixed velocity at 5 points per 10 milliseconds
-        val tracker = VelocityTracker1D(differentialDataPoints = false, Strategy.Lsq2)
+        val tracker = VelocityTracker1D(isDataDifferential = false, Strategy.Lsq2)
         tracker.addDataPoint(0, 0f)
         tracker.addDataPoint(10, 5f)
         tracker.addDataPoint(20, 10f)
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index 2509402..1e0d0a9 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -45,6 +45,7 @@
 import androidx.compose.ui.input.pointer.PointerInputModifier
 import androidx.compose.ui.layout.AlignmentLine
 import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.LayoutModifierImpl
 import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
@@ -2317,45 +2318,49 @@
 
     @Test
     fun modifierMatchesWrapperWithIdentity() {
-        val modifier1 = Modifier.layout { measurable, constraints ->
-            val placeable = measurable.measure(constraints)
-            layout(placeable.width, placeable.height) {
-                placeable.place(0, 0)
+        val measureLambda1: MeasureScope.(Measurable, Constraints) -> MeasureResult =
+            { measurable, constraints ->
+                val placeable = measurable.measure(constraints)
+                layout(placeable.width, placeable.height) {
+                    placeable.place(0, 0)
+                }
             }
-        }
-        val modifier2 = Modifier.layout { measurable, constraints ->
-            val placeable = measurable.measure(constraints)
-            layout(placeable.width, placeable.height) {
-                placeable.place(1, 1)
+        val modifier1 = Modifier.layout(measureLambda1)
+
+        val measureLambda2: MeasureScope.(Measurable, Constraints) -> MeasureResult =
+            { measurable, constraints ->
+                val placeable = measurable.measure(constraints)
+                layout(placeable.width, placeable.height) {
+                    placeable.place(1, 1)
+                }
             }
-        }
+        val modifier2 = Modifier.layout(measureLambda2)
 
         val root = LayoutNode()
         root.modifier = modifier1.then(modifier2)
 
-        val wrapper1 = root.outerCoordinator
-        val wrapper2 = root.outerCoordinator.wrapped
-
         assertEquals(
-            modifier1,
-            (wrapper1 as LayoutModifierNodeCoordinator).layoutModifierNode.toModifier()
+            measureLambda1,
+            ((root.outerCoordinator as LayoutModifierNodeCoordinator)
+                .layoutModifierNode as LayoutModifierImpl).measureBlock
         )
         assertEquals(
-            modifier2,
-            (wrapper2 as LayoutModifierNodeCoordinator).layoutModifierNode.toModifier()
+            measureLambda2,
+            ((root.outerCoordinator.wrapped as LayoutModifierNodeCoordinator)
+                .layoutModifierNode as LayoutModifierImpl).measureBlock
         )
 
         root.modifier = modifier2.then(modifier1)
 
         assertEquals(
-            modifier1,
-            (root.outerCoordinator.wrapped as LayoutModifierNodeCoordinator)
-                .layoutModifierNode
-                .toModifier()
+            measureLambda1,
+            ((root.outerCoordinator.wrapped as LayoutModifierNodeCoordinator)
+                .layoutModifierNode as LayoutModifierImpl).measureBlock
         )
         assertEquals(
-            modifier2,
-            (root.outerCoordinator as LayoutModifierNodeCoordinator).layoutModifierNode.toModifier()
+            measureLambda2,
+            ((root.outerCoordinator as LayoutModifierNodeCoordinator)
+                .layoutModifierNode as LayoutModifierImpl).measureBlock
         )
     }
 
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..9b065fc 100644
--- a/constraintlayout/constraintlayout-compose/api/current.txt
+++ b/constraintlayout/constraintlayout-compose/api/current.txt
@@ -138,10 +138,15 @@
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createAbsoluteLeftBarrier(androidx.constraintlayout.compose.LayoutReference![] elements, optional float margin);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createAbsoluteRightBarrier(androidx.constraintlayout.compose.LayoutReference![] elements, optional float margin);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createBottomBarrier(androidx.constraintlayout.compose.LayoutReference![] elements, optional float margin);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createColumn(androidx.constraintlayout.compose.LayoutReference![] elements, optional int[] rowWeights, optional float verticalGap, optional float padding);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createColumn(androidx.constraintlayout.compose.LayoutReference![] elements, optional float verticalGap, optional int[] rowWeights, optional float paddingHorizontal, optional float paddingVertical);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createEndBarrier(androidx.constraintlayout.compose.LayoutReference![] elements, optional float margin);
     method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createFlow(androidx.constraintlayout.compose.LayoutReference![]? elements, optional boolean flowVertically, optional float verticalGap, optional float horizontalGap, optional int maxElement, optional float padding, optional androidx.constraintlayout.compose.Wrap wrapMode, optional androidx.constraintlayout.compose.VerticalAlign verticalAlign, optional androidx.constraintlayout.compose.HorizontalAlign horizontalAlign, optional float horizontalFlowBias, optional float verticalFlowBias, optional androidx.constraintlayout.compose.FlowStyle verticalStyle, optional androidx.constraintlayout.compose.FlowStyle horizontalStyle);
     method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createFlow(androidx.constraintlayout.compose.LayoutReference![]? elements, optional boolean flowVertically, optional float verticalGap, optional float horizontalGap, optional int maxElement, optional float paddingHorizontal, optional float paddingVertical, optional androidx.constraintlayout.compose.Wrap wrapMode, optional androidx.constraintlayout.compose.VerticalAlign verticalAlign, optional androidx.constraintlayout.compose.HorizontalAlign horizontalAlign, optional float horizontalFlowBias, optional float verticalFlowBias, optional androidx.constraintlayout.compose.FlowStyle verticalStyle, optional androidx.constraintlayout.compose.FlowStyle horizontalStyle);
     method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createFlow(androidx.constraintlayout.compose.LayoutReference![]? elements, optional boolean flowVertically, optional float verticalGap, optional float horizontalGap, optional int maxElement, optional float paddingLeft, optional float paddingTop, optional float paddingRight, optional float paddingBottom, optional androidx.constraintlayout.compose.Wrap wrapMode, optional androidx.constraintlayout.compose.VerticalAlign verticalAlign, optional androidx.constraintlayout.compose.HorizontalAlign horizontalAlign, optional float horizontalFlowBias, optional float verticalFlowBias, optional androidx.constraintlayout.compose.FlowStyle verticalStyle, optional androidx.constraintlayout.compose.FlowStyle horizontalStyle);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createGrid(androidx.constraintlayout.compose.LayoutReference![] elements, optional int orientation, optional int rows, optional int columns, optional float verticalGap, optional float horizontalGap, optional int[] rowWeights, optional int[] columnWeights, optional String skips, optional String spans, optional float padding);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createGrid(androidx.constraintlayout.compose.LayoutReference![] elements, optional int orientation, optional int rows, optional int columns, optional float verticalGap, optional float horizontalGap, optional int[] rowWeights, optional int[] columnWeights, optional String skips, optional String spans, optional float paddingHorizontal, optional float paddingVertical);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createGrid(androidx.constraintlayout.compose.LayoutReference![] elements, optional int orientation, optional int rows, optional int columns, optional float verticalGap, optional float horizontalGap, optional int[] rowWeights, optional int[] columnWeights, optional String skips, optional String spans, optional float paddingLeft, optional float paddingTop, optional float paddingRight, optional float paddingBottom);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromAbsoluteLeft(float offset);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromAbsoluteLeft(float fraction);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromAbsoluteRight(float offset);
@@ -155,6 +160,8 @@
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createGuidelineFromTop(float offset);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createGuidelineFromTop(float fraction);
     method public final androidx.constraintlayout.compose.HorizontalChainReference createHorizontalChain(androidx.constraintlayout.compose.LayoutReference![] elements, optional androidx.constraintlayout.compose.ChainStyle chainStyle);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createRow(androidx.constraintlayout.compose.LayoutReference![] elements, optional float horizontalGap, optional int[] columnWeights, optional float padding);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createRow(androidx.constraintlayout.compose.LayoutReference![] elements, optional float horizontalGap, optional int[] columnWeights, optional float paddingHorizontal, optional float paddingVertical);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createStartBarrier(androidx.constraintlayout.compose.LayoutReference![] elements, optional float margin);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createTopBarrier(androidx.constraintlayout.compose.LayoutReference![] elements, optional float margin);
     method public final androidx.constraintlayout.compose.VerticalChainReference createVerticalChain(androidx.constraintlayout.compose.LayoutReference![] elements, optional androidx.constraintlayout.compose.ChainStyle chainStyle);
@@ -411,9 +418,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..9a20684 100644
--- a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
@@ -173,10 +173,15 @@
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createAbsoluteLeftBarrier(androidx.constraintlayout.compose.LayoutReference![] elements, optional float margin);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createAbsoluteRightBarrier(androidx.constraintlayout.compose.LayoutReference![] elements, optional float margin);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createBottomBarrier(androidx.constraintlayout.compose.LayoutReference![] elements, optional float margin);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createColumn(androidx.constraintlayout.compose.LayoutReference![] elements, optional int[] rowWeights, optional float verticalGap, optional float padding);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createColumn(androidx.constraintlayout.compose.LayoutReference![] elements, optional float verticalGap, optional int[] rowWeights, optional float paddingHorizontal, optional float paddingVertical);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createEndBarrier(androidx.constraintlayout.compose.LayoutReference![] elements, optional float margin);
     method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createFlow(androidx.constraintlayout.compose.LayoutReference![]? elements, optional boolean flowVertically, optional float verticalGap, optional float horizontalGap, optional int maxElement, optional float padding, optional androidx.constraintlayout.compose.Wrap wrapMode, optional androidx.constraintlayout.compose.VerticalAlign verticalAlign, optional androidx.constraintlayout.compose.HorizontalAlign horizontalAlign, optional float horizontalFlowBias, optional float verticalFlowBias, optional androidx.constraintlayout.compose.FlowStyle verticalStyle, optional androidx.constraintlayout.compose.FlowStyle horizontalStyle);
     method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createFlow(androidx.constraintlayout.compose.LayoutReference![]? elements, optional boolean flowVertically, optional float verticalGap, optional float horizontalGap, optional int maxElement, optional float paddingHorizontal, optional float paddingVertical, optional androidx.constraintlayout.compose.Wrap wrapMode, optional androidx.constraintlayout.compose.VerticalAlign verticalAlign, optional androidx.constraintlayout.compose.HorizontalAlign horizontalAlign, optional float horizontalFlowBias, optional float verticalFlowBias, optional androidx.constraintlayout.compose.FlowStyle verticalStyle, optional androidx.constraintlayout.compose.FlowStyle horizontalStyle);
     method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createFlow(androidx.constraintlayout.compose.LayoutReference![]? elements, optional boolean flowVertically, optional float verticalGap, optional float horizontalGap, optional int maxElement, optional float paddingLeft, optional float paddingTop, optional float paddingRight, optional float paddingBottom, optional androidx.constraintlayout.compose.Wrap wrapMode, optional androidx.constraintlayout.compose.VerticalAlign verticalAlign, optional androidx.constraintlayout.compose.HorizontalAlign horizontalAlign, optional float horizontalFlowBias, optional float verticalFlowBias, optional androidx.constraintlayout.compose.FlowStyle verticalStyle, optional androidx.constraintlayout.compose.FlowStyle horizontalStyle);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createGrid(androidx.constraintlayout.compose.LayoutReference![] elements, optional int orientation, optional int rows, optional int columns, optional float verticalGap, optional float horizontalGap, optional int[] rowWeights, optional int[] columnWeights, optional String skips, optional String spans, optional float padding);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createGrid(androidx.constraintlayout.compose.LayoutReference![] elements, optional int orientation, optional int rows, optional int columns, optional float verticalGap, optional float horizontalGap, optional int[] rowWeights, optional int[] columnWeights, optional String skips, optional String spans, optional float paddingHorizontal, optional float paddingVertical);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createGrid(androidx.constraintlayout.compose.LayoutReference![] elements, optional int orientation, optional int rows, optional int columns, optional float verticalGap, optional float horizontalGap, optional int[] rowWeights, optional int[] columnWeights, optional String skips, optional String spans, optional float paddingLeft, optional float paddingTop, optional float paddingRight, optional float paddingBottom);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromAbsoluteLeft(float offset);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromAbsoluteLeft(float fraction);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromAbsoluteRight(float offset);
@@ -190,6 +195,8 @@
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createGuidelineFromTop(float offset);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createGuidelineFromTop(float fraction);
     method public final androidx.constraintlayout.compose.HorizontalChainReference createHorizontalChain(androidx.constraintlayout.compose.LayoutReference![] elements, optional androidx.constraintlayout.compose.ChainStyle chainStyle);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createRow(androidx.constraintlayout.compose.LayoutReference![] elements, optional float horizontalGap, optional int[] columnWeights, optional float padding);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createRow(androidx.constraintlayout.compose.LayoutReference![] elements, optional float horizontalGap, optional int[] columnWeights, optional float paddingHorizontal, optional float paddingVertical);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createStartBarrier(androidx.constraintlayout.compose.LayoutReference![] elements, optional float margin);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createTopBarrier(androidx.constraintlayout.compose.LayoutReference![] elements, optional float margin);
     method public final androidx.constraintlayout.compose.VerticalChainReference createVerticalChain(androidx.constraintlayout.compose.LayoutReference![] elements, optional androidx.constraintlayout.compose.ChainStyle chainStyle);
@@ -586,9 +593,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 +601,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 +735,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..a52b8ea 100644
--- a/constraintlayout/constraintlayout-compose/api/restricted_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
@@ -145,10 +145,15 @@
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createAbsoluteLeftBarrier(androidx.constraintlayout.compose.LayoutReference![] elements, optional float margin);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createAbsoluteRightBarrier(androidx.constraintlayout.compose.LayoutReference![] elements, optional float margin);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createBottomBarrier(androidx.constraintlayout.compose.LayoutReference![] elements, optional float margin);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createColumn(androidx.constraintlayout.compose.LayoutReference![] elements, optional int[] rowWeights, optional float verticalGap, optional float padding);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createColumn(androidx.constraintlayout.compose.LayoutReference![] elements, optional float verticalGap, optional int[] rowWeights, optional float paddingHorizontal, optional float paddingVertical);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createEndBarrier(androidx.constraintlayout.compose.LayoutReference![] elements, optional float margin);
     method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createFlow(androidx.constraintlayout.compose.LayoutReference![]? elements, optional boolean flowVertically, optional float verticalGap, optional float horizontalGap, optional int maxElement, optional float padding, optional androidx.constraintlayout.compose.Wrap wrapMode, optional androidx.constraintlayout.compose.VerticalAlign verticalAlign, optional androidx.constraintlayout.compose.HorizontalAlign horizontalAlign, optional float horizontalFlowBias, optional float verticalFlowBias, optional androidx.constraintlayout.compose.FlowStyle verticalStyle, optional androidx.constraintlayout.compose.FlowStyle horizontalStyle);
     method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createFlow(androidx.constraintlayout.compose.LayoutReference![]? elements, optional boolean flowVertically, optional float verticalGap, optional float horizontalGap, optional int maxElement, optional float paddingHorizontal, optional float paddingVertical, optional androidx.constraintlayout.compose.Wrap wrapMode, optional androidx.constraintlayout.compose.VerticalAlign verticalAlign, optional androidx.constraintlayout.compose.HorizontalAlign horizontalAlign, optional float horizontalFlowBias, optional float verticalFlowBias, optional androidx.constraintlayout.compose.FlowStyle verticalStyle, optional androidx.constraintlayout.compose.FlowStyle horizontalStyle);
     method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createFlow(androidx.constraintlayout.compose.LayoutReference![]? elements, optional boolean flowVertically, optional float verticalGap, optional float horizontalGap, optional int maxElement, optional float paddingLeft, optional float paddingTop, optional float paddingRight, optional float paddingBottom, optional androidx.constraintlayout.compose.Wrap wrapMode, optional androidx.constraintlayout.compose.VerticalAlign verticalAlign, optional androidx.constraintlayout.compose.HorizontalAlign horizontalAlign, optional float horizontalFlowBias, optional float verticalFlowBias, optional androidx.constraintlayout.compose.FlowStyle verticalStyle, optional androidx.constraintlayout.compose.FlowStyle horizontalStyle);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createGrid(androidx.constraintlayout.compose.LayoutReference![] elements, optional int orientation, optional int rows, optional int columns, optional float verticalGap, optional float horizontalGap, optional int[] rowWeights, optional int[] columnWeights, optional String skips, optional String spans, optional float padding);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createGrid(androidx.constraintlayout.compose.LayoutReference![] elements, optional int orientation, optional int rows, optional int columns, optional float verticalGap, optional float horizontalGap, optional int[] rowWeights, optional int[] columnWeights, optional String skips, optional String spans, optional float paddingHorizontal, optional float paddingVertical);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createGrid(androidx.constraintlayout.compose.LayoutReference![] elements, optional int orientation, optional int rows, optional int columns, optional float verticalGap, optional float horizontalGap, optional int[] rowWeights, optional int[] columnWeights, optional String skips, optional String spans, optional float paddingLeft, optional float paddingTop, optional float paddingRight, optional float paddingBottom);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromAbsoluteLeft(float offset);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromAbsoluteLeft(float fraction);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromAbsoluteRight(float offset);
@@ -162,6 +167,8 @@
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createGuidelineFromTop(float offset);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createGuidelineFromTop(float fraction);
     method public final androidx.constraintlayout.compose.HorizontalChainReference createHorizontalChain(androidx.constraintlayout.compose.LayoutReference![] elements, optional androidx.constraintlayout.compose.ChainStyle chainStyle);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createRow(androidx.constraintlayout.compose.LayoutReference![] elements, optional float horizontalGap, optional int[] columnWeights, optional float padding);
+    method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createRow(androidx.constraintlayout.compose.LayoutReference![] elements, optional float horizontalGap, optional int[] columnWeights, optional float paddingHorizontal, optional float paddingVertical);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createStartBarrier(androidx.constraintlayout.compose.LayoutReference![] elements, optional float margin);
     method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createTopBarrier(androidx.constraintlayout.compose.LayoutReference![] elements, optional float margin);
     method public final androidx.constraintlayout.compose.VerticalChainReference createVerticalChain(androidx.constraintlayout.compose.LayoutReference![] elements, optional androidx.constraintlayout.compose.ChainStyle chainStyle);
@@ -528,9 +535,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/integration-tests/constraintlayout-compose-demos/build.gradle b/constraintlayout/constraintlayout-compose/integration-tests/demos/build.gradle
similarity index 100%
rename from constraintlayout/constraintlayout-compose/integration-tests/constraintlayout-compose-demos/build.gradle
rename to constraintlayout/constraintlayout-compose/integration-tests/demos/build.gradle
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/constraintlayout-compose-demos/src/main/java/androidx/constraintlayout/compose/demos/AllDemos.kt b/constraintlayout/constraintlayout-compose/integration-tests/demos/src/main/java/androidx/constraintlayout/compose/demos/AllDemos.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/integration-tests/constraintlayout-compose-demos/src/main/java/androidx/constraintlayout/compose/demos/AllDemos.kt
rename to constraintlayout/constraintlayout-compose/integration-tests/demos/src/main/java/androidx/constraintlayout/compose/demos/AllDemos.kt
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/constraintlayout-compose-demos/src/main/java/androidx/constraintlayout/compose/demos/ChainsDemo.kt b/constraintlayout/constraintlayout-compose/integration-tests/demos/src/main/java/androidx/constraintlayout/compose/demos/ChainsDemo.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/integration-tests/constraintlayout-compose-demos/src/main/java/androidx/constraintlayout/compose/demos/ChainsDemo.kt
rename to constraintlayout/constraintlayout-compose/integration-tests/demos/src/main/java/androidx/constraintlayout/compose/demos/ChainsDemo.kt
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/constraintlayout-compose-demos/src/main/java/androidx/constraintlayout/compose/demos/CustomKeyAttributesDemo.kt b/constraintlayout/constraintlayout-compose/integration-tests/demos/src/main/java/androidx/constraintlayout/compose/demos/CustomKeyAttributesDemo.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/integration-tests/constraintlayout-compose-demos/src/main/java/androidx/constraintlayout/compose/demos/CustomKeyAttributesDemo.kt
rename to constraintlayout/constraintlayout-compose/integration-tests/demos/src/main/java/androidx/constraintlayout/compose/demos/CustomKeyAttributesDemo.kt
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/constraintlayout-compose-demos/src/main/java/androidx/constraintlayout/compose/demos/OnSwipeDemos.kt b/constraintlayout/constraintlayout-compose/integration-tests/demos/src/main/java/androidx/constraintlayout/compose/demos/OnSwipeDemos.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/integration-tests/constraintlayout-compose-demos/src/main/java/androidx/constraintlayout/compose/demos/OnSwipeDemos.kt
rename to constraintlayout/constraintlayout-compose/integration-tests/demos/src/main/java/androidx/constraintlayout/compose/demos/OnSwipeDemos.kt
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/GridDslTest.kt b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/GridDslTest.kt
new file mode 100644
index 0000000..5146e0c
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/GridDslTest.kt
@@ -0,0 +1,503 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.constraintlayout.compose
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+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 org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests for the Grid Helper
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class GridDslTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Before
+    fun setup() {
+        isDebugInspectorInfoEnabled = true
+    }
+
+    @After
+    fun tearDown() {
+        isDebugInspectorInfoEnabled = false
+    }
+
+    @Test
+    fun testTwoByTwo() {
+        val rootSize = 200.dp
+        val boxesCount = 4
+        val rows = 2
+        val columns = 2
+        rule.setContent {
+            gridComposableTest(
+                modifier = Modifier.size(rootSize),
+                boxesCount = boxesCount,
+                gridOrientation = 0,
+                numRows = rows,
+                numColumns = columns,
+                hGap = 0,
+                vGap = 0,
+                gridSpans = "",
+                gridSkips = "",
+                gridRowWeights = intArrayOf(),
+                gridColumnWeights = intArrayOf(),
+            )
+        }
+        var leftX = 0.dp
+        var topY = 0.dp
+        var rightX: Dp
+        var bottomY: Dp
+
+        // 10.dp is the size of a singular box
+        val gapSize = (rootSize - (10.dp * 2f)) / (columns * 2f)
+        rule.waitForIdle()
+        leftX += gapSize
+        topY += gapSize
+        rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(leftX, topY)
+        rightX = leftX + 10.dp + gapSize + gapSize
+        rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(rightX, topY)
+        bottomY = topY + 10.dp + gapSize + gapSize
+        rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(leftX, bottomY)
+        rule.onNodeWithTag("box3").assertPositionInRootIsEqualTo(rightX, bottomY)
+    }
+
+    @Test
+    fun testOrientation() {
+        val rootSize = 200.dp
+        val boxesCount = 4
+        val rows = 2
+        val columns = 2
+        rule.setContent {
+            gridComposableTest(
+                modifier = Modifier.size(rootSize),
+                boxesCount = boxesCount,
+                gridOrientation = 1,
+                numRows = rows,
+                numColumns = columns,
+                hGap = 0,
+                vGap = 0,
+                gridSpans = "",
+                gridSkips = "",
+                gridRowWeights = intArrayOf(),
+                gridColumnWeights = intArrayOf()
+            )
+        }
+        var leftX = 0.dp
+        var topY = 0.dp
+        var rightX: Dp
+        var bottomY: Dp
+
+        // 10.dp is the size of a singular box
+        val gapSize = (rootSize - (10.dp * 2f)) / (columns * 2f)
+        rule.waitForIdle()
+        leftX += gapSize
+        topY += gapSize
+        rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(leftX, topY)
+        rightX = leftX + 10.dp + gapSize + gapSize
+        rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(rightX, topY)
+        bottomY = topY + 10.dp + gapSize + gapSize
+        rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(leftX, bottomY)
+        rule.onNodeWithTag("box3").assertPositionInRootIsEqualTo(rightX, bottomY)
+    }
+
+    @Test
+    fun testRows() {
+        val rootSize = 200.dp
+        val boxesCount = 4
+        val rows = 0
+        val columns = 1
+        rule.setContent {
+            gridComposableTest(
+                modifier = Modifier.size(rootSize),
+                boxesCount = boxesCount,
+                gridOrientation = 0,
+                numRows = rows,
+                numColumns = columns,
+                hGap = 0,
+                vGap = 0,
+                gridSpans = "",
+                gridSkips = "",
+                gridRowWeights = intArrayOf(),
+                gridColumnWeights = intArrayOf()
+            )
+        }
+        var expectedX = 0.dp
+        var expectedY = 0.dp
+
+        // 10.dp is the size of a singular box
+        val hGapSize = (rootSize - 10.dp) / 2f
+        val vGapSize = (rootSize - (10.dp * 4f)) / (boxesCount * 2f)
+        rule.waitForIdle()
+        expectedX += hGapSize
+        expectedY += vGapSize
+        rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(expectedX, expectedY)
+        expectedY += vGapSize + vGapSize + 10.dp
+        rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(expectedX, expectedY)
+        expectedY += vGapSize + vGapSize + 10.dp
+        rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(expectedX, expectedY)
+        expectedY += vGapSize + vGapSize + 10.dp
+        rule.onNodeWithTag("box3").assertPositionInRootIsEqualTo(expectedX, expectedY)
+    }
+
+    @Test
+    fun testColumns() {
+        val rootSize = 200.dp
+        val boxesCount = 4
+        val rows = 1
+        val columns = 0
+        rule.setContent {
+            gridComposableTest(
+                modifier = Modifier.size(rootSize),
+                boxesCount = boxesCount,
+                gridOrientation = 0,
+                numRows = rows,
+                numColumns = columns,
+                hGap = 0,
+                vGap = 0,
+                gridSpans = "",
+                gridSkips = "",
+                gridRowWeights = intArrayOf(),
+                gridColumnWeights = intArrayOf()
+            )
+        }
+        var expectedX = 0.dp
+        var expectedY = 0.dp
+
+        // 10.dp is the size of a singular box
+        val hGapSize = (rootSize - (10.dp * 4f)) / (boxesCount * 2f)
+        val vGapSize = (rootSize - 10.dp) / 2f
+        rule.waitForIdle()
+        expectedX += hGapSize
+        expectedY += vGapSize
+        rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(expectedX, expectedY)
+        expectedX += hGapSize + hGapSize + 10.dp
+        rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(expectedX, expectedY)
+        expectedX += hGapSize + hGapSize + 10.dp
+        rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(expectedX, expectedY)
+        expectedX += hGapSize + hGapSize + 10.dp
+        rule.onNodeWithTag("box3").assertPositionInRootIsEqualTo(expectedX, expectedY)
+    }
+
+    @Test
+    fun testSkips() {
+        val rootSize = 200.dp
+        val boxesCount = 3
+        val rows = 2
+        val columns = 2
+        rule.setContent {
+            gridComposableTest(
+                modifier = Modifier.size(rootSize),
+                boxesCount = boxesCount,
+                gridOrientation = 0,
+                numRows = rows,
+                numColumns = columns,
+                hGap = 0,
+                vGap = 0,
+                gridSpans = "",
+                gridSkips = "0:1x1",
+                gridRowWeights = intArrayOf(),
+                gridColumnWeights = intArrayOf()
+            )
+        }
+        var leftX = 0.dp
+        var topY = 0.dp
+        var rightX: Dp
+        var bottomY: Dp
+
+        // 10.dp is the size of a singular box
+        val gapSize = (rootSize - (10.dp * 2f)) / (columns * 2f)
+        rule.waitForIdle()
+        leftX += gapSize
+        topY += gapSize
+        rightX = leftX + 10.dp + gapSize + gapSize
+        rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(rightX, topY)
+        bottomY = topY + 10.dp + gapSize + gapSize
+        rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(leftX, bottomY)
+        rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(rightX, bottomY)
+    }
+
+    @Test
+    fun testSpans() {
+        val rootSize = 200.dp
+        val boxesCount = 3
+        val rows = 2
+        val columns = 2
+        rule.setContent {
+            gridComposableTest(
+                modifier = Modifier.size(rootSize),
+                boxesCount = boxesCount,
+                gridOrientation = 0,
+                numRows = rows,
+                numColumns = columns,
+                hGap = 0,
+                vGap = 0,
+                gridSpans = "0:1x2",
+                gridSkips = "",
+                gridRowWeights = intArrayOf(),
+                gridColumnWeights = intArrayOf()
+            )
+        }
+        var leftX = 0.dp
+        var topY = 0.dp
+        var rightX: Dp
+        var bottomY: Dp
+
+        // 10.dp is the size of a singular box
+        var spanLeft = (rootSize - 10.dp) / 2f
+        val gapSize = (rootSize - (10.dp * 2f)) / (columns * 2f)
+        rule.waitForIdle()
+        leftX += gapSize
+        topY += gapSize
+        rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(spanLeft, topY)
+        rightX = leftX + 10.dp + gapSize + gapSize
+        bottomY = topY + 10.dp + gapSize + gapSize
+        rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(leftX, bottomY)
+        rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(rightX, bottomY)
+    }
+
+    @Test
+    fun testRowWeights() {
+        val rootSize = 200.dp
+        val boxesCount = 2
+        val rows = 0
+        val columns = 1
+        val weights = intArrayOf(1, 3)
+        rule.setContent {
+            gridComposableTest(
+                modifier = Modifier.size(rootSize),
+                boxesCount = boxesCount,
+                gridOrientation = 0,
+                numRows = rows,
+                numColumns = columns,
+                hGap = 0,
+                vGap = 0,
+                gridSpans = "",
+                gridSkips = "",
+                gridRowWeights = weights,
+                gridColumnWeights = intArrayOf()
+            )
+        }
+        var expectedLeft = (rootSize - 10.dp) / 2f
+        var expectedTop = 0.dp
+
+        // 10.dp is the size of a singular box
+        // first box takes the 1/4 of the height
+        val firstGapSize = (rootSize / 4 - 10.dp) / 2
+        // second box takes the 3/4 of the height
+        val secondGapSize = ((rootSize * 3 / 4) - 10.dp) / 2
+        rule.waitForIdle()
+        expectedTop += firstGapSize
+        rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(expectedLeft, expectedTop)
+        expectedTop += 10.dp + firstGapSize + secondGapSize
+        rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(expectedLeft, expectedTop)
+    }
+
+    @Test
+    fun testColumnWeights() {
+        val rootSize = 200.dp
+        val boxesCount = 2
+        val rows = 1
+        val columns = 0
+        val weights = intArrayOf(1, 3)
+        rule.setContent {
+            gridComposableTest(
+                modifier = Modifier.size(rootSize),
+                boxesCount = boxesCount,
+                gridOrientation = 0,
+                numRows = rows,
+                numColumns = columns,
+                hGap = 0,
+                vGap = 0,
+                gridSpans = "",
+                gridSkips = "",
+                gridRowWeights = intArrayOf(),
+                gridColumnWeights = weights
+            )
+        }
+        var expectedLeft = 0.dp
+        var expectedTop = (rootSize - 10.dp) / 2f
+
+        // 10.dp is the size of a singular box
+        // first box takes the 1/4 of the width
+        val firstGapSize = (rootSize / 4 - 10.dp) / 2
+        // second box takes the 3/4 of the width
+        val secondGapSize = ((rootSize * 3 / 4) - 10.dp) / 2
+        rule.waitForIdle()
+        expectedLeft += firstGapSize
+
+        rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(expectedLeft, expectedTop)
+        expectedLeft += 10.dp + firstGapSize + secondGapSize
+        rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(expectedLeft, expectedTop)
+    }
+
+    @Test
+    fun testGaps() {
+        val rootSize = 200.dp
+        val hGap = 10.dp
+        val vGap = 20.dp
+        rule.setContent {
+            gridComposableGapTest(
+                modifier = Modifier.size(rootSize),
+                hGap = Math.round(hGap.value),
+                vGap = Math.round(vGap.value),
+            )
+        }
+        var expectedLeft = 0.dp
+        var expectedTop = 0.dp
+
+        val boxWidth = (rootSize - hGap) / 2f
+        val boxHeight = (rootSize - vGap) / 2f
+
+        rule.waitForIdle()
+        rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(0.dp, 0.dp)
+        expectedLeft += boxWidth + hGap
+        rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(expectedLeft, 0.dp)
+        expectedTop += boxHeight + vGap
+        rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(0.dp, expectedTop)
+        rule.onNodeWithTag("box3").assertPositionInRootIsEqualTo(expectedLeft, expectedTop)
+    }
+
+    @Composable
+    private fun gridComposableTest(
+        modifier: Modifier = Modifier,
+        numRows: Int,
+        numColumns: Int,
+        gridSpans: String,
+        gridSkips: String,
+        gridRowWeights: IntArray,
+        gridColumnWeights: IntArray,
+        boxesCount: Int,
+        gridOrientation: Int,
+        vGap: Int,
+        hGap: Int,
+    ) {
+        ConstraintLayout(
+            ConstraintSet {
+                val ids = (0 until boxesCount).map { "box$it" }.toTypedArray()
+                val elem = arrayListOf<LayoutReference>()
+                for (i in ids.indices) {
+                    elem.add(createRefFor(ids[i]))
+                }
+
+                val g1 = createGrid(
+                    elements = elem.toTypedArray(),
+                    orientation = gridOrientation,
+                    skips = gridSkips,
+                    spans = gridSpans,
+                    rows = numRows,
+                    columns = numColumns,
+                    verticalGap = vGap.dp,
+                    horizontalGap = hGap.dp,
+                    rowWeights = gridRowWeights,
+                    columnWeights = gridColumnWeights,
+                )
+                constrain(g1) {
+                    width = Dimension.matchParent
+                    height = Dimension.matchParent
+                }
+            },
+            modifier = modifier
+        ) {
+            val ids = (0 until boxesCount).map { "box$it" }.toTypedArray()
+            ids.forEach { id ->
+                Box(
+                    Modifier
+                        .layoutId(id)
+                        .background(Color.Red)
+                        .testTag(id)
+                        .size(10.dp)
+                )
+            }
+        }
+    }
+
+    @Composable
+    private fun gridComposableGapTest(
+        modifier: Modifier = Modifier,
+        vGap: Int,
+        hGap: Int,
+    ) {
+        ConstraintLayout(
+            ConstraintSet {
+                val a = createRefFor("box0")
+                val b = createRefFor("box1")
+                val c = createRefFor("box2")
+                val d = createRefFor("box3")
+                val g1 = createGrid(
+                    a, b, c, d,
+                    rows = 2,
+                    columns = 2,
+                    verticalGap = vGap.dp,
+                    horizontalGap = hGap.dp,
+                )
+                constrain(g1) {
+                    width = Dimension.matchParent
+                    height = Dimension.matchParent
+                }
+                constrain(a) {
+                    width = Dimension.fillToConstraints
+                    height = Dimension.fillToConstraints
+                }
+                constrain(b) {
+                    width = Dimension.fillToConstraints
+                    height = Dimension.fillToConstraints
+                }
+                constrain(c) {
+                    width = Dimension.fillToConstraints
+                    height = Dimension.fillToConstraints
+                }
+                constrain(d) {
+                    width = Dimension.fillToConstraints
+                    height = Dimension.fillToConstraints
+                }
+            },
+            modifier = modifier,
+        ) {
+            val ids = (0 until 4).map { "box$it" }.toTypedArray()
+            ids.forEach { id ->
+                Box(
+                    Modifier
+                        .layoutId(id)
+                        .background(Color.Red)
+                        .testTag(id)
+                        .size(10.dp)
+                )
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/RowColumnDslTest.kt b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/RowColumnDslTest.kt
new file mode 100644
index 0000000..79e7534
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/RowColumnDslTest.kt
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.constraintlayout.compose
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests for the Grid Helper (Row / Column)
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class RowColumnDslTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Before
+    fun setup() {
+        isDebugInspectorInfoEnabled = true
+    }
+
+    @After
+    fun tearDown() {
+        isDebugInspectorInfoEnabled = false
+    }
+
+    @Test
+    fun testColumns() {
+        val rootSize = 200.dp
+        val boxesCount = 4
+        rule.setContent {
+            ColumnComposableTest(
+                modifier = Modifier.size(rootSize),
+                boxesCount = boxesCount,
+                vGap = 0,
+                gridRowWeights = intArrayOf(),
+            )
+        }
+        var expectedX = 0.dp
+        var expectedY = 0.dp
+
+        // 10.dp is the size of a singular box
+        val hGapSize = (rootSize - 10.dp) / 2f
+        val vGapSize = (rootSize - (10.dp * 4f)) / (boxesCount * 2f)
+        rule.waitForIdle()
+        expectedX += hGapSize
+        expectedY += vGapSize
+        rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(expectedX, expectedY)
+        expectedY += vGapSize + vGapSize + 10.dp
+        rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(expectedX, expectedY)
+        expectedY += vGapSize + vGapSize + 10.dp
+        rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(expectedX, expectedY)
+        expectedY += vGapSize + vGapSize + 10.dp
+        rule.onNodeWithTag("box3").assertPositionInRootIsEqualTo(expectedX, expectedY)
+    }
+
+    @Test
+    fun testRows() {
+        val rootSize = 200.dp
+        val boxesCount = 4
+        rule.setContent {
+            RowComposableTest(
+                modifier = Modifier.size(rootSize),
+                boxesCount = boxesCount,
+                hGap = 0,
+                gridColumnWeights = intArrayOf()
+            )
+        }
+        var expectedX = 0.dp
+        var expectedY = 0.dp
+
+        // 10.dp is the size of a singular box
+        val hGapSize = (rootSize - (10.dp * 4f)) / (boxesCount * 2f)
+        val vGapSize = (rootSize - 10.dp) / 2f
+        rule.waitForIdle()
+        expectedX += hGapSize
+        expectedY += vGapSize
+        rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(expectedX, expectedY)
+        expectedX += hGapSize + hGapSize + 10.dp
+        rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(expectedX, expectedY)
+        expectedX += hGapSize + hGapSize + 10.dp
+        rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(expectedX, expectedY)
+        expectedX += hGapSize + hGapSize + 10.dp
+        rule.onNodeWithTag("box3").assertPositionInRootIsEqualTo(expectedX, expectedY)
+    }
+
+    @Composable
+    private fun ColumnComposableTest(
+        modifier: Modifier = Modifier,
+        gridRowWeights: IntArray,
+        boxesCount: Int,
+        vGap: Int,
+    ) {
+        ConstraintLayout(
+            ConstraintSet {
+                val ids = (0 until boxesCount).map { "box$it" }.toTypedArray()
+                val elem = arrayListOf<LayoutReference>()
+                for (i in ids.indices) {
+                    elem.add(createRefFor(ids[i]))
+                }
+
+                val g1 = createColumn(
+                    elements = elem.toTypedArray(),
+                    verticalGap = vGap.dp,
+                    rowWeights = gridRowWeights,
+                )
+                constrain(g1) {
+                    width = Dimension.matchParent
+                    height = Dimension.matchParent
+                }
+            },
+            modifier = modifier
+        ) {
+            val ids = (0 until boxesCount).map { "box$it" }.toTypedArray()
+            ids.forEach { id ->
+                Box(
+                    Modifier
+                        .layoutId(id)
+                        .background(Color.Red)
+                        .testTag(id)
+                        .size(10.dp)
+                )
+            }
+        }
+    }
+
+    @Composable
+    private fun RowComposableTest(
+        modifier: Modifier = Modifier,
+        gridColumnWeights: IntArray,
+        boxesCount: Int,
+        hGap: Int,
+    ) {
+        ConstraintLayout(
+            ConstraintSet {
+                val ids = (0 until boxesCount).map { "box$it" }.toTypedArray()
+                val elem = arrayListOf<LayoutReference>()
+                for (i in ids.indices) {
+                    elem.add(createRefFor(ids[i]))
+                }
+
+                val g1 = createRow(
+                    elements = elem.toTypedArray(),
+                    horizontalGap = hGap.dp,
+                    columnWeights = gridColumnWeights,
+                )
+                constrain(g1) {
+                    width = Dimension.matchParent
+                    height = Dimension.matchParent
+                }
+            },
+            modifier = modifier
+        ) {
+            val ids = (0 until boxesCount).map { "box$it" }.toTypedArray()
+            ids.forEach { id ->
+                Box(
+                    Modifier
+                        .layoutId(id)
+                        .background(Color.Red)
+                        .testTag(id)
+                        .size(10.dp)
+                )
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/RowColumnTest.kt b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/RowColumnTest.kt
index 92e9b24..3dee3b0 100644
--- a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/RowColumnTest.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/RowColumnTest.kt
@@ -57,13 +57,13 @@
     }
 
     @Test
-    fun testRows() {
+    fun testColumns() {
         val rootSize = 200.dp
         val boxesCount = 4
         rule.setContent {
             RowColumnComposableTest(
                 modifier = Modifier.size(rootSize),
-                type = "'row'",
+                type = "'column'",
                 width = "'parent'",
                 height = "'parent'",
                 boxesCount = boxesCount,
@@ -95,13 +95,13 @@
     }
 
     @Test
-    fun testColumns() {
+    fun testRows() {
         val rootSize = 200.dp
         val boxesCount = 4
         rule.setContent {
             RowColumnComposableTest(
                 modifier = Modifier.size(rootSize),
-                type = "'column'",
+                type = "'row'",
                 width = "'parent'",
                 height = "'parent'",
                 boxesCount = boxesCount,
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayoutBaseScope.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayoutBaseScope.kt
index 2e74d93..02a2026 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayoutBaseScope.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayoutBaseScope.kt
@@ -691,6 +691,578 @@
     }
 
     /**
+     * Creates a Grid based helper that lays out its elements in a single Row.
+     * Example:
+     * ConstraintLayout(
+     *  ConstraintSet {
+     *      val a = createRefFor("1")
+     *      val b = createRefFor("2")
+     *      val c = createRefFor("3")
+     *      val d = createRefFor("4")
+     *      val e = createRefFor("5")
+     *      val weights = intArrayOf(3, 3, 2, 2, 1)
+     *      val g1 = createRow(
+     *          a, b, c, d, e,
+     *          horizontalGap = 10.dp,
+     *          columnWeights = weights,
+     *          padding = 10.dp,
+     *      )
+     *      constrain(g1) {
+     *          width = Dimension.matchParent
+     *          height = Dimension.matchParent
+     *      },
+     *      modifier = Modifier.fillMaxSize()
+     *  ) {
+     *      val numArray = arrayOf("1", "2", "3", "4", "5")
+     *      for (num in numArray) {
+     *          Button(
+     *              modifier = Modifier.layoutId(num).width(120.dp),
+     *              onClick = {},
+     *          ) {
+     *              Text(text = String.format("btn%s", num))
+     *          }
+     *       }
+     *    }
+     *
+     * @param elements [LayoutReference]s to be laid out by the Grid helper
+     * @param horizontalGap defines the gap between views in the x axis
+     * @param columnWeights defines the weight of each column
+     * @param padding sets padding around the content
+     */
+    fun createRow(
+        vararg elements: LayoutReference,
+        horizontalGap: Dp = 0.dp,
+        columnWeights: IntArray = intArrayOf(),
+        padding: Dp = 0.dp,
+    ): ConstrainedLayoutReference {
+        return createGrid(
+            elements = elements,
+            rows = 1,
+            horizontalGap = horizontalGap,
+            columnWeights = columnWeights,
+            paddingLeft = padding,
+            paddingTop = padding,
+            paddingRight = padding,
+            paddingBottom = padding,
+        )
+    }
+
+    /**
+     * Creates a Grid based helper that lays out its elements in a single Row.
+     * Example:
+     * ConstraintLayout(
+     *  ConstraintSet {
+     *      val a = createRefFor("1")
+     *      val b = createRefFor("2")
+     *      val c = createRefFor("3")
+     *      val d = createRefFor("4")
+     *      val e = createRefFor("5")
+     *      val weights = intArrayOf(3, 3, 2, 2, 1)
+     *      val g1 = createRow(
+     *          a, b, c, d, e,
+     *          horizontalGap = 10.dp,
+     *          columnWeights = weights,
+     *          paddingHorizontal = 10.dp,
+     *          paddingVertical = 10.dp,
+     *      )
+     *      constrain(g1) {
+     *          width = Dimension.matchParent
+     *          height = Dimension.matchParent
+     *      },
+     *      modifier = Modifier.fillMaxSize()
+     *  ) {
+     *      val numArray = arrayOf("1", "2", "3", "4", "5")
+     *      for (num in numArray) {
+     *          Button(
+     *              modifier = Modifier.layoutId(num).width(120.dp),
+     *              onClick = {},
+     *          ) {
+     *              Text(text = String.format("btn%s", num))
+     *          }
+     *       }
+     *   }
+     *
+     * @param elements [LayoutReference]s to be laid out by the Grid helper
+     * @param horizontalGap defines the gap between views in the y axis
+     * @param columnWeights defines the weight of each column
+     * @param paddingHorizontal sets paddingLeft and paddingRight of the content
+     * @param paddingVertical sets paddingTop and paddingBottom of the content
+     */
+    fun createRow(
+        vararg elements: LayoutReference,
+        horizontalGap: Dp = 0.dp,
+        columnWeights: IntArray = intArrayOf(),
+        paddingHorizontal: Dp = 0.dp,
+        paddingVertical: Dp = 0.dp,
+    ): ConstrainedLayoutReference {
+        return createGrid(
+            elements = elements,
+            rows = 1,
+            horizontalGap = horizontalGap,
+            columnWeights = columnWeights,
+            paddingLeft = paddingHorizontal,
+            paddingTop = paddingVertical,
+            paddingRight = paddingHorizontal,
+            paddingBottom = paddingVertical,
+        )
+    }
+
+    /**
+     * Creates a Grid based helper that lays out its elements in a single Column.
+     * Example:
+     * ConstraintLayout(
+     *  ConstraintSet {
+     *      val a = createRefFor("1")
+     *      val b = createRefFor("2")
+     *      val c = createRefFor("3")
+     *      val d = createRefFor("4")
+     *      val e = createRefFor("5")
+     *      val weights = intArrayOf(3, 3, 2, 2, 1)
+     *      val g1 = createColumn(
+     *          a, b, c, d, e,
+     *          verticalGap = 10.dp,
+     *          rowWeights = weights,
+     *          padding = 10.dp,
+     *      )
+     *      constrain(g1) {
+     *          width = Dimension.matchParent
+     *          height = Dimension.matchParent
+     *      },
+     *      modifier = Modifier.fillMaxSize()
+     *  ) {
+     *      val numArray = arrayOf("1", "2", "3", "4", "5")
+     *      for (num in numArray) {
+     *          Button(
+     *              modifier = Modifier.layoutId(num).width(120.dp),
+     *              onClick = {},
+     *          ) {
+     *              Text(text = String.format("btn%s", num))
+     *          }
+     *       }
+     *    }
+     *
+     * @param elements [LayoutReference]s to be laid out by the Grid helper
+     * @param verticalGap defines the gap between views in the y axis
+     * @param rowWeights defines the weight of each row
+     * @param padding sets padding around the content
+     */
+    fun createColumn(
+        vararg elements: LayoutReference,
+        rowWeights: IntArray = intArrayOf(),
+        verticalGap: Dp = 0.dp,
+        padding: Dp = 0.dp,
+    ): ConstrainedLayoutReference {
+        return createGrid(
+            elements = elements,
+            columns = 1,
+            verticalGap = verticalGap,
+            rowWeights = rowWeights,
+            paddingLeft = padding,
+            paddingTop = padding,
+            paddingRight = padding,
+            paddingBottom = padding,
+        )
+    }
+
+    /**
+     * Creates a Grid based helper that lays out its elements in a single Column.
+     * Example:
+     * ConstraintLayout(
+     *  ConstraintSet {
+     *      val a = createRefFor("1")
+     *      val b = createRefFor("2")
+     *      val c = createRefFor("3")
+     *      val d = createRefFor("4")
+     *      val e = createRefFor("5")
+     *      val weights = intArrayOf(3, 3, 2, 2, 1)
+     *      val g1 = createColumn(
+     *          a, b, c, d, e,
+     *          verticalGap = 10.dp,
+     *          rowWeights = weights,
+     *          padding = 10.dp,
+     *      )
+     *      constrain(g1) {
+     *          width = Dimension.matchParent
+     *          height = Dimension.matchParent
+     *      },
+     *      modifier = Modifier.fillMaxSize()
+     *  ) {
+     *      val numArray = arrayOf("1", "2", "3", "4", "5")
+     *      for (num in numArray) {
+     *          Button(
+     *              modifier = Modifier.layoutId(num).width(120.dp),
+     *              onClick = {},
+     *          ) {
+     *              Text(text = String.format("btn%s", num))
+     *          }
+     *       }
+     *    }
+     *
+     * @param elements [LayoutReference]s to be laid out by the Grid helper
+     * @param verticalGap defines the gap between views in the y axis
+     * @param rowWeights defines the weight of each row
+     * @param paddingHorizontal sets paddingLeft and paddingRight of the content
+     * @param paddingVertical sets paddingTop and paddingBottom of the content
+     */
+    fun createColumn(
+        vararg elements: LayoutReference,
+        verticalGap: Dp = 0.dp,
+        rowWeights: IntArray = intArrayOf(),
+        paddingHorizontal: Dp = 0.dp,
+        paddingVertical: Dp = 0.dp,
+    ): ConstrainedLayoutReference {
+        return createGrid(
+            elements = elements,
+            columns = 1,
+            verticalGap = verticalGap,
+            rowWeights = rowWeights,
+            paddingLeft = paddingHorizontal,
+            paddingTop = paddingVertical,
+            paddingRight = paddingHorizontal,
+            paddingBottom = paddingVertical,
+        )
+    }
+
+    /**
+     * Creates a Grid representation with a Grid Helper.
+     * Example:
+     * ConstraintLayout(
+     *  ConstraintSet {
+     *      val a = createRefFor("1")
+     *      val b = createRefFor("2")
+     *      val c = createRefFor("3")
+     *      val d = createRefFor("4")
+     *      val e = createRefFor("5")
+     *      val f = createRefFor("6")
+     *      val g = createRefFor("7")
+     *      val h = createRefFor("8")
+     *      val i = createRefFor("9")
+     *      val j = createRefFor("0")
+     *      val k = createRefFor("box")
+     *      val weights = intArrayOf(3, 3, 2, 2)
+     *      val g1 = createGrid(
+     *          k, a, b, c, d, e, f, g, h, i, j, k,
+     *          rows = 5,
+     *          columns = 3,
+     *          verticalGap = 25.dp,
+     *          horizontalGap = 25.dp,
+     *          spans = "0:1x3",
+     *          skips = "12:1x1",
+     *          rowWeights = weights,
+     *          paddingHorizontal = 10.dp,
+     *          paddingVertical = 10.dp,
+     *      )
+     *      constrain(g1) {
+     *          width = Dimension.matchParent
+     *          height = Dimension.matchParent
+     *      },
+     *      modifier = Modifier.fillMaxSize()
+     *  ) {
+     *      val numArray = arrayOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "0")
+     *      for (num in numArray) {
+     *          Button(
+     *              modifier = Modifier.layoutId(num).width(120.dp),
+     *              onClick = {},
+     *          ) {
+     *              Text(text = String.format("btn%s", num))
+     *          }
+     *      }
+     *      Box(
+     *          modifier = Modifier.background(Color.Gray).layoutId("box"),
+     *          Alignment.BottomEnd
+     *       ) {
+     *          Text("100", fontSize = 80.sp)
+     *       }
+     *    }
+     *
+     * @param elements [LayoutReference]s to be laid out by the Grid helper
+     * @param orientation 0 if horizontal and 1 if vertical
+     * @param rows sets the number of rows in Grid
+     * @param columns sets the number of columns in Grid
+     * @param verticalGap defines the gap between views in the y axis
+     * @param horizontalGap defines the gap between views in the x axis
+     * @param rowWeights defines the weight of each row
+     * @param columnWeights defines the weight of each column
+     * @param skips defines the positions in a Grid to be skipped
+     *        the format of the input string is "index:rowxcol"
+     *        index - the index of the starting position
+     *        row - the number of rows to skip
+     *        col- the number of columns to skip
+     * @param spans defines the spanned area(s) in Grid
+     *        the format of the input string is "index:rowxcol"
+     *        index - the index of the starting position
+     *        row - the number of rows to span
+     *        col- the number of columns to span
+     * @param padding sets padding around the content
+     */
+    fun createGrid(
+        vararg elements: LayoutReference,
+        orientation: Int = 0,
+        rows: Int = 0,
+        columns: Int = 0,
+        verticalGap: Dp = 0.dp,
+        horizontalGap: Dp = 0.dp,
+        rowWeights: IntArray = intArrayOf(),
+        columnWeights: IntArray = intArrayOf(),
+        skips: String = "",
+        spans: String = "",
+        padding: Dp = 0.dp,
+    ): ConstrainedLayoutReference {
+        return createGrid(
+            elements = elements,
+            orientation = orientation,
+            rows = rows,
+            columns = columns,
+            horizontalGap = horizontalGap,
+            verticalGap = verticalGap,
+            rowWeights = rowWeights,
+            columnWeights = columnWeights,
+            skips = skips,
+            spans = spans,
+            paddingLeft = padding,
+            paddingTop = padding,
+            paddingRight = padding,
+            paddingBottom = padding,
+        )
+    }
+
+    /**
+     * Creates a Grid representation with a Grid Helper.
+     * Example:
+     * ConstraintLayout(
+     *  ConstraintSet {
+     *      val a = createRefFor("1")
+     *      val b = createRefFor("2")
+     *      val c = createRefFor("3")
+     *      val d = createRefFor("4")
+     *      val e = createRefFor("5")
+     *      val f = createRefFor("6")
+     *      val g = createRefFor("7")
+     *      val h = createRefFor("8")
+     *      val i = createRefFor("9")
+     *      val j = createRefFor("0")
+     *      val k = createRefFor("box")
+     *      val weights = intArrayOf(3, 3, 2, 2)
+     *      val g1 = createGrid(
+     *          k, a, b, c, d, e, f, g, h, i, j, k,
+     *          rows = 5,
+     *          columns = 3,
+     *          verticalGap = 25.dp,
+     *          horizontalGap = 25.dp,
+     *          spans = "0:1x3",
+     *          skips = "12:1x1",
+     *          rowWeights = weights,
+     *          paddingHorizontal = 10.dp,
+     *          paddingVertical = 10.dp,
+     *      )
+     *      constrain(g1) {
+     *          width = Dimension.matchParent
+     *          height = Dimension.matchParent
+     *      },
+     *      modifier = Modifier.fillMaxSize()
+     *  ) {
+     *      val numArray = arrayOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "0")
+     *      for (num in numArray) {
+     *          Button(
+     *              modifier = Modifier.layoutId(num).width(120.dp),
+     *              onClick = {},
+     *          ) {
+     *              Text(text = String.format("btn%s", num))
+     *          }
+     *      }
+     *      Box(
+     *          modifier = Modifier.background(Color.Gray).layoutId("box"),
+     *          Alignment.BottomEnd
+     *       ) {
+     *          Text("100", fontSize = 80.sp)
+     *       }
+     *    }
+     *
+     * @param elements [LayoutReference]s to be laid out by the Grid helper
+     * @param rowWeights defines the weight of each row
+     * @param rows sets the number of rows in Grid
+     * @param columns sets the number of columns in Grid
+     * @param verticalGap defines the gap between views in the y axis
+     * @param horizontalGap defines the gap between views in the x axis
+     * @param columnWeights defines the weight of each column
+     * @param orientation 0 if horizontal and 1 if vertical
+     * @param skips defines the positions in a Grid to be skipped
+     *        the format of the input string is "index:rowxcol"
+     *        index - the index of the starting position
+     *        row - the number of rows to skip
+     *        col- the number of columns to skip
+     * @param spans defines the spanned area(s) in Grid
+     *        the format of the input string is "index:rowxcol"
+     *        index - the index of the starting position
+     *        row - the number of rows to span
+     *        col- the number of columns to span
+     * @param paddingHorizontal sets paddingLeft and paddingRight of the content
+     * @param paddingVertical sets paddingTop and paddingBottom of the content
+     */
+    fun createGrid(
+        vararg elements: LayoutReference,
+        orientation: Int = 0,
+        rows: Int = 0,
+        columns: Int = 0,
+        verticalGap: Dp = 0.dp,
+        horizontalGap: Dp = 0.dp,
+        rowWeights: IntArray = intArrayOf(),
+        columnWeights: IntArray = intArrayOf(),
+        skips: String = "",
+        spans: String = "",
+        paddingHorizontal: Dp = 0.dp,
+        paddingVertical: Dp = 0.dp,
+    ): ConstrainedLayoutReference {
+        return createGrid(
+            elements = elements,
+            rowWeights = rowWeights,
+            columnWeights = columnWeights,
+            orientation = orientation,
+            rows = rows,
+            columns = columns,
+            horizontalGap = horizontalGap,
+            verticalGap = verticalGap,
+            skips = skips,
+            spans = spans,
+            paddingLeft = paddingHorizontal,
+            paddingTop = paddingVertical,
+            paddingRight = paddingHorizontal,
+            paddingBottom = paddingVertical,
+        )
+    }
+
+    /**
+     * Creates a Grid representation with a Grid Helper.
+     * Example:
+     * ConstraintLayout(
+     *  ConstraintSet {
+     *      val a = createRefFor("1")
+     *      val b = createRefFor("2")
+     *      val c = createRefFor("3")
+     *      val d = createRefFor("4")
+     *      val e = createRefFor("5")
+     *      val f = createRefFor("6")
+     *      val g = createRefFor("7")
+     *      val h = createRefFor("8")
+     *      val i = createRefFor("9")
+     *      val j = createRefFor("0")
+     *      val k = createRefFor("box")
+     *      val weights = intArrayOf(3, 3, 2, 2)
+     *      val g1 = createGrid(
+     *          k, a, b, c, d, e, f, g, h, i, j, k,
+     *          rows = 5,
+     *          columns = 3,
+     *          verticalGap = 25.dp,
+     *          horizontalGap = 25.dp,
+     *          spans = "0:1x3",
+     *          skips = "12:1x1",
+     *          rowWeights = weights,
+     *          paddingLeft = 10.dp,
+     *          paddingTop = 10.dp,
+     *          paddingRight = 10.dp,
+     *          paddingBottom = 10.dp,
+     *      )
+     *      constrain(g1) {
+     *          width = Dimension.matchParent
+     *          height = Dimension.matchParent
+     *      },
+     *      modifier = Modifier.fillMaxSize()
+     *  ) {
+     *      val numArray = arrayOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "0")
+     *      for (num in numArray) {
+     *          Button(
+     *              modifier = Modifier.layoutId(num).width(120.dp),
+     *              onClick = {},
+     *          ) {
+     *              Text(text = String.format("btn%s", num))
+     *          }
+     *      }
+     *      Box(
+     *          modifier = Modifier.background(Color.Gray).layoutId("box"),
+     *          Alignment.BottomEnd
+     *       ) {
+     *          Text("100", fontSize = 80.sp)
+     *       }
+     *    }
+     *
+     * @param elements [LayoutReference]s to be laid out by the Grid helper
+     * @param orientation 0 if horizontal and 1 if vertical
+     * @param rows sets the number of rows in Grid
+     * @param columns sets the number of columns in Grid
+     * @param verticalGap defines the gap between views in the y axis
+     * @param horizontalGap defines the gap between views in the x axis
+     * @param rowWeights defines the weight of each row
+     * @param columnWeights defines the weight of each column
+     * @param skips defines the positions in a Grid to be skipped
+     *        the format of the input string is "index:rowxcol"
+     *        index - the index of the starting position
+     *        row - the number of rows to skip
+     *        col- the number of columns to skip
+     * @param spans defines the spanned area(s) in Grid
+     *        the format of the input string is "index:rowxcol"
+     *        index - the index of the starting position
+     *        row - the number of rows to span
+     *        col- the number of columns to span
+     * @param paddingLeft sets paddingLeft of the content
+     * @param paddingTop sets paddingTop of the content
+     * @param paddingRight sets paddingRight of the content
+     * @param paddingBottom sets paddingBottom of the content
+     */
+    fun createGrid(
+        vararg elements: LayoutReference,
+        orientation: Int = 0,
+        rows: Int = 0,
+        columns: Int = 0,
+        verticalGap: Dp = 0.dp,
+        horizontalGap: Dp = 0.dp,
+        rowWeights: IntArray = intArrayOf(),
+        columnWeights: IntArray = intArrayOf(),
+        skips: String = "",
+        spans: String = "",
+        paddingLeft: Dp = 0.dp,
+        paddingTop: Dp = 0.dp,
+        paddingRight: Dp = 0.dp,
+        paddingBottom: Dp = 0.dp,
+    ): ConstrainedLayoutReference {
+        val ref = ConstrainedLayoutReference(createHelperId())
+        val elementArray = CLArray(charArrayOf())
+        elements.forEach {
+            elementArray.add(CLString.from(it.id.toString()))
+        }
+        val paddingArray = CLArray(charArrayOf()).apply {
+            add(CLNumber(paddingLeft.value))
+            add(CLNumber(paddingTop.value))
+            add(CLNumber(paddingRight.value))
+            add(CLNumber(paddingBottom.value))
+        }
+        var strRowWeights = ""
+        var strColumnWeights = ""
+        if (rowWeights.size > 1) {
+            strRowWeights = rowWeights.joinToString(",")
+        }
+        if (columnWeights.size > 1) {
+            strColumnWeights = columnWeights.joinToString(",")
+        }
+
+        ref.asCLContainer().apply {
+            put("contains", elementArray)
+            putString("type", "grid")
+            putNumber("orientation", orientation.toFloat())
+            putNumber("rows", rows.toFloat())
+            putNumber("columns", columns.toFloat())
+            putNumber("vGap", verticalGap.value)
+            putNumber("hGap", horizontalGap.value)
+            put("padding", paddingArray)
+            putString("rowWeights", strRowWeights)
+            putString("columnWeights", strColumnWeights)
+            putString("skips", skips)
+            putString("spans", spans)
+        }
+
+        return ref
+    }
+
+    /**
      * Creates a horizontal chain including the referenced layouts.
      *
      * Use [constrain] with the resulting [HorizontalChainReference] to modify the start/left and
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/api/current.txt b/constraintlayout/constraintlayout-core/api/current.txt
index a73dc5b..8eee998 100644
--- a/constraintlayout/constraintlayout-core/api/current.txt
+++ b/constraintlayout/constraintlayout-core/api/current.txt
@@ -2599,6 +2599,10 @@
     method public int getColumnsSet();
     method public float getHorizontalGaps();
     method public int getOrientation();
+    method public int getPaddingBottom();
+    method public int getPaddingLeft();
+    method public int getPaddingRight();
+    method public int getPaddingTop();
     method public String? getRowWeights();
     method public int getRowsSet();
     method public String? getSkips();
@@ -2608,6 +2612,10 @@
     method public void setColumnsSet(int);
     method public void setHorizontalGaps(float);
     method public void setOrientation(int);
+    method public void setPaddingBottom(int);
+    method public void setPaddingLeft(int);
+    method public void setPaddingRight(int);
+    method public void setPaddingTop(int);
     method public void setRowWeights(String);
     method public void setRowsSet(int);
     method public void setSkips(String);
diff --git a/constraintlayout/constraintlayout-core/api/public_plus_experimental_current.txt b/constraintlayout/constraintlayout-core/api/public_plus_experimental_current.txt
index a73dc5b..8eee998 100644
--- a/constraintlayout/constraintlayout-core/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout-core/api/public_plus_experimental_current.txt
@@ -2599,6 +2599,10 @@
     method public int getColumnsSet();
     method public float getHorizontalGaps();
     method public int getOrientation();
+    method public int getPaddingBottom();
+    method public int getPaddingLeft();
+    method public int getPaddingRight();
+    method public int getPaddingTop();
     method public String? getRowWeights();
     method public int getRowsSet();
     method public String? getSkips();
@@ -2608,6 +2612,10 @@
     method public void setColumnsSet(int);
     method public void setHorizontalGaps(float);
     method public void setOrientation(int);
+    method public void setPaddingBottom(int);
+    method public void setPaddingLeft(int);
+    method public void setPaddingRight(int);
+    method public void setPaddingTop(int);
     method public void setRowWeights(String);
     method public void setRowsSet(int);
     method public void setSkips(String);
diff --git a/constraintlayout/constraintlayout-core/api/restricted_current.txt b/constraintlayout/constraintlayout-core/api/restricted_current.txt
index 8b557da..a7d6598 100644
--- a/constraintlayout/constraintlayout-core/api/restricted_current.txt
+++ b/constraintlayout/constraintlayout-core/api/restricted_current.txt
@@ -2602,6 +2602,10 @@
     method public int getColumnsSet();
     method public float getHorizontalGaps();
     method public int getOrientation();
+    method public int getPaddingBottom();
+    method public int getPaddingLeft();
+    method public int getPaddingRight();
+    method public int getPaddingTop();
     method public String? getRowWeights();
     method public int getRowsSet();
     method public String? getSkips();
@@ -2611,6 +2615,10 @@
     method public void setColumnsSet(int);
     method public void setHorizontalGaps(float);
     method public void setOrientation(int);
+    method public void setPaddingBottom(int);
+    method public void setPaddingLeft(int);
+    method public void setPaddingRight(int);
+    method public void setPaddingTop(int);
     method public void setRowWeights(String);
     method public void setRowsSet(int);
     method public void setSkips(String);
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/Motion.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/Motion.java
index 7bbecb4..044ff78 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/Motion.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/Motion.java
@@ -47,8 +47,8 @@
 import java.util.HashSet;
 
 /**
- * This contains the picture of a view through the a transition and is used to interpolate it
- * During an transition every view has a MotionController which drives its position.
+ * Contains the picture of a view through a transition and is used to interpolate it.
+ * During a transition every view has a MotionController which drives its position.
  * <p>
  * All parameter which affect a views motion are added to MotionController and then setup()
  * builds out the splines that control the view.
@@ -231,7 +231,7 @@
     }
 
     /**
-     * Will return the id of the view to move relative to
+     * Returns the id of the view to move relative to.
      * The position at the start and then end will be viewed relative to this view
      * -1 is the return value if NOT in polar mode
      *
@@ -275,7 +275,8 @@
     }
 
     /**
-     * fill the array point with the center coordinates point[0] is filled with the
+     * Fills the array point with the center coordinates.
+     * point[0] is filled with the
      * x coordinate of "time" 0.0 mPoints[point.length-1] is filled with the y coordinate of "time"
      * 1.0
      *
@@ -364,7 +365,8 @@
     }
 
     /**
-     * fill the array point with the center coordinates point[0] is filled with the
+     * Fills the array point with the center coordinates.
+     * point[0] is filled with the
      * x coordinate of "time" 0.0 mPoints[point.length-1] is filled with the y coordinate of "time"
      * 1.0
      *
@@ -1200,7 +1202,7 @@
     }
 
     /**
-     * Calculates the adjusted (and optional velocity)
+     * Calculates the adjusted position (and optional velocity).
      * Note if requesting velocity staggering is not considered
      *
      * @param position position pre stagger
@@ -1634,8 +1636,8 @@
     }
 
     /**
-     * Get the keyFrames for the view controlled by this MotionController
-     * the info data structure is of the form
+     * Gets the keyFrames for the view controlled by this MotionController.
+     * The info data structure is of the form
      * 0 length if your are at index i the [i+len+1] is the next entry
      * 1 type  1=Attributes, 2=Position, 3=TimeCycle 4=Cycle 5=Trigger
      * 2 position
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/parser/CLElement.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/parser/CLElement.java
index 371c7a8..f0f2ca1 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/parser/CLElement.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/parser/CLElement.java
@@ -118,6 +118,10 @@
     // @TODO: add description
     public String content() {
         String content = new String(mContent);
+        // Handle empty string
+        if (content.length() < 1) {
+            return "";
+        }
         if (mEnd == Long.MAX_VALUE || mEnd < mStart) {
             return content.substring((int) mStart, (int) mStart + 1);
         }
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..ef32159 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
@@ -67,8 +67,8 @@
         }
 
         DesignElement(String id,
-                String type,
-                HashMap<String, String> params) {
+                      String type,
+                      HashMap<String, String> params) {
             mId = id;
             mType = type;
             mParams = params;
@@ -98,11 +98,11 @@
         }
 
         void put(String elementName,
-                float from,
-                float to,
-                float step,
-                String prefix,
-                String postfix) {
+                 float from,
+                 float to,
+                 float step,
+                 String prefix,
+                 String postfix) {
             if (mGenerators.containsKey(elementName)) {
                 if (mGenerators.get(elementName) instanceof OverrideValue) {
                     return;
@@ -197,10 +197,10 @@
         float mMax;
 
         FiniteGenerator(float from,
-                float to,
-                float step,
-                String prefix,
-                String postfix) {
+                        float to,
+                        float step,
+                        String prefix,
+                        String postfix) {
             mFrom = from;
             mTo = to;
             mStep = step;
@@ -343,7 +343,7 @@
      * Parse ConstraintSets and populate MotionScene
      */
     static void parseConstraintSets(CoreMotionScene scene,
-            CLObject json) throws CLParsingException {
+                                    CLObject json) throws CLParsingException {
         ArrayList<String> constraintSetNames = json.names();
         if (constraintSetNames == null) {
             return;
@@ -383,7 +383,7 @@
     }
 
     static void override(CLObject baseJson,
-            String name, CLObject overrideValue) throws CLParsingException {
+                         String name, CLObject overrideValue) throws CLParsingException {
         if (!baseJson.has(name)) {
             baseJson.put(name, overrideValue);
         } else {
@@ -590,8 +590,8 @@
     }
 
     private static void parseVariables(State state,
-            LayoutVariables layoutVariables,
-            CLObject json) throws CLParsingException {
+                                       LayoutVariables layoutVariables,
+                                       CLObject json) throws CLParsingException {
         ArrayList<String> elements = json.names();
         if (elements == null) {
             return;
@@ -682,8 +682,8 @@
     }
 
     static void parseHelpers(State state,
-            LayoutVariables layoutVariables,
-            CLArray element) throws CLParsingException {
+                             LayoutVariables layoutVariables,
+                             CLArray element) throws CLParsingException {
         for (int i = 0; i < element.size(); i++) {
             CLElement helper = element.get(i);
             if (helper instanceof CLArray) {
@@ -709,8 +709,8 @@
     }
 
     static void parseGenerate(State state,
-            LayoutVariables layoutVariables,
-            CLObject json) throws CLParsingException {
+                              LayoutVariables layoutVariables,
+                              CLObject json) throws CLParsingException {
         ArrayList<String> elements = json.names();
         if (elements == null) {
             return;
@@ -727,7 +727,7 @@
     }
 
     static void parseChain(int orientation, State state,
-            LayoutVariables margins, CLArray helper) throws CLParsingException {
+                           LayoutVariables margins, CLArray helper) throws CLParsingException {
         ChainReference chain = (orientation == ConstraintWidget.HORIZONTAL)
                 ? state.horizontalChain() : state.verticalChain();
         CLElement refs = helper.get(1);
@@ -804,10 +804,10 @@
      * @throws CLParsingException
      */
     private static void parseChainType(String orientation,
-            State state,
-            String chainName,
-            LayoutVariables margins,
-            CLObject object) throws CLParsingException {
+                                       State state,
+                                       String chainName,
+                                       LayoutVariables margins,
+                                       CLObject object) throws CLParsingException {
 
         ChainReference chain = (orientation.charAt(0) == 'h')
                 ? state.horizontalChain() : state.verticalChain();
@@ -959,11 +959,15 @@
                     break;
                 case "rows":
                     int rows = element.get(param).getInt();
-                    grid.setRowsSet(rows);
+                    if (rows > 0) {
+                        grid.setRowsSet(rows);
+                    }
                     break;
                 case "columns":
                     int columns = element.get(param).getInt();
-                    grid.setColumnsSet(columns);
+                    if (columns > 0) {
+                        grid.setColumnsSet(columns);
+                    }
                     break;
                 case "hGap":
                     float hGap = element.get(param).getFloat();
@@ -997,6 +1001,37 @@
                         grid.setColumnWeights(columnWeights);
                     }
                     break;
+                case "padding":
+                    CLElement paddingObject = element.get(param);
+                    int paddingLeft = 0;
+                    int paddingTop = 0;
+                    int paddingRight = 0;
+                    int paddingBottom = 0;
+                    if (paddingObject instanceof CLArray && ((CLArray) paddingObject).size() > 1) {
+                        paddingLeft = ((CLArray) paddingObject).getInt(0);
+                        paddingRight = paddingLeft;
+                        paddingTop = ((CLArray) paddingObject).getInt(1);
+                        paddingBottom = paddingTop;
+                        if (((CLArray) paddingObject).size() > 2) {
+                            paddingRight = ((CLArray) paddingObject).getInt(2);
+                            try {
+                                paddingBottom = ((CLArray) paddingObject).getInt(3);
+                            } catch (ArrayIndexOutOfBoundsException e) {
+                                paddingBottom = 0;
+                            }
+
+                        }
+                    } else {
+                        paddingLeft = paddingObject.getInt();
+                        paddingTop = paddingLeft;
+                        paddingRight = paddingLeft;
+                        paddingBottom = paddingLeft;
+                    }
+                    grid.setPaddingLeft(paddingLeft);
+                    grid.setPaddingTop(paddingTop);
+                    grid.setPaddingRight(paddingRight);
+                    grid.setPaddingBottom(paddingBottom);
+                    break;
                 default:
                     ConstraintReference reference = state.constraints(name);
                     applyAttribute(state, layoutVariables, reference, element, param);
@@ -1035,10 +1070,10 @@
      * @throws CLParsingException
      */
     private static void parseFlowType(String flowType,
-                                       State state,
-                                       String flowName,
-                                       LayoutVariables layoutVariables,
-                                       CLObject element) throws CLParsingException {
+                                      State state,
+                                      String flowName,
+                                      LayoutVariables layoutVariables,
+                                      CLObject element) throws CLParsingException {
         boolean isVertical = flowType.charAt(0) == 'v';
         FlowReference flow = state.getFlow(flowName, isVertical);
 
@@ -1094,31 +1129,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);
@@ -1297,7 +1317,7 @@
     }
 
     static void parseGuideline(int orientation,
-            State state, CLArray helper) throws CLParsingException {
+                               State state, CLArray helper) throws CLParsingException {
         CLElement params = helper.get(1);
         if (!(params instanceof CLObject)) {
             return;
@@ -1982,9 +2002,9 @@
     }
 
     static Dimension parseDimension(CLObject element,
-            String constraintName,
-            State state,
-            CorePixelDp dpToPixels) throws CLParsingException {
+                                    String constraintName,
+                                    State state,
+                                    CorePixelDp dpToPixels) throws CLParsingException {
         CLElement dimensionElement = element.get(constraintName);
         Dimension dimension = Dimension.createFixed(0);
         if (dimensionElement instanceof CLString) {
@@ -2049,4 +2069,4 @@
         }
         return null;
     }
-}
+}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/helpers/GridReference.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/helpers/GridReference.java
index eb8bfef..9571ddf 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/helpers/GridReference.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/helpers/GridReference.java
@@ -31,9 +31,9 @@
     public GridReference(@NonNull State state, @NonNull State.Helper type) {
         super(state, type);
         if (type == State.Helper.ROW) {
-            this.mColumnsSet = 1;
-        } else if (type == State.Helper.COLUMN) {
             this.mRowsSet = 1;
+        } else if (type == State.Helper.COLUMN) {
+            this.mColumnsSet = 1;
         }
     }
 
@@ -43,6 +43,26 @@
     private GridCore mGrid;
 
     /**
+     * padding left
+     */
+    private int mPaddingLeft = 0;
+
+    /**
+     * padding right
+     */
+    private int mPaddingRight = 0;
+
+    /**
+     * padding top
+     */
+    private int mPaddingTop = 0;
+
+    /**
+     * padding bottom
+     */
+    private int mPaddingBottom = 0;
+
+    /**
      * The orientation of the widgets arrangement horizontally or vertically
      */
     private int mOrientation;
@@ -88,6 +108,70 @@
     private String mSkips;
 
     /**
+     * get padding left
+     * @return padding left
+     */
+    public int getPaddingLeft() {
+        return mPaddingLeft;
+    }
+
+    /**
+     * set padding left
+     * @param paddingLeft padding left to be set
+     */
+    public void setPaddingLeft(int paddingLeft) {
+        mPaddingLeft = paddingLeft;
+    }
+
+    /**
+     * get padding right
+     * @return padding right
+     */
+    public int getPaddingRight() {
+        return mPaddingRight;
+    }
+
+    /**
+     * set padding right
+     * @param paddingRight padding right to be set
+     */
+    public void setPaddingRight(int paddingRight) {
+        mPaddingRight = paddingRight;
+    }
+
+    /**
+     * get padding top
+     * @return padding top
+     */
+    public int getPaddingTop() {
+        return mPaddingTop;
+    }
+
+    /**
+     * set padding top
+     * @param paddingTop padding top to be set
+     */
+    public void setPaddingTop(int paddingTop) {
+        mPaddingTop = paddingTop;
+    }
+
+    /**
+     * get padding bottom
+     * @return padding bottom
+     */
+    public int getPaddingBottom() {
+        return mPaddingBottom;
+    }
+
+    /**
+     * set padding bottom
+     * @param paddingBottom padding bottom to be set
+     */
+    public void setPaddingBottom(int paddingBottom) {
+        mPaddingBottom = paddingBottom;
+    }
+
+    /**
      * Get the number of rows
      * @return the number of rows
      */
@@ -312,4 +396,4 @@
         // General attributes of a widget
         applyBase();
     }
-}
+}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout/api/current.txt b/constraintlayout/constraintlayout/api/current.txt
index 469ddb0..6e30c9f 100644
--- a/constraintlayout/constraintlayout/api/current.txt
+++ b/constraintlayout/constraintlayout/api/current.txt
@@ -120,6 +120,20 @@
     field protected float mComputedMinY;
   }
 
+  public class LogJson extends androidx.constraintlayout.widget.ConstraintHelper {
+    ctor public LogJson(android.content.Context);
+    ctor public LogJson(android.content.Context, android.util.AttributeSet?);
+    ctor public LogJson(android.content.Context, android.util.AttributeSet?, int);
+    method public void periodicStart();
+    method public void periodicStop();
+    method public void setDelay(int);
+    method public void writeLog();
+    field public static final int LOG_API = 4; // 0x4
+    field public static final int LOG_DELAYED = 2; // 0x2
+    field public static final int LOG_LAYOUT = 3; // 0x3
+    field public static final int LOG_PERIODIC = 1; // 0x1
+  }
+
   public class MotionEffect extends androidx.constraintlayout.motion.widget.MotionHelper {
     ctor public MotionEffect(android.content.Context!);
     ctor public MotionEffect(android.content.Context!, android.util.AttributeSet!);
diff --git a/constraintlayout/constraintlayout/api/public_plus_experimental_current.txt b/constraintlayout/constraintlayout/api/public_plus_experimental_current.txt
index 469ddb0..6e30c9f 100644
--- a/constraintlayout/constraintlayout/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout/api/public_plus_experimental_current.txt
@@ -120,6 +120,20 @@
     field protected float mComputedMinY;
   }
 
+  public class LogJson extends androidx.constraintlayout.widget.ConstraintHelper {
+    ctor public LogJson(android.content.Context);
+    ctor public LogJson(android.content.Context, android.util.AttributeSet?);
+    ctor public LogJson(android.content.Context, android.util.AttributeSet?, int);
+    method public void periodicStart();
+    method public void periodicStop();
+    method public void setDelay(int);
+    method public void writeLog();
+    field public static final int LOG_API = 4; // 0x4
+    field public static final int LOG_DELAYED = 2; // 0x2
+    field public static final int LOG_LAYOUT = 3; // 0x3
+    field public static final int LOG_PERIODIC = 1; // 0x1
+  }
+
   public class MotionEffect extends androidx.constraintlayout.motion.widget.MotionHelper {
     ctor public MotionEffect(android.content.Context!);
     ctor public MotionEffect(android.content.Context!, android.util.AttributeSet!);
diff --git a/constraintlayout/constraintlayout/api/restricted_current.txt b/constraintlayout/constraintlayout/api/restricted_current.txt
index 469ddb0..6e30c9f 100644
--- a/constraintlayout/constraintlayout/api/restricted_current.txt
+++ b/constraintlayout/constraintlayout/api/restricted_current.txt
@@ -120,6 +120,20 @@
     field protected float mComputedMinY;
   }
 
+  public class LogJson extends androidx.constraintlayout.widget.ConstraintHelper {
+    ctor public LogJson(android.content.Context);
+    ctor public LogJson(android.content.Context, android.util.AttributeSet?);
+    ctor public LogJson(android.content.Context, android.util.AttributeSet?, int);
+    method public void periodicStart();
+    method public void periodicStop();
+    method public void setDelay(int);
+    method public void writeLog();
+    field public static final int LOG_API = 4; // 0x4
+    field public static final int LOG_DELAYED = 2; // 0x2
+    field public static final int LOG_LAYOUT = 3; // 0x3
+    field public static final int LOG_PERIODIC = 1; // 0x1
+  }
+
   public class MotionEffect extends androidx.constraintlayout.motion.widget.MotionHelper {
     ctor public MotionEffect(android.content.Context!);
     ctor public MotionEffect(android.content.Context!, android.util.AttributeSet!);
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/LogJson.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/LogJson.java
new file mode 100644
index 0000000..6b1c9db
--- /dev/null
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/LogJson.java
@@ -0,0 +1,772 @@
+/*

+ * Copyright (C) 2023 The Android Open Source Project

+ *

+ * Licensed under the Apache License, Version 2.0 (the "License");

+ * you may not use this file except in compliance with the License.

+ * You may obtain a copy of the License at

+ *

+ *      http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS,

+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+ * See the License for the specific language governing permissions and

+ * limitations under the License.

+ */

+

+package androidx.constraintlayout.helper.widget;

+

+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;

+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;

+

+import static androidx.constraintlayout.widget.ConstraintSet.Layout.UNSET_GONE_MARGIN;

+

+import android.annotation.SuppressLint;

+import android.content.Context;

+import android.content.res.TypedArray;

+import android.os.Environment;

+import android.util.AttributeSet;

+import android.util.Log;

+import android.util.TypedValue;

+import android.view.View;

+import android.view.ViewGroup;

+import android.widget.Button;

+import android.widget.TextView;

+

+import androidx.annotation.RequiresApi;

+import androidx.constraintlayout.motion.widget.Debug;

+import androidx.constraintlayout.widget.ConstraintAttribute;

+import androidx.constraintlayout.widget.ConstraintHelper;

+import androidx.constraintlayout.widget.ConstraintLayout;

+import androidx.constraintlayout.widget.ConstraintSet;

+import androidx.constraintlayout.widget.R;

+

+import java.io.File;

+import java.io.FileOutputStream;

+import java.io.IOException;

+import java.io.StringWriter;

+import java.io.Writer;

+import java.util.HashMap;

+import java.util.concurrent.atomic.AtomicInteger;

+

+/**

+ * This is a class is a debugging/logging utility to write out the constraints in JSON

+ * This is used for debugging purposes

+ * <ul>

+ *     <li>logJsonTo - defines the output log console or "fileName"</li>

+ *     <li>logJsonMode - mode one of:

+ *     <b>periodic</b>, <b>delayed</b>, <b>layout</b> or <b>api</b></li>

+ *     <li>logJsonDelay - the duration of the delay or the delay between repeated logs</li>

+ * </ul>

+ * logJsonTo supports:

+ * <ul>

+ *     <li>log - logs using log.v("JSON5", ...)</li>

+ *     <li>console - logs using System.out.println(...)</li>

+ *     <li>[fileName] - will write to /storage/emulated/0/Download/[fileName].json5</li>

+ * </ul>

+ * logJsonMode modes are:

+ * <ul>

+ *     <li>periodic - after window is attached will log every delay ms</li>

+ *     <li>delayed - log once after delay ms</li>

+ *     <li>layout - log every time there is a layout call</li>

+ *     <li>api - do not automatically log developer will call writeLog</li>

+ * </ul>

+ *

+ * The defaults are:

+ * <ul>

+ *     <li>logJsonTo="log"</li>

+ *     <li>logJsonMode="delayed"</li>

+ *     <li>logJsonDelay="1000"</li>

+ * </ul>

+ *  Usage:

+ *  <p></p>

+ *  <pre>

+ *  {@code

+ *      <androidx.constraintlayout.helper.widget.LogJson

+ *         android:layout_width="0dp"

+ *         android:layout_height="0dp"

+ *         android:visibility="gone"

+ *         app:logJsonTo="log"

+ *         app:logJsonMode="delayed"

+ *         app:logJsonDelay="1000"

+ *         />

+ *  }

+ * </pre>

+ * </p>

+ */

+public class LogJson extends ConstraintHelper {

+    private static final String TAG = "JSON5";

+    private int mDelay = 1000;

+    private int mMode = LOG_DELAYED;

+    private String mLogToFile = null;

+    private boolean mLogConsole = true;

+

+    public static final int LOG_PERIODIC = 1;

+    public static final int LOG_DELAYED = 2;

+    public static final int LOG_LAYOUT = 3;

+    public static final int LOG_API = 4;

+    private boolean mPeriodic = false;

+

+    public LogJson(@androidx.annotation.NonNull Context context) {

+        super(context);

+    }

+

+    public LogJson(@androidx.annotation.NonNull Context context,

+                   @androidx.annotation.Nullable AttributeSet attrs) {

+        super(context, attrs);

+        initLogJson(attrs);

+

+    }

+

+    public LogJson(@androidx.annotation.NonNull Context context,

+                   @androidx.annotation.Nullable AttributeSet attrs, int defStyleAttr) {

+        super(context, attrs, defStyleAttr);

+        initLogJson(attrs);

+    }

+

+    private void initLogJson(AttributeSet attrs) {

+

+        if (attrs != null) {

+            TypedArray a = getContext().obtainStyledAttributes(attrs,

+                    R.styleable.LogJson);

+            final int count = a.getIndexCount();

+            for (int i = 0; i < count; i++) {

+                int attr = a.getIndex(i);

+                if (attr == R.styleable.LogJson_logJsonDelay) {

+                    mDelay = a.getInt(attr, mDelay);

+                } else if (attr == R.styleable.LogJson_logJsonMode) {

+                    mMode = a.getInt(attr, mMode);

+                } else if (attr == R.styleable.LogJson_logJsonTo) {

+                    TypedValue v = a.peekValue(attr);

+                    if (v.type == TypedValue.TYPE_STRING) {

+                        mLogToFile = a.getString(attr);

+                    } else {

+                        int value = a.getInt(attr, 0);

+                        mLogConsole = value == 2;

+                    }

+                }

+            }

+            a.recycle();

+        }

+        setVisibility(GONE);

+    }

+

+    @Override

+    protected void onAttachedToWindow() {

+        super.onAttachedToWindow();

+        switch (mMode) {

+            case LOG_PERIODIC:

+                mPeriodic = true;

+                this.postDelayed(this::periodic, mDelay);

+                break;

+            case LOG_DELAYED:

+                this.postDelayed(this::writeLog, mDelay);

+                break;

+            case LOG_LAYOUT:

+                ConstraintLayout cl = (ConstraintLayout) getParent();

+                cl.addOnLayoutChangeListener((v, a, b, c, d, e, f, g, h) -> logOnLayout());

+        }

+    }

+

+    private void logOnLayout() {

+        if (mMode == LOG_LAYOUT) {

+            writeLog();

+        }

+    }

+

+    /**

+     * Set the duration of periodic logging of constraints

+     *

+     * @param duration the time in ms between writing files

+     */

+    public void setDelay(int duration) {

+        mDelay = duration;

+    }

+

+    /**

+     * Start periodic sampling

+     */

+    public void periodicStart() {

+        if (mPeriodic) {

+            return;

+        }

+        mPeriodic = true;

+        this.postDelayed(this::periodic, mDelay);

+    }

+

+    /**

+     * Stop periodic sampling

+     */

+    public void periodicStop() {

+        mPeriodic = false;

+    }

+

+    private void periodic() {

+        if (mPeriodic) {

+            writeLog();

+            this.postDelayed(this::periodic, mDelay);

+        }

+    }

+

+    /**

+     * This writes a JSON5 representation of the constraintSet

+     */

+    public void writeLog() {

+        String str = asString((ConstraintLayout) this.getParent());

+        if (mLogToFile == null) {

+            if (mLogConsole) {

+                System.out.println(str);

+            } else {

+                logBigString(str);

+            }

+        } else {

+            String name = toFile(str, mLogToFile);

+            Log.v("JSON", "\"" + name + "\" written!");

+        }

+    }

+

+    /**

+     * This writes the JSON5 description of the constraintLayout to a file named fileName.json5

+     * in the download directory which can be pulled with:

+     * "adb pull "/storage/emulated/0/Download/" ."

+     *

+     * @param str      String to write as a file

+     * @param fileName file name

+     * @return full path name of file

+     */

+    private static String toFile(String str, String fileName) {

+        FileOutputStream outputStream;

+        if (!fileName.endsWith(".json5")) {

+            fileName += ".json5";

+        }

+        try {

+            File down =

+                    Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);

+            File file = new File(down, fileName);

+            outputStream = new FileOutputStream(file);

+            outputStream.write(str.getBytes());

+            outputStream.close();

+            return file.getCanonicalPath();

+        } catch (IOException e) {

+            return e.toString();

+        }

+    }

+

+    @SuppressLint("LogConditional")

+    private void logBigString(String str) {

+        int len = str.length();

+        for (int i = 0; i < len; i++) {

+            int k = str.indexOf("\n", i);

+            if (k == -1) {

+                Log.v(TAG, str.substring(i));

+                break;

+            }

+            Log.v(TAG, str.substring(i, k));

+            i = k;

+        }

+    }

+

+    /**

+     * Get a JSON5 String that represents the Constraints in a running ConstraintLayout

+     *

+     * @param constraintLayout its constraints are converted to a string

+     * @return JSON5 string

+     */

+    private static String asString(ConstraintLayout constraintLayout) {

+        JsonWriter c = new JsonWriter();

+        return c.constraintLayoutToJson(constraintLayout);

+    }

+

+    // ================================== JSON writer==============================================

+

+    private static class JsonWriter {

+        public static final int UNSET = ConstraintLayout.LayoutParams.UNSET;

+        ConstraintSet mSet;

+        Writer mWriter;

+        Context mContext;

+        int mUnknownCount = 0;

+        final String mLEFT = "left";

+        final String mRIGHT = "right";

+        final String mBASELINE = "baseline";

+        final String mBOTTOM = "bottom";

+        final String mTOP = "top";

+        final String mSTART = "start";

+        final String mEND = "end";

+        private static final String INDENT = "    ";

+        private static final String SMALL_INDENT = "  ";

+        HashMap<Integer, String> mIdMap = new HashMap<>();

+        private static final String LOG_JSON = LogJson.class.getSimpleName();

+        private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);

+        HashMap<Integer, String> mNames = new HashMap<>();

+

+        private static int generateViewId() {

+            final int max_id = 0x00FFFFFF;

+            for (;;) {

+                final int result = sNextGeneratedId.get();

+                int newValue = result + 1;

+                if (newValue > max_id) {

+                    newValue = 1;

+                }

+                if (sNextGeneratedId.compareAndSet(result, newValue)) {

+                    return result;

+                }

+            }

+        }

+

+        @RequiresApi(17)

+        private static class JellyBean {

+            static int generateViewId() {

+                return View.generateViewId();

+            }

+        }

+

+        String constraintLayoutToJson(ConstraintLayout constraintLayout) {

+            StringWriter writer = new StringWriter();

+

+            int count = constraintLayout.getChildCount();

+            for (int i = 0; i < count; i++) {

+                View v = constraintLayout.getChildAt(i);

+                String name = v.getClass().getSimpleName();

+                int id = v.getId();

+                if (id == -1) {

+                    if (android.os.Build.VERSION.SDK_INT

+                            >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {

+                        id = JellyBean.generateViewId();

+                    } else {

+                        id = generateViewId();

+                    }

+                    v.setId(id);

+                    if (!LOG_JSON.equals(name)) {

+                        name = "noid_" + name;

+                    }

+                    mNames.put(id, name);

+                } else if (LOG_JSON.equals(name)) {

+                    mNames.put(id, name);

+                }

+            }

+            writer.append("{\n");

+

+            writeWidgets(writer, constraintLayout);

+            writer.append("  ConstraintSet:{\n");

+            ConstraintSet set = new ConstraintSet();

+            set.clone(constraintLayout);

+            String name =

+                    (constraintLayout.getId() == -1) ? "cset" : Debug.getName(constraintLayout);

+            try {

+                writer.append(name + ":");

+                setup(writer, set, constraintLayout);

+                writeLayout();

+                writer.append("\n");

+            } catch (IOException e) {

+                throw new RuntimeException(e);

+            }

+            writer.append("  }\n");

+            writer.append("}\n");

+            return writer.toString();

+        }

+

+        private void writeWidgets(StringWriter writer, ConstraintLayout constraintLayout) {

+            writer.append("Widgets:{\n");

+            int count = constraintLayout.getChildCount();

+

+            for (int i = -1; i < count; i++) {

+                View v = (i == -1) ? constraintLayout : constraintLayout.getChildAt(i);

+                int id = v.getId();

+                if (LOG_JSON.equals(v.getClass().getSimpleName())) {

+                    continue;

+                }

+                String name = mNames.containsKey(id) ? mNames.get(id)

+                        : ((i == -1) ? "parent" : Debug.getName(v));

+                String cname = v.getClass().getSimpleName();

+                String bounds = ", bounds: [" + v.getLeft() + ", " + v.getTop()

+                        + ", " + v.getRight() + ", " + v.getBottom() + "]},\n";

+                writer.append("  " + name + ": { ");

+                if (i == -1) {

+                    writer.append("type: '" + v.getClass().getSimpleName() + "' , ");

+

+                    try {

+                        ViewGroup.LayoutParams p = (ViewGroup.LayoutParams) v.getLayoutParams();

+                        String wrap = "'WRAP_CONTENT'";

+                        String match = "'MATCH_PARENT'";

+                        String w = p.width == MATCH_PARENT ? match :

+                                (p.width == WRAP_CONTENT) ? wrap : p.width + "";

+                        writer.append("width: " + w + ", ");

+                        String h = p.height == MATCH_PARENT ? match :

+                                (p.height == WRAP_CONTENT) ? wrap : p.height + "";

+                        writer.append("height: ").append(h);

+                    } catch (Exception e) {

+                    }

+                } else if (cname.contains("Text")) {

+                    if (v instanceof TextView) {

+                        writer.append("type: 'Text', label: '"

+                                + escape(((TextView) v).getText().toString()) + "'");

+                    } else {

+                        writer.append("type: 'Text' },\n");

+                    }

+                } else if (cname.contains("Button")) {

+                    if (v instanceof Button) {

+                        writer.append("type: 'Button', label: '" + ((Button) v).getText() + "'");

+                    } else

+                        writer.append("type: 'Button'");

+                } else if (cname.contains("Image")) {

+                    writer.append("type: 'Image'");

+                } else if (cname.contains("View")) {

+                    writer.append("type: 'Box'");

+                } else {

+                    writer.append("type: '" + v.getClass().getSimpleName() + "'");

+                }

+                writer.append(bounds);

+            }

+            writer.append("},\n");

+        }

+

+        private static String escape(String str) {

+            return str.replaceAll("'", "\\'");

+        }

+

+        JsonWriter() {

+        }

+

+        void setup(Writer writer,

+                   ConstraintSet set,

+                   ConstraintLayout layout) throws IOException {

+            this.mWriter = writer;

+            this.mContext = layout.getContext();

+            this.mSet = set;

+            set.getConstraint(2);

+        }

+

+        private int[] getIDs() {

+            return mSet.getKnownIds();

+        }

+

+        private ConstraintSet.Constraint getConstraint(int id) {

+            return mSet.getConstraint(id);

+        }

+

+        private void writeLayout() throws IOException {

+            mWriter.write("{\n");

+            for (Integer id : getIDs()) {

+                ConstraintSet.Constraint c = getConstraint(id);

+                String idName = getSimpleName(id);

+                if (LOG_JSON.equals(idName)) { // skip LogJson it is for used to log

+                    continue;

+                }

+                mWriter.write(SMALL_INDENT + idName + ":{\n");

+                ConstraintSet.Layout l = c.layout;

+                if (l.mReferenceIds != null) {

+                    StringBuilder ref =

+                            new StringBuilder("type: '_" + idName + "_' , contains: [");

+                    for (int r = 0; r < l.mReferenceIds.length; r++) {

+                        int rid = l.mReferenceIds[r];

+                        ref.append((r == 0) ? "" : ", ").append(getName(rid));

+                    }

+                    mWriter.write(ref + "]\n");

+                }

+                if (l.mReferenceIdString != null) {

+                    StringBuilder ref =

+                            new StringBuilder(SMALL_INDENT + "type: '???' , contains: [");

+                    String[] rids = l.mReferenceIdString.split(",");

+                    for (int r = 0; r < rids.length; r++) {

+                        String rid = rids[r];

+                        ref.append((r == 0) ? "" : ", ").append("`").append(rid).append("`");

+                    }

+                    mWriter.write(ref + "]\n");

+                }

+                writeDimension("height", l.mHeight, l.heightDefault, l.heightPercent,

+                        l.heightMin, l.heightMax, l.constrainedHeight);

+                writeDimension("width", l.mWidth, l.widthDefault, l.widthPercent,

+                        l.widthMin, l.widthMax, l.constrainedWidth);

+

+                writeConstraint(mLEFT, l.leftToLeft, mLEFT, l.leftMargin, l.goneLeftMargin);

+                writeConstraint(mLEFT, l.leftToRight, mRIGHT, l.leftMargin, l.goneLeftMargin);

+                writeConstraint(mRIGHT, l.rightToLeft, mLEFT, l.rightMargin, l.goneRightMargin);

+                writeConstraint(mRIGHT, l.rightToRight, mRIGHT, l.rightMargin, l.goneRightMargin);

+                writeConstraint(mBASELINE, l.baselineToBaseline, mBASELINE, UNSET,

+                        l.goneBaselineMargin);

+                writeConstraint(mBASELINE, l.baselineToTop, mTOP, UNSET, l.goneBaselineMargin);

+                writeConstraint(mBASELINE, l.baselineToBottom,

+                        mBOTTOM, UNSET, l.goneBaselineMargin);

+

+                writeConstraint(mTOP, l.topToBottom, mBOTTOM, l.topMargin, l.goneTopMargin);

+                writeConstraint(mTOP, l.topToTop, mTOP, l.topMargin, l.goneTopMargin);

+                writeConstraint(mBOTTOM, l.bottomToBottom, mBOTTOM, l.bottomMargin,

+                        l.goneBottomMargin);

+                writeConstraint(mBOTTOM, l.bottomToTop, mTOP, l.bottomMargin, l.goneBottomMargin);

+                writeConstraint(mSTART, l.startToStart, mSTART, l.startMargin, l.goneStartMargin);

+                writeConstraint(mSTART, l.startToEnd, mEND, l.startMargin, l.goneStartMargin);

+                writeConstraint(mEND, l.endToStart, mSTART, l.endMargin, l.goneEndMargin);

+                writeConstraint(mEND, l.endToEnd, mEND, l.endMargin, l.goneEndMargin);

+

+                writeVariable("horizontalBias", l.horizontalBias, 0.5f);

+                writeVariable("verticalBias", l.verticalBias, 0.5f);

+

+                writeCircle(l.circleConstraint, l.circleAngle, l.circleRadius);

+

+                writeGuideline(l.orientation, l.guideBegin, l.guideEnd, l.guidePercent);

+                writeVariable("dimensionRatio", l.dimensionRatio);

+                writeVariable("barrierMargin", l.mBarrierMargin);

+                writeVariable("type", l.mHelperType);

+                writeVariable("ReferenceId", l.mReferenceIdString);

+                writeVariable("mBarrierAllowsGoneWidgets",

+                        l.mBarrierAllowsGoneWidgets, true);

+                writeVariable("WrapBehavior", l.mWrapBehavior);

+

+                writeVariable("verticalWeight", l.verticalWeight);

+                writeVariable("horizontalWeight", l.horizontalWeight);

+                writeVariable("horizontalChainStyle", l.horizontalChainStyle);

+                writeVariable("verticalChainStyle", l.verticalChainStyle);

+                writeVariable("barrierDirection", l.mBarrierDirection);

+                if (l.mReferenceIds != null) {

+                    writeVariable("ReferenceIds", l.mReferenceIds);

+                }

+                writeTransform(c.transform);

+                writeCustom(c.mCustomConstraints);

+

+                mWriter.write("  },\n");

+            }

+            mWriter.write("},\n");

+        }

+

+        private void writeTransform(ConstraintSet.Transform transform) throws IOException {

+            if (transform.applyElevation) {

+                writeVariable("elevation", transform.elevation);

+            }

+            writeVariable("rotationX", transform.rotationX, 0);

+            writeVariable("rotationY", transform.rotationY, 0);

+            writeVariable("rotationZ", transform.rotation, 0);

+            writeVariable("scaleX", transform.scaleX, 1);

+            writeVariable("scaleY", transform.scaleY, 1);

+            writeVariable("translationX", transform.translationX, 0);

+            writeVariable("translationY", transform.translationY, 0);

+            writeVariable("translationZ", transform.translationZ, 0);

+        }

+

+        private void writeCustom(HashMap<String, ConstraintAttribute> cset) throws IOException {

+            if (cset != null && cset.size() > 0) {

+                mWriter.write(INDENT + "custom: {\n");

+                for (String s : cset.keySet()) {

+                    ConstraintAttribute attr = cset.get(s);

+                    if (attr == null) {

+                        continue;

+                    }

+                    String custom = INDENT + SMALL_INDENT + attr.getName() + ": ";

+                    switch (attr.getType()) {

+                        case INT_TYPE:

+                            custom += attr.getIntegerValue();

+                            break;

+                        case COLOR_TYPE:

+                            custom += colorString(attr.getColorValue());

+                            break;

+                        case FLOAT_TYPE:

+                            custom += attr.getFloatValue();

+                            break;

+                        case STRING_TYPE:

+                            custom += "'" + attr.getStringValue() + "'";

+                            break;

+                        case DIMENSION_TYPE:

+                            custom = custom + attr.getFloatValue();

+                            break;

+                        case REFERENCE_TYPE:

+                        case COLOR_DRAWABLE_TYPE:

+                        case BOOLEAN_TYPE:

+                            custom = null;

+                    }

+                    if (custom != null) {

+                        mWriter.write(custom + ",\n");

+                    }

+                }

+                mWriter.write(SMALL_INDENT + "   } \n");

+            }

+        }

+

+        private static String colorString(int v) {

+            String str = "00000000" + Integer.toHexString(v);

+            return "#" + str.substring(str.length() - 8);

+        }

+

+        private void writeGuideline(int orientation,

+                                    int guideBegin,

+                                    int guideEnd,

+                                    float guidePercent) throws IOException {

+            writeVariable("orientation", orientation);

+            writeVariable("guideBegin", guideBegin);

+            writeVariable("guideEnd", guideEnd);

+            writeVariable("guidePercent", guidePercent);

+        }

+

+        private void writeDimension(String dimString,

+                                    int dim,

+                                    int dimDefault,

+                                    float dimPercent,

+                                    int dimMin,

+                                    int dimMax,

+                                    boolean unusedConstrainedDim) throws IOException {

+            if (dim == 0) {

+                if (dimMax != UNSET || dimMin != UNSET) {

+                    String s = "-----";

+                    switch (dimDefault) {

+                        case 0: // spread

+                            s = INDENT + dimString + ": {value:'spread'";

+                            break;

+                        case 1: //  wrap

+                            s = INDENT + dimString + ": {value:'wrap'";

+                            break;

+                        case 2: // percent

+                            s = INDENT + dimString + ": {value: '" + dimPercent + "%'";

+                            break;

+                    }

+                    if (dimMax != UNSET) {

+                        s += ", max: " + dimMax;

+                    }

+                    if (dimMax != UNSET) {

+                        s += ", min: " + dimMin;

+                    }

+                    s += "},\n";

+                    mWriter.write(s);

+                    return;

+                }

+

+                switch (dimDefault) {

+                    case 0: // spread is the default

+                        break;

+                    case 1: //  wrap

+                        mWriter.write(INDENT + dimString + ": '???????????',\n");

+                        return;

+                    case 2: // percent

+                        mWriter.write(INDENT + dimString + ": '" + dimPercent + "%',\n");

+                }

+

+            } else if (dim == -2) {

+                mWriter.write(INDENT + dimString + ": 'wrap',\n");

+            } else if (dim == -1) {

+                mWriter.write(INDENT + dimString + ": 'parent',\n");

+            } else {

+                mWriter.write(INDENT + dimString + ": " + dim + ",\n");

+            }

+        }

+

+        private String getSimpleName(int id) {

+            if (mIdMap.containsKey(id)) {

+                return "" + mIdMap.get(id);

+            }

+            if (id == 0) {

+                return "parent";

+            }

+            String name = lookup(id);

+            mIdMap.put(id, name);

+            return "" + name + "";

+        }

+

+        private String getName(int id) {

+            return "'" + getSimpleName(id) + "'";

+        }

+

+        private String lookup(int id) {

+            try {

+                if (mNames.containsKey(id)) {

+                    return mNames.get(id);

+                }

+                if (id != -1) {

+                    return mContext.getResources().getResourceEntryName(id);

+                } else {

+                    return "unknown" + ++mUnknownCount;

+                }

+            } catch (Exception ex) {

+                return "unknown" + ++mUnknownCount;

+            }

+        }

+

+        private void writeConstraint(String my,

+                                     int constraint,

+                                     String other,

+                                     int margin,

+                                     int goneMargin) throws IOException {

+            if (constraint == UNSET) {

+                return;

+            }

+            mWriter.write(INDENT + my);

+            mWriter.write(":[");

+            mWriter.write(getName(constraint));

+            mWriter.write(", ");

+            mWriter.write("'" + other + "'");

+            if (margin != 0 || goneMargin != UNSET_GONE_MARGIN) {

+                mWriter.write(", " + margin);

+                if (goneMargin != UNSET_GONE_MARGIN) {

+                    mWriter.write(", " + goneMargin);

+                }

+            }

+            mWriter.write("],\n");

+        }

+

+        private void writeCircle(int circleConstraint,

+                                 float circleAngle,

+                                 int circleRadius) throws IOException {

+            if (circleConstraint == UNSET) {

+                return;

+            }

+            mWriter.write(INDENT + "circle");

+            mWriter.write(":[");

+            mWriter.write(getName(circleConstraint));

+            mWriter.write(", " + circleAngle);

+            mWriter.write(circleRadius + "],\n");

+        }

+

+        private void writeVariable(String name, int value) throws IOException {

+            if (value == 0 || value == -1) {

+                return;

+            }

+            mWriter.write(INDENT + name);

+            mWriter.write(": " + value);

+            mWriter.write(",\n");

+        }

+

+        private void writeVariable(String name, float value) throws IOException {

+            if (value == UNSET) {

+                return;

+            }

+            mWriter.write(INDENT + name);

+            mWriter.write(": " + value);

+            mWriter.write(",\n");

+        }

+

+        private void writeVariable(String name, float value, float def) throws IOException {

+            if (value == def) {

+                return;

+            }

+            mWriter.write(INDENT + name);

+            mWriter.write(": " + value);

+            mWriter.write(",\n");

+        }

+

+        private void writeVariable(String name, boolean value, boolean def) throws IOException {

+            if (value == def) {

+                return;

+            }

+            mWriter.write(INDENT + name);

+            mWriter.write(": " + value);

+            mWriter.write(",\n");

+        }

+

+        private void writeVariable(String name, int[] value) throws IOException {

+            if (value == null) {

+                return;

+            }

+            mWriter.write(INDENT + name);

+            mWriter.write(": ");

+            for (int i = 0; i < value.length; i++) {

+                mWriter.write(((i == 0) ? "[" : ", ") + getName(value[i]));

+            }

+            mWriter.write("],\n");

+        }

+

+        private void writeVariable(String name, String value) throws IOException {

+            if (value == null) {

+                return;

+            }

+            mWriter.write(INDENT + name);

+            mWriter.write(": '" + value);

+            mWriter.write("',\n");

+        }

+    }

+}

diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/MotionController.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/MotionController.java
index 78ae33e..af29c05 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/MotionController.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/MotionController.java
@@ -54,7 +54,7 @@
 import java.util.HashSet;
 
 /**
- * This contains the picture of a view through the a transition and is used to interpolate it
+ * Contains the picture of a view through a transition and is used to interpolate it.
  * During an transition every view has a MotionController which drives its position.
  * <p>
  * All parameter which affect a views motion are added to MotionController and then setup()
@@ -228,7 +228,7 @@
     }
 
     /**
-     * Will return the id of the view to move relative to
+     * Will return the id of the view to move relative to.
      * The position at the start and then end will be viewed relative to this view
      * -1 is the return value if NOT in polar mode
      *
@@ -287,7 +287,8 @@
     }
 
     /**
-     * fill the array point with the center coordinates point[0] is filled with the
+     * fill the array point with the center coordinates.
+     * point[0] is filled with the
      * x coordinate of "time" 0.0 mPoints[point.length-1] is filled with the y coordinate of "time"
      * 1.0
      *
@@ -375,7 +376,8 @@
     }
 
     /**
-     * fill the array point with the center coordinates point[0] is filled with the
+     * fill the array point with the center coordinates.
+     * point[0] is filled with the
      * x coordinate of "time" 0.0 mPoints[point.length-1] is filled with the y coordinate of "time"
      * 1.0
      *
@@ -1232,7 +1234,7 @@
     }
 
     /**
-     * Calculates the adjusted (and optional velocity)
+     * Calculates the adjusted position (and optional velocity).
      * Note if requesting velocity staggering is not considered
      *
      * @param position position pre stagger
@@ -1663,7 +1665,7 @@
     }
 
     /**
-     * Get the keyFrames for the view controlled by this MotionController
+     * Get the keyFrames for the view controlled by this MotionController.
      * the info data structure is of the form
      * 0 length if your are at index i the [i+len+1] is the next entry
      * 1 type  1=Attributes, 2=Position, 3=TimeCycle 4=Cycle 5=Trigger
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
index 1c94351..d0ece80 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
@@ -67,33 +67,15 @@
  * <p>
  * There are currently various types of constraints that you can use:
  * <ul>
- * <li>
- * <a href="#RelativePositioning">Relative positioning</a>
- * </li>
- * <li>
- * <a href="#Margins">Margins</a>
- * </li>
- * <li>
- * <a href="#CenteringPositioning">Centering positioning</a>
- * </li>
- * <li>
- * <a href="#CircularPositioning">Circular positioning</a>
- * </li>
- * <li>
- * <a href="#VisibilityBehavior">Visibility behavior</a>
- * </li>
- * <li>
- * <a href="#DimensionConstraints">Dimension constraints</a>
- * </li>
- * <li>
- * <a href="#Chains">Chains</a>
- * </li>
- * <li>
- * <a href="#VirtualHelpers">Virtual Helpers objects</a>
- * </li>
- * <li>
- * <a href="#Optimizer">Optimizer</a>
- * </li>
+ * <li>Relative positioning</li>
+ * <li>Margins</li>
+ * <li>Centering positioning</li>
+ * <li>Circular positioning</li>
+ * <li>Visibility behavior</li>
+ * <li>Dimension constraints</li>
+ * <li>Chains</li>
+ * <li>Virtual Helpers objects</li>
+ * <li>Optimizer</li>
  * </ul>
  * </p>
  *
@@ -105,10 +87,9 @@
  * ConstraintLayout.LayoutParams} for layout attributes
  * </p>
  *
- * <div class="special reference">
- * <h3>Developer Guide</h3>
+ * <h2>Developer Guide</h2>
  *
- * <h4 id="RelativePositioning"> Relative positioning </h4>
+ * <h3 id="RelativePositioning"> Relative positioning </h3>
  * <p>
  * Relative positioning is one of the basic building blocks of creating layouts in ConstraintLayout.
  * Those constraints allow you to position a given widget relative to another one. You can constrain
@@ -171,7 +152,7 @@
  *
  * </p>
  *
- * <h4 id="Margins"> Margins </h4>
+ * <h3 id="Margins"> Margins </h3>
  * <p>
  * <div align="center" >
  * <img width="325px" src="resources/images/relative-positioning-margin.png">
@@ -191,7 +172,7 @@
  * </ul>
  * <p>Note that a margin can only be positive or equal to zero,
  * and takes a {@code Dimension}.</p>
- * <h4 id="GoneMargin"> Margins when connected to a GONE widget</h4>
+ * <h3 id="GoneMargin"> Margins when connected to a GONE widget</h3>
  * <p>When a position constraint target's visibility is {@code View.GONE},
  * you can also indicate a different
  * margin value to be used using the following attributes:</p>
@@ -207,7 +188,7 @@
  * </p>
  *
  * </p>
- * <h4 id="CenteringPositioning"> Centering positioning and bias</h4>
+ * <h3 id="CenteringPositioning"> Centering positioning and bias</h3>
  * <p>
  * A useful aspect of {@code ConstraintLayout} is in how it deals with "impossible" constraints.
  * For example, if
@@ -235,7 +216,7 @@
  * in the parent container.
  * This will apply similarly for vertical constraints.
  * </p>
- * <h5 id="Bias">Bias</h5>
+ * <b>Bias</b>
  * <p>
  * The default when encountering such opposite constraints is to center the widget;
  * but you can tweak
@@ -266,7 +247,7 @@
  * </p>
  * </p>
  *
- * <h4 id="CircularPositioning"> Circular positioning (<b>Added in 1.1</b>)</h4>
+ * <h3 id="CircularPositioning"> Circular positioning (<b>Added in 1.1</b>)</h3>
  * <p>
  * You can constrain a widget center relative to another widget center,
  * at an angle and a distance. This allows
@@ -292,7 +273,7 @@
  *         }
  *     </pre>
  * </p>
- * <h4 id="VisibilityBehavior"> Visibility behavior </h4>
+ * <h3 id="VisibilityBehavior"> Visibility behavior </h3>
  * <p>
  * {@code ConstraintLayout} has a specific handling of widgets being marked as {@code View.GONE}.
  * <p>{@code GONE} widgets, as usual, are not going to be displayed and
@@ -327,8 +308,8 @@
  * (see <a href="#GoneMargin">the section above about the gone margin attributes</a>).
  * </p>
  *
- * <h4 id="DimensionConstraints"> Dimensions constraints </h4>
- * <h5>Minimum dimensions on ConstraintLayout</h5>
+ * <h3 id="DimensionConstraints"> Dimensions constraints </h3>
+ * <b>Minimum dimensions on ConstraintLayout</b>
  * <p>
  * You can define minimum and maximum sizes for the {@code ConstraintLayout} itself:
  * <ul>
@@ -340,7 +321,7 @@
  * Those minimum and maximum dimensions will be used by
  * {@code ConstraintLayout} when its dimensions are set to {@code WRAP_CONTENT}.
  * </p>
- * <h5>Widgets dimension constraints</h5>
+ * <b>Widgets dimension constraints</b>
  * <p>
  * The dimension of the widgets can be specified by setting the
  * {@code android:layout_width} and
@@ -367,7 +348,7 @@
  * left/right or top/bottom constraints being set to {@code "parent"}.
  * </p>
  * </p>
- * <h5>WRAP_CONTENT : enforcing constraints (<i><b>Added in 1.1</b></i>)</h5>
+ * <b>WRAP_CONTENT : enforcing constraints (<i>Added in 1.1</i>)</b>
  * <p>
  * If a dimension is set to {@code WRAP_CONTENT}, in versions before 1.1
  * they will be treated as a literal dimension -- meaning, constraints will
@@ -380,11 +361,12 @@
  * <li>{@code app:layout_constrainedHeight="true|false"}</li>
  * </ul>
  * </p>
- * <h5>MATCH_CONSTRAINT dimensions (<i><b>Added in 1.1</b></i>)</h5>
+ * <b>MATCH_CONSTRAINT dimensions (<i>Added in 1.1</i>)</b>
  * <p>
  * When a dimension is set to {@code MATCH_CONSTRAINT},
  * the default behavior is to have the resulting size take all the available space.
  * Several additional modifiers are available:
+ * </p>
  * <ul>
  * <li>{@code layout_constraintWidth_min} and {@code layout_constraintHeight_min} :
  * will set the minimum size for this dimension</li>
@@ -393,11 +375,11 @@
  * <li>{@code layout_constraintWidth_percent} and {@code layout_constraintHeight_percent} :
  * will set the size of this dimension as a percentage of the parent</li>
  * </ul>
- * <h6>Min and Max</h6>
- * The value indicated for min and max can be either a dimension in Dp,
- * or "wrap", which will use the same value as what {@code WRAP_CONTENT} would do.
- * <h6>Percent dimension</h6>
- * To use percent, you need to set the following:
+ * <b>Min and Max</b>
+ * <p>The value indicated for min and max can be either a dimension in Dp,
+ * or "wrap", which will use the same value as what {@code WRAP_CONTENT} would do.</p>
+ * <b>Percent dimension</b>
+ * <p>To use percent, you need to set the following</p>
  * <ul>
  * <li>The dimension should be set to {@code MATCH_CONSTRAINT} (0dp)</li>
  * <li>The default should be set to percent {@code app:layout_constraintWidth_default="percent"}
@@ -405,8 +387,7 @@
  * <li>Then set the {@code layout_constraintWidth_percent}
  * or {@code layout_constraintHeight_percent} attributes to a value between 0 and 1</li>
  * </ul>
- * </p>
- * <h5>Ratio</h5>
+ * <b>Ratio</b>
  * <p>
  * You can also define one dimension of a widget as a ratio of the other one.
  * In order to do that, you
@@ -459,10 +440,10 @@
  *
  * </p>
  *
- * <h4 id="Chains">Chains</h4>
+ * <h3 id="Chains">Chains</h3>
  * <p>Chains provide group-like behavior in a single axis (horizontally or vertically).
  * The other axis can be constrained independently.</p>
- * <h5>Creating a chain</h5>
+ * <b>Creating a chain</b>
  * <p>
  * A set of widgets are considered a chain if they are linked together via a
  * bi-directional connection (see Fig. 9, showing a minimal chain, with two widgets).
@@ -472,7 +453,7 @@
  * <br><b><i>Fig. 9 - Chain</i></b>
  * </div>
  * <p>
- * <h5>Chain heads</h5>
+ * <b>Chain heads</b>
  * <p>
  * Chains are controlled by attributes set on the first element of the chain
  * (the "head" of the chain):
@@ -483,10 +464,10 @@
  * </div>
  * <p>The head is the left-most widget for horizontal chains,
  * and the top-most widget for vertical chains.</p>
- * <h5>Margins in chains</h5>
+ * <b>Margins in chains</b>
  * <p>If margins are specified on connections, they will be taken into account.
  * In the case of spread chains, margins will be deducted from the allocated space.</p>
- * <h5>Chain Style</h5>
+ * <b>Chain Style</b>
  * <p>When setting the attribute {@code layout_constraintHorizontal_chainStyle} or
  * {@code layout_constraintVertical_chainStyle} on the first element of a chain,
  * the behavior of the chain will change according to the specified style
@@ -506,7 +487,7 @@
  * <br><b><i>Fig. 11 - Chains Styles</i></b>
  * </div>
  * </p>
- * <h5>Weighted chains</h5>
+ * <b>Weighted chains</b>
  * <p>The default behavior of a chain is to spread the elements equally in the available space.
  * If one or more elements are using {@code MATCH_CONSTRAINT}, they
  * will use the available empty space (equally divided among themselves).
@@ -518,7 +499,7 @@
  * with the first element using a weight of 2 and the second a weight of 1,
  * the space occupied by the first element will be twice that of the second element.</p>
  *
- * <h5>Margins and chains (<i><b>in 1.1</b></i>)</h5>
+ * <b>Margins and chains (<i>in 1.1</i>)</b>
  * <p>When using margins on elements in a chain, the margins are additive.</p>
  * <p>For example, on a horizontal chain, if one element defines
  * a right margin of 10dp and the next element
@@ -528,7 +509,7 @@
  * leftover space used by chains
  * to position items. The leftover space does not contain the margins.</p>
  *
- * <h4 id="VirtualHelpers"> Virtual Helper objects </h4>
+ * <h3 id="VirtualHelpers"> Virtual Helper objects </h3>
  * <p>In addition to the intrinsic capabilities detailed previously,
  * you can also use special helper objects
  * in {@code ConstraintLayout} to help you with your layout. Currently, the
@@ -538,7 +519,7 @@
  * then be positioned by constraining them to such guidelines. In <b>1.1</b>,
  * {@code Barrier} and {@code Group} were added too.</p>
  *
- * <h4 id="Optimizer">Optimizer (<i><b>in 1.1</b></i>)</h4>
+ * <h3 id="Optimizer">Optimizer (<i><b>in 1.1</b></i>)</h3>
  * <p>
  * In 1.1 we exposed the constraints optimizer. You can decide which optimizations
  * are applied by adding the tag <i>app:layout_optimizationLevel</i>
@@ -556,7 +537,6 @@
  * <p>This attribute is a mask, so you can decide to turn on or off
  * specific optimizations by listing the ones you want.
  * For example: <i>app:layout_optimizationLevel="direct|barrier|chain"</i> </p>
- * </div>
  */
 public class ConstraintLayout extends ViewGroup {
     /**
diff --git a/constraintlayout/constraintlayout/src/main/res/values/attrs.xml b/constraintlayout/constraintlayout/src/main/res/values/attrs.xml
index d9029b17..aba4fb6 100644
--- a/constraintlayout/constraintlayout/src/main/res/values/attrs.xml
+++ b/constraintlayout/constraintlayout/src/main/res/values/attrs.xml
@@ -816,6 +816,21 @@
 
     </declare-styleable>
 
+    <!--    This is the api for logging constraints in JSON5 format -->
+    <declare-styleable name="LogJson">
+        <attr name="logJsonTo" format="enum|string">
+            <enum name="log" value="1" />
+            <enum name="console" value="2" />
+        </attr>
+        <attr name="logJsonMode" format="enum">
+            <enum name="periodic" value="1" />
+            <enum name="delayed" value="2" />
+            <enum name="layout" value="3" />
+            <enum name="api" value="4" />
+        </attr>
+        <attr name="logJsonDelay" format="integer" />
+    </declare-styleable>
+
     <declare-styleable name="Constraint">
         <attr name="android:orientation" />
         <attr name="android:id" />
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-animation/src/main/java/androidx/core/animation/AccelerateInterpolator.java b/core/core-animation/src/main/java/androidx/core/animation/AccelerateInterpolator.java
index bb2b79f..de01686 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/AccelerateInterpolator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/AccelerateInterpolator.java
@@ -27,7 +27,7 @@
 
 /**
  * An interpolator where the rate of change starts out slowly and
- * and then accelerates.
+ * then accelerates.
  */
 public class AccelerateInterpolator implements Interpolator {
     private final float mFactor;
diff --git a/core/core-animation/src/main/java/androidx/core/animation/DecelerateInterpolator.java b/core/core-animation/src/main/java/androidx/core/animation/DecelerateInterpolator.java
index 7ee1d90..3483c0b 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/DecelerateInterpolator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/DecelerateInterpolator.java
@@ -28,7 +28,7 @@
 
 /**
  * An interpolator where the rate of change starts out quickly and
- * and then decelerates.
+ * then decelerates.
  *
  */
 public class DecelerateInterpolator implements Interpolator {
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..5da3a24 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,23 @@
         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});
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
+        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
@@ -327,13 +298,34 @@
     @LargeTest
     @Test
     public void testSplitsDefaultChecksums() throws Exception {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         installSplits(new String[]{TEST_V4_APK, TEST_V4_SPLIT0, TEST_V4_SPLIT1, TEST_V4_SPLIT2,
                 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)
@@ -353,38 +345,82 @@
     @LargeTest
     @Test
     public void testSplitsSha256Checksums() throws Exception {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         installSplits(new String[]{TEST_V4_APK, TEST_V4_SPLIT0, TEST_V4_SPLIT1, TEST_V4_SPLIT2,
                 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");
         }
     }
 
@@ -410,13 +446,25 @@
     @LargeTest
     @Test
     public void testFixedDefaultChecksums() throws Exception {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         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)
@@ -435,20 +483,17 @@
     @LargeTest
     @Test
     public void testFixedV1DefaultChecksums() throws Exception {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         installPackage(TEST_FIXED_APK_V1);
         assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
 
         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)
@@ -467,27 +512,26 @@
     @LargeTest
     @Test
     public void testFixedSha512DefaultChecksums() throws Exception {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         installPackage(TEST_FIXED_APK_V2_SHA512);
         assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
 
         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);
         }
     }
 
@@ -507,21 +551,18 @@
     @LargeTest
     @Test
     public void testFixedVerityDefaultChecksums() throws Exception {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         installPackage(TEST_FIXED_APK_VERITY);
         assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
 
         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)
@@ -735,25 +776,19 @@
     @LargeTest
     @Test
     public void testInstallerChecksumsTrustNone() throws Exception {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         installApkWithChecksums(NO_SIGNATURE);
 
         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)
@@ -771,40 +806,34 @@
     @LargeTest
     @Test
     public void testInstallerChecksumsTrustAll() throws Exception {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         installApkWithChecksums(NO_SIGNATURE);
 
         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)
@@ -846,42 +875,34 @@
     @LargeTest
     @Test
     public void testInstallerSignedChecksums() throws Exception {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         final byte[] signature = InstallerApi31.readSignature();
         final Certificate certificate = InstallerApi31.readCertificate();
 
         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-i18n/src/androidTest/java/androidx/core/i18n/CheckTheJavaApisTest.java b/core/core-i18n/src/androidTest/java/androidx/core/i18n/CheckTheJavaApisTest.java
index 8f35b82..23fa07b 100644
--- a/core/core-i18n/src/androidTest/java/androidx/core/i18n/CheckTheJavaApisTest.java
+++ b/core/core-i18n/src/androidTest/java/androidx/core/i18n/CheckTheJavaApisTest.java
@@ -32,6 +32,7 @@
 import androidx.core.i18n.DateTimeFormatterSkeletonOptions.Timezone;
 import androidx.core.i18n.DateTimeFormatterSkeletonOptions.WeekDay;
 import androidx.core.i18n.DateTimeFormatterSkeletonOptions.Year;
+import androidx.core.os.BuildCompat;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
@@ -85,8 +86,12 @@
     }
 
     @Test @SmallTest
-    @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
+    @SdkSuppress(maxSdkVersion = 33) // b/262909049: Do not run this test on pre-release Android U.
     public void testSkeletonOptions() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         final DateTimeFormatterSkeletonOptions.Builder builder =
                 new DateTimeFormatterSkeletonOptions.Builder()
                         .setYear(Year.NUMERIC)
@@ -103,7 +108,9 @@
 
         DateTimeFormatterSkeletonOptions dateTimeFormatterOptions = builder.build();
         String expected;
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+        if (BuildCompat.isAtLeastU()) {
+            expected = "Mo., 23. August 2021 n. Chr. um 19:53:47,123 MEZ";
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
             expected = "Mo., 23. August 2021 n. Chr., 19:53:47,123 MEZ";
         } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
             expected = "Mo., 23. August 2021 n. Chr., 10:53:47,123 GMT-07:00";
@@ -141,7 +148,9 @@
 
         // Verify DateTimeFormatterSkeletonOptions.fromString
         dateTimeFormatterOptions = DateTimeFormatterSkeletonOptions.fromString("yMMMMdjmsEEEE");
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+        if (BuildCompat.isAtLeastU()) {
+            expected = "Montag, 23. August 2021 um 19:53:47";
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
             expected = "Montag, 23. August 2021, 19:53:47";
         } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
             expected = "Montag, 23. August 2021, 10:53:47";
diff --git a/core/core-i18n/src/androidTest/java/androidx/core/i18n/DateTimeFormatterCommonOptionsTest.kt b/core/core-i18n/src/androidTest/java/androidx/core/i18n/DateTimeFormatterCommonOptionsTest.kt
index 1ac2699a..265f46b 100644
--- a/core/core-i18n/src/androidTest/java/androidx/core/i18n/DateTimeFormatterCommonOptionsTest.kt
+++ b/core/core-i18n/src/androidTest/java/androidx/core/i18n/DateTimeFormatterCommonOptionsTest.kt
@@ -18,16 +18,17 @@
 
 import android.icu.text.DateFormat
 import android.os.Build
+import androidx.core.os.BuildCompat
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
 import java.util.Calendar
 import java.util.GregorianCalendar
 import java.util.Locale
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
 
 /** Must execute on an Android device. */
 @RunWith(AndroidJUnit4::class)
@@ -39,13 +40,15 @@
     )
 
     @Test @SmallTest
-    @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
+    @SdkSuppress(maxSdkVersion = 33) // b/262909049: Do not run this test on pre-release Android U.
     fun test() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         val commonFormats = mapOf(
             DateTimeFormatterCommonOptions.ABBR_MONTH_WEEKDAY_DAY to "Sun, Sep 19",
             DateTimeFormatterCommonOptions.ABBR_MONTH_DAY to "Sep 19",
-            DateTimeFormatterCommonOptions.HOUR_MINUTE to "9:42 PM",
-            DateTimeFormatterCommonOptions.HOUR_MINUTE_SECOND to "9:42:12 PM",
             DateTimeFormatterCommonOptions.MINUTE_SECOND to "42:12",
             DateTimeFormatterCommonOptions.MONTH_DAY to "September 19",
             DateTimeFormatterCommonOptions.MONTH_WEEKDAY_DAY to "Sunday, September 19",
@@ -61,10 +64,24 @@
             DateTimeFormatterCommonOptions.YEAR_NUM_MONTH_DAY to "9/19/2021",
             DateTimeFormatterCommonOptions.YEAR_NUM_MONTH_WEEKDAY_DAY to "Sun, 9/19/2021"
         )
+        val commonFormatsVersionDependent = when {
+            BuildCompat.isAtLeastU() -> mapOf(
+                DateTimeFormatterCommonOptions.HOUR_MINUTE to "9:42\u202FPM",
+                DateTimeFormatterCommonOptions.HOUR_MINUTE_SECOND to "9:42:12\u202FPM"
+            )
+            else -> mapOf(
+                DateTimeFormatterCommonOptions.HOUR_MINUTE to "9:42 PM",
+                DateTimeFormatterCommonOptions.HOUR_MINUTE_SECOND to "9:42:12 PM"
+            )
+        }
         commonFormats.forEach { entry ->
             assertEquals(entry.value,
                 DateTimeFormatter(appContext, entry.key, Locale.US).format(testCalendar))
         }
+        commonFormatsVersionDependent.forEach { entry ->
+            assertEquals(entry.value,
+                DateTimeFormatter(appContext, entry.key, Locale.US).format(testCalendar))
+        }
     }
 
     @Test @SmallTest
diff --git a/core/core-i18n/src/androidTest/java/androidx/core/i18n/DateTimeFormatterTest.kt b/core/core-i18n/src/androidTest/java/androidx/core/i18n/DateTimeFormatterTest.kt
index d16c736..3671c09 100644
--- a/core/core-i18n/src/androidTest/java/androidx/core/i18n/DateTimeFormatterTest.kt
+++ b/core/core-i18n/src/androidTest/java/androidx/core/i18n/DateTimeFormatterTest.kt
@@ -96,11 +96,13 @@
     }
 
     @Test @SmallTest
-    @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun test() {
         val locale = Locale.US
         val options = SkeletonOptions.fromString("yMMMdjms")
-        val expected = "Sep 19, 2021, 9:42:12 PM"
+        val expected = when {
+            BuildCompat.isAtLeastU() -> "Sep 19, 2021, 9:42:12\u202FPM"
+            else -> "Sep 19, 2021, 9:42:12 PM"
+        }
 
         // Test Calendar
         assertEquals(expected, DateTimeFormatter(appContext, options, locale).format(testCalendar))
@@ -111,7 +113,6 @@
     }
 
     @Test @SmallTest
-    @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun testApi() {
         val builder = SkeletonOptions.Builder()
             .setYear(SkeletonOptions.Year.NUMERIC)
@@ -124,14 +125,24 @@
         val localeFr = Locale.FRANCE
         val localeUs = Locale.US
 
-        val expectedUs12 = "Sep 19, 2021, 9:42:12 PM"
+        val expectedUs12 = when {
+            BuildCompat.isAtLeastU() -> "Sep 19, 2021, 9:42:12\u202FPM"
+            else -> "Sep 19, 2021, 9:42:12 PM"
+        }
         val expectedUs24 = "Sep 19, 2021, 21:42:12"
-        val expectedUs12Milli = "Sep 19, 2021, 9:42:12.345 PM"
+        val expectedUs12Milli = when {
+            BuildCompat.isAtLeastU() -> "Sep 19, 2021, 9:42:12.345\u202FPM"
+            else -> "Sep 19, 2021, 9:42:12.345 PM"
+        }
 
         val expectedFr12: String
         val expectedFr24: String
         when {
-            Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { // >= 31
+             BuildCompat.isAtLeastU() -> { // >= 31
+                 expectedFr12 = "19 sept. 2021, 9:42:12\u202FPM"
+                 expectedFr24 = "19 sept. 2021, 21:42:12"
+             }
+             Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { // >= 31
                 expectedFr12 = "19 sept. 2021, 9:42:12 PM"
                 expectedFr24 = "19 sept. 2021, 21:42:12"
             }
@@ -186,10 +197,7 @@
     }
 
     @Test @SmallTest
-    @SdkSuppress(
-        minSdkVersion = AVAILABLE_LANGUAGE_TAG,
-        maxSdkVersion = 33 // b/262909049: Failing on SDK 34
-    )
+    @SdkSuppress(minSdkVersion = AVAILABLE_LANGUAGE_TAG)
     // Without `Locale.forLanguageTag` we can't even build a locale with `-u-` extension.
     fun testSystemSupportForExtensionU() {
         val enUsForceH11 = Locale.forLanguageTag("en-US-u-hc-h11")
@@ -202,21 +210,16 @@
         val expectedUs12: String
         val expectedUs23: String
         val expectedUs24: String
-        if (isHcExtensionHonored) {
-            // TODO: check this. Is `-u-hc-` not honored at all? File bug, maybe implement workaround.
-            expectedUs = "9:42:12 PM"
-            expectedUs11 = expectedUs
-            expectedUs12 = expectedUs
-            // Is this a bug? Looks like h23 is not honored on Android S (API 31)?
-            expectedUs23 = if (isHcExtensionHonored) "9:42:12 PM" else "21:42:12"
-            expectedUs24 = expectedUs23
+        // TODO: check this. Is `-u-hc-` not honored at all? File bug, maybe implement workaround.
+        if (BuildCompat.isAtLeastU()) {
+            expectedUs = "9:42:12\u202FPM"
         } else {
             expectedUs = "9:42:12 PM"
-            expectedUs11 = expectedUs
-            expectedUs12 = expectedUs
-            expectedUs23 = expectedUs
-            expectedUs24 = expectedUs
         }
+        expectedUs11 = expectedUs
+        expectedUs12 = expectedUs
+        expectedUs23 = expectedUs
+        expectedUs24 = expectedUs
 
         var formatter: java.text.DateFormat
 
@@ -234,12 +237,12 @@
     }
 
     @Test @SmallTest
-    @SdkSuppress(
-        minSdkVersion = AVAILABLE_LANGUAGE_TAG,
-        maxSdkVersion = 33 // b/262909049: Failing on SDK 34
-    )
+    @SdkSuppress(minSdkVersion = AVAILABLE_LANGUAGE_TAG)
     fun testHourCycleOverrides() {
-        val expectedUs12 = "Sep 19, 2021, 9:42:12 PM"
+        val expectedUs12 = when {
+            BuildCompat.isAtLeastU() -> "Sep 19, 2021, 9:42:12\u202FPM"
+            else -> "Sep 19, 2021, 9:42:12 PM"
+        }
         val expectedUs24 = "Sep 19, 2021, 21:42:12"
         val builder = SkeletonOptions.Builder()
             .setYear(SkeletonOptions.Year.NUMERIC)
@@ -316,13 +319,14 @@
         } else {
             "12:43 AM || 4:43 AM || 8:43 AM || 12:43 PM || 4:43 PM || 8:43 PM"
         }
-        val expectedZh = if (BuildCompat.isAtLeastT()) {
+        val expectedZh = when {
             // Chinese changed to 24h from ICU 70.1
-            "00:43 || 04:43 || 08:43 || 12:43 || 16:43 || 20:43"
-        } else if (isFlexiblePeriodAvailable) {
-            "凌晨12:43 || 凌晨4:43 || 上午8:43 || 中午12:43 || 下午4:43 || 晚上8:43"
-        } else {
-            "上午12:43 || 上午4:43 || 上午8:43 || 下午12:43 || 下午4:43 || 下午8:43"
+            Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU ->
+                "00:43 || 04:43 || 08:43 || 12:43 || 16:43 || 20:43"
+            isFlexiblePeriodAvailable ->
+                "凌晨12:43 || 凌晨4:43 || 上午8:43 || 中午12:43 || 下午4:43 || 晚上8:43"
+            else ->
+                "上午12:43 || 上午4:43 || 上午8:43 || 下午12:43 || 下午4:43 || 下午8:43"
         }
         val expectedFr = "00:43 || 04:43 || 08:43 || 12:43 || 16:43 || 20:43"
 
@@ -395,7 +399,6 @@
     }
 
     @Test @SmallTest
-    @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun testTimeZone() {
         val builder = SkeletonOptions.Builder()
             .setHour(SkeletonOptions.Hour.NUMERIC)
@@ -412,36 +415,47 @@
 
         var options = builder.build()
         assertEquals(
-            if (isIcuAvailable) {
-                "9:42 PM Mountain Daylight Time"
-            } else {
-                "8:42 PM Pacific Daylight Time"
+            when {
+                BuildCompat.isAtLeastU() -> "9:42\u202FPM Mountain Daylight Time"
+                isIcuAvailable -> "9:42 PM Mountain Daylight Time"
+                else -> "8:42 PM Pacific Daylight Time"
             },
             DateTimeFormatter(appContext, options, locale).format(coloradoTime)
         )
 
         options = builder.setTimezone(SkeletonOptions.Timezone.SHORT).build()
         assertEquals(
-            if (isIcuAvailable) "9:42 PM MDT" else "8:42 PM PDT",
+            when {
+                BuildCompat.isAtLeastU() -> "9:42\u202FPM MDT"
+                isIcuAvailable -> "9:42 PM MDT"
+                else -> "8:42 PM PDT"
+             },
             DateTimeFormatter(appContext, options, locale).format(coloradoTime)
         )
 
         options = builder.setTimezone(SkeletonOptions.Timezone.SHORT_GENERIC).build()
         assertEquals(
-            if (isIcuAvailable) "9:42 PM MT" else "8:42 PM PDT",
+            when {
+                BuildCompat.isAtLeastU() -> "9:42\u202FPM MT"
+                isIcuAvailable -> "9:42 PM MT"
+                else -> "8:42 PM PDT"
+             },
             DateTimeFormatter(appContext, options, locale).format(coloradoTime)
         )
 
         options = builder.setTimezone(SkeletonOptions.Timezone.SHORT_OFFSET).build()
         assertEquals(
-            if (isIcuAvailable) "9:42 PM GMT-6" else "8:42 PM PDT",
+            when {
+                BuildCompat.isAtLeastU() -> "9:42\u202FPM GMT-6"
+                isIcuAvailable -> "9:42 PM GMT-6"
+                else -> "8:42 PM PDT"
+             },
             DateTimeFormatter(appContext, options, locale).format(coloradoTime)
         )
     }
 
     @Test @SmallTest
     // Making sure the APIs honor the default timezone
-    @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun testDefaultTimeZone() {
         val options = SkeletonOptions.Builder()
             .setHour(SkeletonOptions.Hour.NUMERIC)
@@ -451,7 +465,10 @@
         val locale = Locale.US
 
         // Honor the current default timezone
-        val expPDT = "9:42 PM Pacific Daylight Time"
+        val expPDT = when {
+            BuildCompat.isAtLeastU() -> "9:42\u202FPM Pacific Daylight Time"
+            else -> "9:42 PM Pacific Daylight Time"
+        }
         // Test Calendar, Date, and milliseconds
         assertEquals(expPDT, DateTimeFormatter(appContext, options, locale).format(testCalendar))
         assertEquals(expPDT, DateTimeFormatter(appContext, options, locale).format(testDate))
@@ -459,7 +476,10 @@
 
         // Change the default timezone.
         TimeZone.setDefault(TimeZone.getTimeZone("America/Denver"))
-        val expMDT = "10:42 PM Mountain Daylight Time"
+        val expMDT = when {
+            BuildCompat.isAtLeastU() -> "10:42\u202FPM Mountain Daylight Time"
+            else -> "10:42 PM Mountain Daylight Time"
+        }
         // The calendar object already has a time zone of its own, captured at creation time.
         // So not matching the default changed after is the expected behavior.
         // BUT!
diff --git a/core/core-ktx/src/main/java/androidx/core/os/OutcomeReceiver.kt b/core/core-ktx/src/main/java/androidx/core/os/OutcomeReceiver.kt
index 61ded5a..74052a3 100644
--- a/core/core-ktx/src/main/java/androidx/core/os/OutcomeReceiver.kt
+++ b/core/core-ktx/src/main/java/androidx/core/os/OutcomeReceiver.kt
@@ -61,8 +61,7 @@
 private class ContinuationOutcomeReceiver<R, E : Throwable>(
     private val continuation: Continuation<R>
 ) : OutcomeReceiver<R, E>, AtomicBoolean(false) {
-    @Suppress("WRONG_TYPE_PARAMETER_NULLABILITY_FOR_JAVA_OVERRIDE")
-    override fun onResult(result: R) {
+    override fun onResult(result: R & Any) {
         // Do not attempt to resume more than once, even if the caller of the returned
         // OutcomeReceiver is buggy and tries anyway.
         if (compareAndSet(false, true)) {
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index aa9867b..3f5d92d 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -1804,6 +1804,7 @@
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.Q) public static boolean isAtLeastQ();
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
     method @Deprecated @ChecksSdkIntAtLeast(api=31, codename="S") public static boolean isAtLeastS();
+    field @ChecksSdkIntAtLeast(extension=android.os.ext.SdkExtensions.AD_SERVICES) public static final int AD_SERVICES_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.R) public static final int R_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.S) public static final int S_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.TIRAMISU) public static final int T_EXTENSION_INT;
@@ -3135,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 7960fa9..9df9106 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -1807,6 +1807,7 @@
     method @Deprecated @ChecksSdkIntAtLeast(api=32, codename="Sv2") @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static boolean isAtLeastSv2();
     method @ChecksSdkIntAtLeast(api=33, codename="Tiramisu") @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static boolean isAtLeastT();
     method @ChecksSdkIntAtLeast(codename="UpsideDownCake") @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static boolean isAtLeastU();
+    field @ChecksSdkIntAtLeast(extension=android.os.ext.SdkExtensions.AD_SERVICES) public static final int AD_SERVICES_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.R) public static final int R_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.S) public static final int S_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.TIRAMISU) public static final int T_EXTENSION_INT;
@@ -3141,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 63e99d4..51353767 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -2144,6 +2144,7 @@
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.Q) public static boolean isAtLeastQ();
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
     method @Deprecated @ChecksSdkIntAtLeast(api=31, codename="S") public static boolean isAtLeastS();
+    field @ChecksSdkIntAtLeast(extension=android.os.ext.SdkExtensions.AD_SERVICES) public static final int AD_SERVICES_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.R) public static final int R_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.S) public static final int S_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.TIRAMISU) public static final int T_EXTENSION_INT;
@@ -3595,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/graphics/drawable/WrappedDrawableApi14Test.java b/core/core/src/androidTest/java/androidx/core/graphics/drawable/WrappedDrawableApi14Test.java
index c45c598..dc2b172 100644
--- a/core/core/src/androidTest/java/androidx/core/graphics/drawable/WrappedDrawableApi14Test.java
+++ b/core/core/src/androidTest/java/androidx/core/graphics/drawable/WrappedDrawableApi14Test.java
@@ -16,22 +16,23 @@
 
 package androidx.core.graphics.drawable;
 
-import static org.mockito.Mockito.verify;
+import static org.junit.Assert.assertEquals;
 
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.view.View;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.MediumTest;
 import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mockito;
+
+import java.util.concurrent.atomic.AtomicInteger;
 
 @RunWith(AndroidJUnit4.class)
-@MediumTest
+@SmallTest
 public class WrappedDrawableApi14Test {
 
     /**
@@ -40,11 +41,19 @@
     @SdkSuppress(minSdkVersion = 23)
     @Test
     public void testSetLayoutDirection() {
-        // Note that Mockito is VERY SLOW on CF targets, so this test must be medium+.
-        Drawable baseDrawable = Mockito.spy(new ColorDrawable());
+        AtomicInteger layoutDirectionChangedTo = new AtomicInteger(-1);
+
+        Drawable baseDrawable = new ColorDrawable() {
+            @Override
+            public boolean onLayoutDirectionChanged(int layoutDirection) {
+                layoutDirectionChangedTo.set(layoutDirection);
+                return super.onLayoutDirectionChanged(layoutDirection);
+            }
+        };
         WrappedDrawableApi14 drawable = new WrappedDrawableApi14(baseDrawable);
+        drawable.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
         drawable.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
 
-        verify(baseDrawable).setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
+        assertEquals(layoutDirectionChangedTo.get(), View.LAYOUT_DIRECTION_LTR);
     }
 }
diff --git a/core/core/src/androidTest/java/androidx/core/os/BuildCompatTest.java b/core/core/src/androidTest/java/androidx/core/os/BuildCompatTest.java
index 8fd4f53..efb9aa0 100644
--- a/core/core/src/androidTest/java/androidx/core/os/BuildCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/os/BuildCompatTest.java
@@ -16,6 +16,7 @@
 
 package androidx.core.os;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
@@ -50,4 +51,16 @@
         assertFalse(BuildCompat.isAtLeastPreReleaseCodename("RMR1", "REL"));
     }
 
+    @Test
+    public void extensionConstants() {
+        if (!BuildCompat.isAtLeastR()) {
+            assertEquals(0, BuildCompat.R_EXTENSION_INT);
+            assertEquals(0, BuildCompat.S_EXTENSION_INT);
+        }
+        if (BuildCompat.isAtLeastS()) {
+            assertTrue(BuildCompat.R_EXTENSION_INT >= 1);
+            assertTrue(BuildCompat.S_EXTENSION_INT >= 1);
+        }
+    }
+
 }
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/view/inputmethod/EditorInfoCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java
index e4dd61e..1e14e1f 100644
--- a/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java
@@ -23,7 +23,6 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.anyInt;
 
 import android.os.Parcel;
 import android.support.v4.BaseInstrumentationTestCase;
@@ -238,12 +237,12 @@
 
         assertTrue(TextUtils.equals(expectedTextBeforeCursor,
                 EditorInfoCompat.getInitialTextBeforeCursor(editorInfo, LONG_EXP_TEXT_LENGTH,
-                        anyInt())));
+                        0)));
         assertTrue(TextUtils.equals(expectedSelectedText,
-                EditorInfoCompat.getInitialSelectedText(editorInfo, anyInt())));
+                EditorInfoCompat.getInitialSelectedText(editorInfo, 0)));
         assertTrue(TextUtils.equals(expectedTextAfterCursor,
                 EditorInfoCompat.getInitialTextAfterCursor(editorInfo, LONG_EXP_TEXT_LENGTH,
-                        anyInt())));
+                        0)));
     }
 
     @Test
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/app/NotificationCompat.java b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
index d3dda08..4d30246 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
@@ -1073,7 +1073,7 @@
         BubbleMetadata mBubbleMetadata;
         Notification mNotification = new Notification();
         boolean mSilent;
-        Icon mSmallIcon;
+        Object mSmallIcon; // Icon
 
         /**
          * @deprecated This field was not meant to be public.
@@ -4101,8 +4101,7 @@
                     Api28Impl.setGroupConversation((Notification.MessagingStyle) frameworkStyle,
                             mIsGroupConversation);
                 }
-                Api16Impl.setBuilder((Notification.MessagingStyle) frameworkStyle,
-                        builder.getBuilder());
+                Api16Impl.setBuilder((Notification.Style) frameworkStyle, builder.getBuilder());
             } else {
                 Message latestIncomingMessage = findLatestIncomingMessage();
                 // Set the title
@@ -5057,7 +5056,7 @@
                 }
                 if (style != null) {
                     // Before applying the style, we clear the actions.
-                    Api24Impl.setActions(builderAccessor.getBuilder());
+                    Api24Impl.clearActions(builderAccessor.getBuilder());
 
                     Api16Impl.setBuilder(style, builderAccessor.getBuilder());
                     if (mAnswerButtonColor != null) {
@@ -5116,7 +5115,7 @@
                     List<Action> actionsList = getActionsListWithSystemActions();
                     // Clear any existing actions.
                     if (Build.VERSION.SDK_INT >= 24) {
-                        Api24Impl.setActions(builder);
+                        Api24Impl.clearActions(builder);
                     }
                     // Adds the actions to the builder in the proper order.
                     for (Action action : actionsList) {
@@ -5429,10 +5428,12 @@
             private Api24Impl() {
             }
 
+            /**
+             * Clears actions by calling setActions() with an empty list of arguments.
+             */
             @DoNotInline
-            static Notification.Builder setActions(Notification.Builder builder,
-                    Notification.Action... actions) {
-                return builder.setActions(actions);
+            static Notification.Builder clearActions(Notification.Builder builder) {
+                return builder.setActions();
             }
 
             @DoNotInline
@@ -7076,8 +7077,7 @@
                     Action[] actions = new Action[parcelables.size()];
                     for (int i = 0; i < actions.length; i++) {
                         if (Build.VERSION.SDK_INT >= 20) {
-                            actions[i] = NotificationCompat.getActionCompatFromAction(
-                                    (Notification.Action) parcelables.get(i));
+                            actions[i] = Api20Impl.getActionCompatFromAction(parcelables, i);
                         } else if (Build.VERSION.SDK_INT >= 16) {
                             actions[i] = NotificationCompatJellybean.getActionFromBundle(
                                     (Bundle) parcelables.get(i));
@@ -7882,6 +7882,14 @@
             static Notification.Action build(Notification.Action.Builder builder) {
                 return builder.build();
             }
+
+            @DoNotInline
+            public static Action getActionCompatFromAction(ArrayList<Parcelable> parcelables,
+                    int i) {
+                // Cast to Notification.Action (added in API 19) must happen in static inner class.
+                return NotificationCompat.getActionCompatFromAction(
+                        (Notification.Action) parcelables.get(i));
+            }
         }
 
         /**
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java b/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
index 7dbdfda..a0181d8 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
@@ -708,8 +708,8 @@
 
         @DoNotInline
         static Notification.Builder setSound(Notification.Builder builder, Uri sound,
-                AudioAttributes audioAttributes) {
-            return builder.setSound(sound, audioAttributes);
+                Object audioAttributes /** AudioAttributes **/) {
+            return builder.setSound(sound, (AudioAttributes) audioAttributes);
         }
     }
 
@@ -729,8 +729,9 @@
         }
 
         @DoNotInline
-        static Notification.Builder setSmallIcon(Notification.Builder builder, Icon icon) {
-            return builder.setSmallIcon(icon);
+        static Notification.Builder setSmallIcon(Notification.Builder builder,
+                Object icon /** Icon **/) {
+            return builder.setSmallIcon((Icon) icon);
         }
     }
 
@@ -860,8 +861,9 @@
         }
 
         @DoNotInline
-        static Notification.Builder setLocusId(Notification.Builder builder, LocusId locusId) {
-            return builder.setLocusId(locusId);
+        static Notification.Builder setLocusId(Notification.Builder builder,
+                Object locusId /** LocusId **/) {
+            return builder.setLocusId((LocusId) locusId);
         }
 
         @DoNotInline
diff --git a/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java b/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java
index 26d4b89..33bc87c 100644
--- a/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java
@@ -449,7 +449,7 @@
      * <p>When an app is upgraded and a shortcut is no longer published from AndroidManifest.xml,
      * this will be set to {@code false}.  If the shortcut is not pinned, then it'll disappear.
      * However, if it's pinned, it will still be visible, {@link #isEnabled()} will be
-     * {@code false} and {@link #isEnabled()} will be {@code true}.
+     * {@code false} and {@link #isImmutable()} will be {@code true}.
      */
     public boolean isDeclaredInManifest() {
         return mIsDeclaredInManifest;
diff --git a/core/core/src/main/java/androidx/core/os/BuildCompat.java b/core/core/src/main/java/androidx/core/os/BuildCompat.java
index e50f09e..0af43bd 100644
--- a/core/core/src/main/java/androidx/core/os/BuildCompat.java
+++ b/core/core/src/main/java/androidx/core/os/BuildCompat.java
@@ -21,6 +21,7 @@
 import android.annotation.SuppressLint;
 import android.os.Build;
 import android.os.Build.VERSION;
+import android.os.ext.SdkExtensions;
 
 import androidx.annotation.ChecksSdkIntAtLeast;
 import androidx.annotation.NonNull;
@@ -281,15 +282,27 @@
     @SuppressLint("CompileTimeConstant")
     public static final int T_EXTENSION_INT = VERSION.SDK_INT >= 30 ? Extensions30Impl.TIRAMISU : 0;
 
-    @SuppressLint("ClassVerificationFailure") // Remove when SDK including b/206996004 is imported
+    /**
+     * The value of {@code SdkExtensions.getExtensionVersion(AD_SERVICES)}. This is a convenience
+     * constant which provides the extension version in a similar style to
+     * {@code Build.VERSION.SDK_INT}.
+     * <p>
+     * Compared to calling {@code getExtensionVersion} directly, using this constant has the
+     * benefit of not having to verify the {@code getExtensionVersion} method is available.
+     *
+     * @return the version of the AdServices extension, if it exists. 0 otherwise.
+     */
+    @ChecksSdkIntAtLeast(extension = SdkExtensions.AD_SERVICES)
+    @SuppressLint("CompileTimeConstant")
+    public static final int AD_SERVICES_EXTENSION_INT =
+            VERSION.SDK_INT >= 30 ? Extensions30Impl.AD_SERVICES : 0;
+
     @RequiresApi(30)
     private static final class Extensions30Impl {
-        @SuppressLint("NewApi") // Remove when SDK including b/206996004 is imported
         static final int R = getExtensionVersion(Build.VERSION_CODES.R);
-        @SuppressLint("NewApi") // Remove when SDK including b/206996004 is imported
         static final int S = getExtensionVersion(Build.VERSION_CODES.S);
-        @SuppressLint("NewApi") // Remove when SDK including b/206996004 is imported
         static final int TIRAMISU = getExtensionVersion(Build.VERSION_CODES.TIRAMISU);
+        static final int AD_SERVICES = getExtensionVersion(SdkExtensions.AD_SERVICES);
     }
 
 }
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/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestUtils.kt b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestUtils.kt
index 996e2ad..63dd65a 100644
--- a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestUtils.kt
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestUtils.kt
@@ -59,23 +59,30 @@
          * contain
          * @param subset the subset json that should have equal to or less keys and subvalues than
          * the superset
+         * @param requiredKeys the fixed required keys for this test
          * @return a boolean indicating if the subset was truly a subset of the superset it was
          * tested with
          */
-        fun isSubsetJson(superset: JSONObject, subset: JSONObject): Boolean {
-            val keys = subset.keys()
+        fun isSubsetJson(superset: JSONObject, subset: JSONObject, requiredKeys: JSONObject):
+            Boolean {
+            val keys = requiredKeys.keys()
             for (key in keys) {
-                if (!superset.has(key)) {
+                if (!superset.has(key) || !subset.has(key)) {
                     return false
                 }
+
+                val requiredValues = requiredKeys.get(key)
                 val values = subset.get(key)
                 val superValues = superset.get(key)
 
-                if (values::class.java != superValues::class.java) {
+                if ((values::class.java != superValues::class.java || values::class.java !=
+                    requiredValues::class.java) && requiredValues !is Boolean
+                ) {
                     return false
                 }
-                if (values is JSONObject) {
-                    if (!isSubsetJson(superValues as JSONObject, values)) {
+                if (requiredValues is JSONObject) {
+                    if (!isSubsetJson(superValues as JSONObject, values as JSONObject,
+                            requiredValues)) {
                         return false
                     }
                 } else if (values is JSONArray) {
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/CreatePublicKeyCredentialControllerTestUtils.kt b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/CreatePublicKeyCredentialControllerTestUtils.kt
index 2911c18..027a3ad 100644
--- a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/CreatePublicKeyCredentialControllerTestUtils.kt
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/CreatePublicKeyCredentialControllerTestUtils.kt
@@ -17,6 +17,7 @@
 package androidx.credentials.playservices.createkeycredential
 
 import androidx.credentials.playservices.TestUtils
+import com.google.android.gms.fido.common.Transport
 import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialCreationOptions
 import org.json.JSONArray
 import org.json.JSONException
@@ -42,6 +43,12 @@
              "\"excludeCredentials\": [{\"id\":\"AA\",\"type\":\"public-key\"}]," +
              "\"attestation\": \"none\"}")
 
+        // This signature indicates what the json above, after parsing, must contain
+        const val OPTIONAL_FIELD_MISSING_OPTIONAL_SUBFIELD_SIGNATURE = "{\"rp\":{\"name\":true," +
+            "\"id\":true},\"user\":{\"name\":true,\"id\":true,\"displayName\":true," +
+            "\"icon\":true}, \"challenge\":true,\"pubKeyCredParams\":true," +
+            "\"excludeCredentials\":true," + "\"attestation\":true}"
+
         // optional, but if it exists, required key 'type' exists but is empty in the JSONObject
         // that composes up the JSONArray found at key 'excludeCredentials'
          const val OPTIONAL_FIELD_WITH_EMPTY_REQUIRED_SUBFIELD = ("{\"rp\": {\"name\": " +
@@ -57,7 +64,7 @@
              "-39}, {\"type\": \"public-key\", \"alg\": -257}, {\"type\": \"public-key\", " +
              "\"alg\": -258}, {\"type\": \"public-key\", \"alg\": -259}]," +
              "\"excludeCredentials\": [{\"type\":\"\",\"id\":\"public-key\"," +
-             "\"transports\"=[\"usb\"]}]," +
+             "\"transports\"=[\"ble\"]}]," +
              "\"attestation\": \"none\"}")
 
         // optional, but if it exists, required key 'type' is missing in the JSONObject that
@@ -74,7 +81,7 @@
              "{\"type\": \"public-key\", \"alg\": -38}, {\"type\": \"public-key\", \"alg\": " +
              "-39}, {\"type\": \"public-key\", \"alg\": -257}, {\"type\": \"public-key\", " +
              "\"alg\": -258}, {\"type\": \"public-key\", \"alg\": -259}]," +
-             "\"excludeCredentials\": [{\"id\":\"AA\",\"transports\"=[\"usb\"]}]," +
+             "\"excludeCredentials\": [{\"id\":\"AA\",\"transports\"=[\"ble\"]}]," +
              "\"attestation\": \"none\"}")
 
          // user id is non existent
@@ -119,12 +126,22 @@
             "{\"type\": \"public-key\", \"alg\": -38}, {\"type\": \"public-key\", \"alg\": " +
             "-39}, {\"type\": \"public-key\", \"alg\": -257}, {\"type\": \"public-key\", " +
             "\"alg\": -258}, {\"type\": \"public-key\", \"alg\": -259}], \"timeout\": 60000, " +
-            "\"excludeCredentials\": [{\"id\":\"AA\",\"type\":\"A\",\"transports\"=[\"A\"]}], " +
-            "\"authenticatorSelection\": " +
+            "\"excludeCredentials\": [{\"id\":\"AA\",\"type\":\"public-key\"," +
+            "\"transports\"=[\"ble\"]}], " + "\"authenticatorSelection\": " +
             "{\"authenticatorAttachment\": \"platform\", \"residentKey\": \"required\", " +
             "\"requireResidentKey\": true, \"userVerification\": \"preferred\"}, " +
             "\"attestation\": \"none\"}")
 
+        // This signature indicates what [MAIN_CREATE_JSON_ALL_REQUIRED_AND_OPTIONAL], after
+        // parsing, must contain. It is a 'brace' to ensure required values are tested.
+        const val ALL_REQUIRED_AND_OPTIONAL_SIGNATURE = "{\"rp\":{\"name\":true,\"id\":true}," +
+            "\"user\":{\"id\":true,\"name\":true,\"displayName\":true,\"icon\":true}," +
+            "\"challenge\":true,\"pubKeyCredParams\":true,\"timeout\":true," +
+            "\"excludeCredentials\":true,\"authenticatorSelection\":{" +
+            "\"authenticatorAttachment\":true,\"residentKey\":true,\"requireResidentKey\":true," +
+            "\"userVerification\":true},\"attestation\":true}"
+
+        // Contains all required keys for the JSON, but not any of the other cases
         const val MAIN_CREATE_JSON_ALL_REQUIRED_FIELDS_PRESENT = ("{\"rp\": {\"name\": " +
             "\"Address " + "Book\", " + "\"id\": " + "\"addressbook-c7876.uc.r.appspot.com\"}, " +
             "\"user\": {\"id\": " +
@@ -139,6 +156,13 @@
             "\"alg\": -258}, {\"type\": \"public-key\", \"alg\": -259}]," +
             "\"excludeCredentials\": []," + "\"attestation\": \"none\"}")
 
+        // This signature indicates what [MAIN_CREATE_JSON_ALL_REQUIRED_FIELDS_PRESENT], after
+        // parsing, must contain. It is a 'brace' to ensure required values are tested.
+        const val ALL_REQUIRED_FIELDS_SIGNATURE = "{\"rp\":{\"name\":true,\"id\":true}," +
+            "\"user\":{\"id\":true,\"name\":true,\"displayName\":true,\"icon\":true}," +
+            "\"challenge\":true,\"pubKeyCredParams\":true,\"excludeCredentials\":true," +
+            "\"attestation\":true}"
+
         /**
          * Generates a JSON for the **create request** flow that is maximally filled given the inputs,
          * so it can always be a representative json to any input to compare against for this
@@ -183,8 +207,10 @@
                     val requireResidentKey = selectionCriteria.requireResidentKey!!
                     authSelect.put("requireResidentKey", requireResidentKey)
                 }
+                authSelect.put("userVerification", "preferred")
+                // TODO("Since fido impl accepts this input, but does not return it, adding)
+                // TODO(it directly for test comparison. When available, pull from impl object.")
                 json.put("authenticatorSelection", authSelect)
-                // TODO("Missing userVerification in fido impl")
             }
             val attestation = options.attestationConveyancePreferenceAsString
             if (attestation != null) {
@@ -203,13 +229,24 @@
                     val descriptorI = JSONObject()
                     descriptorI.put("id", TestUtils.b64Encode(descriptorJSON.id))
                     descriptorI.put("type", descriptorJSON.type)
-                    descriptorI.put("transports", descriptorJSON.transports)
+                    descriptorJSON.transports?.let {
+                        descriptorI.put("transports",
+                            createJSONArrayFromTransports(descriptorJSON.transports!!))
+                    }
                     descriptor.put(descriptorI)
                 }
             }
             json.put("excludeCredentials", descriptor)
         }
 
+        private fun createJSONArrayFromTransports(transports: List<Transport>): JSONArray {
+            val jsonArr = JSONArray()
+            for (transport in transports) {
+                jsonArr.put(transport.toString())
+            }
+            return jsonArr
+        }
+
         private fun configureChallengeParamsAndTimeout(
             options: PublicKeyCredentialCreationOptions,
             json: JSONObject
@@ -226,8 +263,8 @@
             }
             json.put("pubKeyCredParams", parameters)
             if (options.timeoutSeconds != null) {
-                val optionalTimeout = options.timeoutSeconds!!
-                json.put("timeout", optionalTimeout)
+                val optionalTimeout: Int = options.timeoutSeconds!!.toInt()
+                json.put("timeout", optionalTimeout * 1000)
             }
         }
 
@@ -260,5 +297,34 @@
             }
             json.put("user", userJson)
         }
+
+        /**
+         * This converts all JSON Leaves to a 'true' boolean value. Note this is lax on
+         * lists/JSONArrays. In short, it creates a 'signature' for a JSONObject. It can be used
+         * to generate constants which can be used to test with.
+         *
+         * For example, given this json object
+         * ```
+         * {"rp":{"name":true,"id":true},"user":{
+         * "id":true,"name":true,"displayName":true,"icon":true
+         * },"challenge":true,"pubKeyCredParams":true,"excludeCredentials":true,"attestation":true}
+         * ```
+         * notice that all the 'leaves' have become true outside of the array exception. This can
+         * be used to make fixed required keys.
+         *
+         * @param json the json object with which to modify in place
+         */
+        @JvmStatic
+        fun convertJsonLeavesToBooleanSignature(json: JSONObject) {
+            val keys = json.keys()
+            for (key in keys) {
+                val value = json.get(key)
+                if (value is JSONObject) {
+                    convertJsonLeavesToBooleanSignature(value)
+                } else {
+                    json.put(key, true)
+                }
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/CredentialProviderCreatePublicKeyCredentialControllerJavaTest.java b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/CredentialProviderCreatePublicKeyCredentialControllerJavaTest.java
index ebf4f56..3f8e78e 100644
--- a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/CredentialProviderCreatePublicKeyCredentialControllerJavaTest.java
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/CredentialProviderCreatePublicKeyCredentialControllerJavaTest.java
@@ -16,11 +16,14 @@
 
 package androidx.credentials.playservices.createpublickeycredential;
 
+import static androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.ALL_REQUIRED_AND_OPTIONAL_SIGNATURE;
+import static androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.ALL_REQUIRED_FIELDS_SIGNATURE;
 import static androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.MAIN_CREATE_JSON_ALL_REQUIRED_AND_OPTIONAL_FIELDS_PRESENT;
 import static androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.MAIN_CREATE_JSON_ALL_REQUIRED_FIELDS_PRESENT;
 import static androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.MAIN_CREATE_JSON_MISSING_REQUIRED_FIELD;
 import static androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.MAIN_CREATE_JSON_REQUIRED_FIELD_EMPTY;
 import static androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.OPTIONAL_FIELD_MISSING_OPTIONAL_SUBFIELD;
+import static androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.OPTIONAL_FIELD_MISSING_OPTIONAL_SUBFIELD_SIGNATURE;
 import static androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.OPTIONAL_FIELD_MISSING_REQUIRED_SUBFIELD;
 import static androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.OPTIONAL_FIELD_WITH_EMPTY_REQUIRED_SUBFIELD;
 import static androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.createJsonObjectFromPublicKeyCredentialCreationOptions;
@@ -64,8 +67,10 @@
                                                 MAIN_CREATE_JSON_ALL_REQUIRED_FIELDS_PRESENT));
                 JSONObject actualJson = createJsonObjectFromPublicKeyCredentialCreationOptions(
                                         actualResponse);
+                JSONObject requiredKeys = new JSONObject(ALL_REQUIRED_FIELDS_SIGNATURE);
 
-                assertThat(TestUtils.Companion.isSubsetJson(expectedJson, actualJson)).isTrue();
+                assertThat(TestUtils.Companion.isSubsetJson(expectedJson, actualJson,
+                        requiredKeys)).isTrue();
                 // TODO("Add remaining tests in detail after discussing ideal form")
             } catch (JSONException e) {
                 throw new RuntimeException(e);
@@ -84,14 +89,15 @@
 
                 PublicKeyCredentialCreationOptions actualResponse =
                         CredentialProviderCreatePublicKeyCredentialController.getInstance(activity)
-                                .convertRequestToPlayServices(
-                                        new CreatePublicKeyCredentialRequest(
-                                                MAIN_CREATE_JSON_ALL_REQUIRED_FIELDS_PRESENT));
+                                .convertRequestToPlayServices(new CreatePublicKeyCredentialRequest(
+                                        MAIN_CREATE_JSON_ALL_REQUIRED_AND_OPTIONAL_FIELDS_PRESENT));
                 JSONObject actualJson =
                         createJsonObjectFromPublicKeyCredentialCreationOptions(
                                         actualResponse);
+                JSONObject requiredKeys = new JSONObject(ALL_REQUIRED_AND_OPTIONAL_SIGNATURE);
 
-                assertThat(TestUtils.Companion.isSubsetJson(expectedJson, actualJson)).isTrue();
+                assertThat(TestUtils.Companion.isSubsetJson(expectedJson, actualJson,
+                        requiredKeys)).isTrue();
                 // TODO("Add remaining tests in detail after discussing ideal form")
             } catch (JSONException e) {
                 throw new RuntimeException(e);
@@ -177,8 +183,11 @@
                                                 OPTIONAL_FIELD_MISSING_OPTIONAL_SUBFIELD));
                 JSONObject actualJson = createJsonObjectFromPublicKeyCredentialCreationOptions(
                                         actualResponse);
+                JSONObject requiredKeys = new
+                        JSONObject(OPTIONAL_FIELD_MISSING_OPTIONAL_SUBFIELD_SIGNATURE);
 
-                assertThat(TestUtils.Companion.isSubsetJson(expectedJson, actualJson)).isTrue();
+                assertThat(TestUtils.Companion.isSubsetJson(expectedJson, actualJson,
+                        requiredKeys)).isTrue();
                 // TODO("Add remaining tests in detail after discussing ideal form")
             } catch (JSONException e) {
                 throw new RuntimeException(e);
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/CredentialProviderCreatePublicKeyCredentialControllerTest.kt b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/CredentialProviderCreatePublicKeyCredentialControllerTest.kt
index 67da7e8..c33a968 100644
--- a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/CredentialProviderCreatePublicKeyCredentialControllerTest.kt
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/CredentialProviderCreatePublicKeyCredentialControllerTest.kt
@@ -20,11 +20,14 @@
 import androidx.credentials.playservices.TestCredentialsActivity
 import androidx.credentials.playservices.TestUtils.Companion.isSubsetJson
 import androidx.credentials.playservices.controllers.CreatePublicKeyCredential.CredentialProviderCreatePublicKeyCredentialController.Companion.getInstance
+import androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.Companion.ALL_REQUIRED_AND_OPTIONAL_SIGNATURE
+import androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.Companion.ALL_REQUIRED_FIELDS_SIGNATURE
 import androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.Companion.MAIN_CREATE_JSON_ALL_REQUIRED_AND_OPTIONAL_FIELDS_PRESENT
 import androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.Companion.MAIN_CREATE_JSON_ALL_REQUIRED_FIELDS_PRESENT
 import androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.Companion.MAIN_CREATE_JSON_MISSING_REQUIRED_FIELD
 import androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.Companion.MAIN_CREATE_JSON_REQUIRED_FIELD_EMPTY
 import androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.Companion.OPTIONAL_FIELD_MISSING_OPTIONAL_SUBFIELD
+import androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.Companion.OPTIONAL_FIELD_MISSING_OPTIONAL_SUBFIELD_SIGNATURE
 import androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.Companion.OPTIONAL_FIELD_MISSING_REQUIRED_SUBFIELD
 import androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.Companion.OPTIONAL_FIELD_WITH_EMPTY_REQUIRED_SUBFIELD
 import androidx.credentials.playservices.createkeycredential.CreatePublicKeyCredentialControllerTestUtils.Companion.createJsonObjectFromPublicKeyCredentialCreationOptions
@@ -49,18 +52,17 @@
         )
         activityScenario.onActivity { activity: TestCredentialsActivity? ->
             try {
-                val expectedJson =
-                    JSONObject(MAIN_CREATE_JSON_ALL_REQUIRED_FIELDS_PRESENT)
+                val expectedJson = JSONObject(MAIN_CREATE_JSON_ALL_REQUIRED_FIELDS_PRESENT)
 
                 val actualResponse = getInstance(activity!!).convertRequestToPlayServices(
                             CreatePublicKeyCredentialRequest(
                                 MAIN_CREATE_JSON_ALL_REQUIRED_FIELDS_PRESENT))
                 val actualJson =
                     createJsonObjectFromPublicKeyCredentialCreationOptions(actualResponse)
+                val requiredKeys =
+                    JSONObject(ALL_REQUIRED_FIELDS_SIGNATURE)
 
-                assertThat(
-                    isSubsetJson(expectedJson, actualJson)
-                ).isTrue()
+                assertThat(isSubsetJson(expectedJson, actualJson, requiredKeys)).isTrue()
                 // TODO("Add remaining tests in detail after discussing ideal form")
             } catch (e: JSONException) {
                 throw RuntimeException(e)
@@ -76,17 +78,17 @@
         activityScenario.onActivity { activity: TestCredentialsActivity? ->
             try {
                 val expectedJson = JSONObject(
-                    MAIN_CREATE_JSON_ALL_REQUIRED_AND_OPTIONAL_FIELDS_PRESENT
-                )
+                    MAIN_CREATE_JSON_ALL_REQUIRED_AND_OPTIONAL_FIELDS_PRESENT)
 
                 val actualResponse = getInstance(activity!!)
                         .convertRequestToPlayServices(CreatePublicKeyCredentialRequest(
-                                MAIN_CREATE_JSON_ALL_REQUIRED_FIELDS_PRESENT))
+                            MAIN_CREATE_JSON_ALL_REQUIRED_AND_OPTIONAL_FIELDS_PRESENT))
                 val actualJson =
                     createJsonObjectFromPublicKeyCredentialCreationOptions(actualResponse)
+                val requiredKeys =
+                    JSONObject(ALL_REQUIRED_AND_OPTIONAL_SIGNATURE)
 
-                assertThat(
-                    isSubsetJson(expectedJson, actualJson)).isTrue()
+                assertThat(isSubsetJson(expectedJson, actualJson, requiredKeys)).isTrue()
                 // TODO("Add remaining tests in detail after discussing ideal form")
             } catch (e: JSONException) {
                 throw java.lang.RuntimeException(e)
@@ -167,9 +169,7 @@
         )
         activityScenario.onActivity { activity: TestCredentialsActivity? ->
             try {
-                val expectedJson = JSONObject(
-                    OPTIONAL_FIELD_MISSING_OPTIONAL_SUBFIELD
-                )
+                val expectedJson = JSONObject(OPTIONAL_FIELD_MISSING_OPTIONAL_SUBFIELD)
 
                 val actualResponse =
                     getInstance(activity!!)
@@ -178,8 +178,10 @@
                                 OPTIONAL_FIELD_MISSING_OPTIONAL_SUBFIELD))
                 val actualJson = createJsonObjectFromPublicKeyCredentialCreationOptions(
                         actualResponse)
+                val requiredKeys =
+                    JSONObject(OPTIONAL_FIELD_MISSING_OPTIONAL_SUBFIELD_SIGNATURE)
 
-                assertThat(isSubsetJson(expectedJson, actualJson)).isTrue()
+                assertThat(isSubsetJson(expectedJson, actualJson, requiredKeys)).isTrue()
                 // TODO("Add remaining tests in detail after discussing ideal form")
             } catch (e: JSONException) {
                 throw java.lang.RuntimeException(e)
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/BeginSignInControllerUtility.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/BeginSignInControllerUtility.kt
index 8fa9bfa..3f915a5 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/BeginSignInControllerUtility.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/BeginSignInControllerUtility.kt
@@ -52,7 +52,7 @@
             BeginSignInRequest {
             var isPublicKeyCredReqFound = false
             val requestBuilder = BeginSignInRequest.Builder()
-            for (option in request.getCredentialOptions) {
+            for (option in request.credentialOptions) {
                 if (option is GetPasswordOption) {
                     requestBuilder.setPasswordRequestOptions(
                         BeginSignInRequest.PasswordRequestOptions.Builder()
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt
index 9ab02c0..b06e360e 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt
@@ -23,6 +23,7 @@
 import androidx.credentials.exceptions.domerrors.AbortError
 import androidx.credentials.exceptions.domerrors.ConstraintError
 import androidx.credentials.exceptions.domerrors.DataError
+import androidx.credentials.exceptions.domerrors.EncodingError
 import androidx.credentials.exceptions.domerrors.InvalidStateError
 import androidx.credentials.exceptions.domerrors.NetworkError
 import androidx.credentials.exceptions.domerrors.NotAllowedError
@@ -357,7 +358,13 @@
                             "transports"
                         )
                         for (j in 0 until descriptorTransports.length()) {
-                            transports.add(Transport.fromString(descriptorTransports.getString(j)))
+                            try {
+                                transports.add(Transport.fromString(
+                                    descriptorTransports.getString(j)))
+                            } catch (e: Transport.UnsupportedTransportException) {
+                                throw CreatePublicKeyCredentialDomException(EncodingError(),
+                                    e.message)
+                            }
                         }
                     }
                     excludeCredentialsList.add(
diff --git a/credentials/credentials/api/current.txt b/credentials/credentials/api/current.txt
index 58f48fd..9bba4ab 100644
--- a/credentials/credentials/api/current.txt
+++ b/credentials/credentials/api/current.txt
@@ -8,18 +8,27 @@
   public abstract class CreateCredentialRequest {
   }
 
+  public static final class CreateCredentialRequest.DisplayInfo {
+    ctor public CreateCredentialRequest.DisplayInfo(String userId, optional String? userDisplayName);
+    ctor public CreateCredentialRequest.DisplayInfo(String userId);
+    method public String? getUserDisplayName();
+    method public String getUserId();
+    property public final String? userDisplayName;
+    property public final String userId;
+  }
+
   public abstract class CreateCredentialResponse {
   }
 
   public class CreateCustomCredentialRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
+    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
     method public final android.os.Bundle getCandidateQueryData();
     method public final android.os.Bundle getCredentialData();
-    method public final boolean getRequireSystemProvider();
     method public final String getType();
+    method public final boolean isSystemProviderRequired();
     property public final android.os.Bundle candidateQueryData;
     property public final android.os.Bundle credentialData;
-    property public final boolean requireSystemProvider;
+    property public final boolean isSystemProviderRequired;
     property public final String type;
   }
 
@@ -44,23 +53,23 @@
   }
 
   public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional boolean allowHybrid);
+    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional boolean preferImmediatelyAvailableCredentials);
     ctor public CreatePublicKeyCredentialRequest(String requestJson);
-    method public boolean getAllowHybrid();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String requestJson;
   }
 
   public final class CreatePublicKeyCredentialRequestPrivileged extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean allowHybrid);
+    ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
     ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash);
-    method public boolean getAllowHybrid();
     method public String getClientDataHash();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRelyingParty();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
     property public final String clientDataHash;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String relyingParty;
     property public final String requestJson;
   }
@@ -68,8 +77,8 @@
   public static final class CreatePublicKeyCredentialRequestPrivileged.Builder {
     ctor public CreatePublicKeyCredentialRequestPrivileged.Builder(String requestJson, String relyingParty, String clientDataHash);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged build();
-    method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setAllowHybrid(boolean allowHybrid);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setClientDataHash(String clientDataHash);
+    method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setRelyingParty(String relyingParty);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setRequestJson(String requestJson);
   }
@@ -87,10 +96,10 @@
     method public suspend Object? clearCredentialState(androidx.credentials.ClearCredentialStateRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public void clearCredentialStateAsync(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void,androidx.credentials.exceptions.ClearCredentialException> callback);
     method public static androidx.credentials.CredentialManager create(android.content.Context context);
-    method public suspend Object? executeCreateCredential(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, kotlin.coroutines.Continuation<? super androidx.credentials.CreateCredentialResponse>);
-    method public void executeCreateCredentialAsync(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
-    method public suspend Object? executeGetCredential(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
-    method public void executeGetCredentialAsync(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
+    method public suspend Object? createCredential(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, kotlin.coroutines.Continuation<? super androidx.credentials.CreateCredentialResponse>);
+    method public void createCredentialAsync(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
+    method public suspend Object? getCredential(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
+    method public void getCredentialAsync(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
     field public static final androidx.credentials.CredentialManager.Companion Companion;
   }
 
@@ -103,6 +112,9 @@
     method public void onResult(R? result);
   }
 
+  public abstract class CredentialOption {
+  }
+
   public interface CredentialProvider {
     method public boolean isAvailableOnDevice();
     method public void onClearCredential(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void,androidx.credentials.exceptions.ClearCredentialException> callback);
@@ -118,24 +130,21 @@
     property public final String type;
   }
 
-  public abstract class GetCredentialOption {
-  }
-
   public final class GetCredentialRequest {
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.GetCredentialOption> getCredentialOptions, optional boolean isAutoSelectAllowed);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.GetCredentialOption> getCredentialOptions);
-    method public java.util.List<androidx.credentials.GetCredentialOption> getGetCredentialOptions();
+    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional boolean isAutoSelectAllowed);
+    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
+    method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
     method public boolean isAutoSelectAllowed();
-    property public final java.util.List<androidx.credentials.GetCredentialOption> getCredentialOptions;
+    property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
     property public final boolean isAutoSelectAllowed;
   }
 
   public static final class GetCredentialRequest.Builder {
     ctor public GetCredentialRequest.Builder();
-    method public androidx.credentials.GetCredentialRequest.Builder addGetCredentialOption(androidx.credentials.GetCredentialOption getCredentialOption);
+    method public androidx.credentials.GetCredentialRequest.Builder addCredentialOption(androidx.credentials.CredentialOption credentialOption);
     method public androidx.credentials.GetCredentialRequest build();
     method public androidx.credentials.GetCredentialRequest.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.GetCredentialRequest.Builder setGetCredentialOptions(java.util.List<? extends androidx.credentials.GetCredentialOption> getCredentialOptions);
+    method public androidx.credentials.GetCredentialRequest.Builder setCredentialOptions(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
   }
 
   public final class GetCredentialResponse {
@@ -144,40 +153,40 @@
     property public final androidx.credentials.Credential credential;
   }
 
-  public class GetCustomCredentialOption extends androidx.credentials.GetCredentialOption {
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
+  public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
+    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
     method public final android.os.Bundle getCandidateQueryData();
     method public final android.os.Bundle getRequestData();
-    method public final boolean getRequireSystemProvider();
     method public final String getType();
+    method public final boolean isSystemProviderRequired();
     property public final android.os.Bundle candidateQueryData;
+    property public final boolean isSystemProviderRequired;
     property public final android.os.Bundle requestData;
-    property public final boolean requireSystemProvider;
     property public final String type;
   }
 
-  public final class GetPasswordOption extends androidx.credentials.GetCredentialOption {
+  public final class GetPasswordOption extends androidx.credentials.CredentialOption {
     ctor public GetPasswordOption();
   }
 
-  public final class GetPublicKeyCredentialOption extends androidx.credentials.GetCredentialOption {
-    ctor public GetPublicKeyCredentialOption(String requestJson, optional boolean allowHybrid);
+  public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
+    ctor public GetPublicKeyCredentialOption(String requestJson, optional boolean preferImmediatelyAvailableCredentials);
     ctor public GetPublicKeyCredentialOption(String requestJson);
-    method public boolean getAllowHybrid();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String requestJson;
   }
 
-  public final class GetPublicKeyCredentialOptionPrivileged extends androidx.credentials.GetCredentialOption {
-    ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean allowHybrid);
+  public final class GetPublicKeyCredentialOptionPrivileged extends androidx.credentials.CredentialOption {
+    ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
     ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash);
-    method public boolean getAllowHybrid();
     method public String getClientDataHash();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRelyingParty();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
     property public final String clientDataHash;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String relyingParty;
     property public final String requestJson;
   }
@@ -185,8 +194,8 @@
   public static final class GetPublicKeyCredentialOptionPrivileged.Builder {
     ctor public GetPublicKeyCredentialOptionPrivileged.Builder(String requestJson, String relyingParty, String clientDataHash);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged build();
-    method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setAllowHybrid(boolean allowHybrid);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setClientDataHash(String clientDataHash);
+    method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setRelyingParty(String relyingParty);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setRequestJson(String requestJson);
   }
diff --git a/credentials/credentials/api/public_plus_experimental_current.txt b/credentials/credentials/api/public_plus_experimental_current.txt
index 58f48fd..9bba4ab 100644
--- a/credentials/credentials/api/public_plus_experimental_current.txt
+++ b/credentials/credentials/api/public_plus_experimental_current.txt
@@ -8,18 +8,27 @@
   public abstract class CreateCredentialRequest {
   }
 
+  public static final class CreateCredentialRequest.DisplayInfo {
+    ctor public CreateCredentialRequest.DisplayInfo(String userId, optional String? userDisplayName);
+    ctor public CreateCredentialRequest.DisplayInfo(String userId);
+    method public String? getUserDisplayName();
+    method public String getUserId();
+    property public final String? userDisplayName;
+    property public final String userId;
+  }
+
   public abstract class CreateCredentialResponse {
   }
 
   public class CreateCustomCredentialRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
+    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
     method public final android.os.Bundle getCandidateQueryData();
     method public final android.os.Bundle getCredentialData();
-    method public final boolean getRequireSystemProvider();
     method public final String getType();
+    method public final boolean isSystemProviderRequired();
     property public final android.os.Bundle candidateQueryData;
     property public final android.os.Bundle credentialData;
-    property public final boolean requireSystemProvider;
+    property public final boolean isSystemProviderRequired;
     property public final String type;
   }
 
@@ -44,23 +53,23 @@
   }
 
   public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional boolean allowHybrid);
+    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional boolean preferImmediatelyAvailableCredentials);
     ctor public CreatePublicKeyCredentialRequest(String requestJson);
-    method public boolean getAllowHybrid();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String requestJson;
   }
 
   public final class CreatePublicKeyCredentialRequestPrivileged extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean allowHybrid);
+    ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
     ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash);
-    method public boolean getAllowHybrid();
     method public String getClientDataHash();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRelyingParty();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
     property public final String clientDataHash;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String relyingParty;
     property public final String requestJson;
   }
@@ -68,8 +77,8 @@
   public static final class CreatePublicKeyCredentialRequestPrivileged.Builder {
     ctor public CreatePublicKeyCredentialRequestPrivileged.Builder(String requestJson, String relyingParty, String clientDataHash);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged build();
-    method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setAllowHybrid(boolean allowHybrid);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setClientDataHash(String clientDataHash);
+    method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setRelyingParty(String relyingParty);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setRequestJson(String requestJson);
   }
@@ -87,10 +96,10 @@
     method public suspend Object? clearCredentialState(androidx.credentials.ClearCredentialStateRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public void clearCredentialStateAsync(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void,androidx.credentials.exceptions.ClearCredentialException> callback);
     method public static androidx.credentials.CredentialManager create(android.content.Context context);
-    method public suspend Object? executeCreateCredential(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, kotlin.coroutines.Continuation<? super androidx.credentials.CreateCredentialResponse>);
-    method public void executeCreateCredentialAsync(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
-    method public suspend Object? executeGetCredential(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
-    method public void executeGetCredentialAsync(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
+    method public suspend Object? createCredential(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, kotlin.coroutines.Continuation<? super androidx.credentials.CreateCredentialResponse>);
+    method public void createCredentialAsync(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
+    method public suspend Object? getCredential(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
+    method public void getCredentialAsync(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
     field public static final androidx.credentials.CredentialManager.Companion Companion;
   }
 
@@ -103,6 +112,9 @@
     method public void onResult(R? result);
   }
 
+  public abstract class CredentialOption {
+  }
+
   public interface CredentialProvider {
     method public boolean isAvailableOnDevice();
     method public void onClearCredential(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void,androidx.credentials.exceptions.ClearCredentialException> callback);
@@ -118,24 +130,21 @@
     property public final String type;
   }
 
-  public abstract class GetCredentialOption {
-  }
-
   public final class GetCredentialRequest {
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.GetCredentialOption> getCredentialOptions, optional boolean isAutoSelectAllowed);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.GetCredentialOption> getCredentialOptions);
-    method public java.util.List<androidx.credentials.GetCredentialOption> getGetCredentialOptions();
+    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional boolean isAutoSelectAllowed);
+    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
+    method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
     method public boolean isAutoSelectAllowed();
-    property public final java.util.List<androidx.credentials.GetCredentialOption> getCredentialOptions;
+    property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
     property public final boolean isAutoSelectAllowed;
   }
 
   public static final class GetCredentialRequest.Builder {
     ctor public GetCredentialRequest.Builder();
-    method public androidx.credentials.GetCredentialRequest.Builder addGetCredentialOption(androidx.credentials.GetCredentialOption getCredentialOption);
+    method public androidx.credentials.GetCredentialRequest.Builder addCredentialOption(androidx.credentials.CredentialOption credentialOption);
     method public androidx.credentials.GetCredentialRequest build();
     method public androidx.credentials.GetCredentialRequest.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.GetCredentialRequest.Builder setGetCredentialOptions(java.util.List<? extends androidx.credentials.GetCredentialOption> getCredentialOptions);
+    method public androidx.credentials.GetCredentialRequest.Builder setCredentialOptions(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
   }
 
   public final class GetCredentialResponse {
@@ -144,40 +153,40 @@
     property public final androidx.credentials.Credential credential;
   }
 
-  public class GetCustomCredentialOption extends androidx.credentials.GetCredentialOption {
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
+  public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
+    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
     method public final android.os.Bundle getCandidateQueryData();
     method public final android.os.Bundle getRequestData();
-    method public final boolean getRequireSystemProvider();
     method public final String getType();
+    method public final boolean isSystemProviderRequired();
     property public final android.os.Bundle candidateQueryData;
+    property public final boolean isSystemProviderRequired;
     property public final android.os.Bundle requestData;
-    property public final boolean requireSystemProvider;
     property public final String type;
   }
 
-  public final class GetPasswordOption extends androidx.credentials.GetCredentialOption {
+  public final class GetPasswordOption extends androidx.credentials.CredentialOption {
     ctor public GetPasswordOption();
   }
 
-  public final class GetPublicKeyCredentialOption extends androidx.credentials.GetCredentialOption {
-    ctor public GetPublicKeyCredentialOption(String requestJson, optional boolean allowHybrid);
+  public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
+    ctor public GetPublicKeyCredentialOption(String requestJson, optional boolean preferImmediatelyAvailableCredentials);
     ctor public GetPublicKeyCredentialOption(String requestJson);
-    method public boolean getAllowHybrid();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String requestJson;
   }
 
-  public final class GetPublicKeyCredentialOptionPrivileged extends androidx.credentials.GetCredentialOption {
-    ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean allowHybrid);
+  public final class GetPublicKeyCredentialOptionPrivileged extends androidx.credentials.CredentialOption {
+    ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
     ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash);
-    method public boolean getAllowHybrid();
     method public String getClientDataHash();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRelyingParty();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
     property public final String clientDataHash;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String relyingParty;
     property public final String requestJson;
   }
@@ -185,8 +194,8 @@
   public static final class GetPublicKeyCredentialOptionPrivileged.Builder {
     ctor public GetPublicKeyCredentialOptionPrivileged.Builder(String requestJson, String relyingParty, String clientDataHash);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged build();
-    method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setAllowHybrid(boolean allowHybrid);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setClientDataHash(String clientDataHash);
+    method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setRelyingParty(String relyingParty);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setRequestJson(String requestJson);
   }
diff --git a/credentials/credentials/api/restricted_current.txt b/credentials/credentials/api/restricted_current.txt
index 58f48fd..9bba4ab 100644
--- a/credentials/credentials/api/restricted_current.txt
+++ b/credentials/credentials/api/restricted_current.txt
@@ -8,18 +8,27 @@
   public abstract class CreateCredentialRequest {
   }
 
+  public static final class CreateCredentialRequest.DisplayInfo {
+    ctor public CreateCredentialRequest.DisplayInfo(String userId, optional String? userDisplayName);
+    ctor public CreateCredentialRequest.DisplayInfo(String userId);
+    method public String? getUserDisplayName();
+    method public String getUserId();
+    property public final String? userDisplayName;
+    property public final String userId;
+  }
+
   public abstract class CreateCredentialResponse {
   }
 
   public class CreateCustomCredentialRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
+    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
     method public final android.os.Bundle getCandidateQueryData();
     method public final android.os.Bundle getCredentialData();
-    method public final boolean getRequireSystemProvider();
     method public final String getType();
+    method public final boolean isSystemProviderRequired();
     property public final android.os.Bundle candidateQueryData;
     property public final android.os.Bundle credentialData;
-    property public final boolean requireSystemProvider;
+    property public final boolean isSystemProviderRequired;
     property public final String type;
   }
 
@@ -44,23 +53,23 @@
   }
 
   public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional boolean allowHybrid);
+    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional boolean preferImmediatelyAvailableCredentials);
     ctor public CreatePublicKeyCredentialRequest(String requestJson);
-    method public boolean getAllowHybrid();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String requestJson;
   }
 
   public final class CreatePublicKeyCredentialRequestPrivileged extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean allowHybrid);
+    ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
     ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash);
-    method public boolean getAllowHybrid();
     method public String getClientDataHash();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRelyingParty();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
     property public final String clientDataHash;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String relyingParty;
     property public final String requestJson;
   }
@@ -68,8 +77,8 @@
   public static final class CreatePublicKeyCredentialRequestPrivileged.Builder {
     ctor public CreatePublicKeyCredentialRequestPrivileged.Builder(String requestJson, String relyingParty, String clientDataHash);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged build();
-    method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setAllowHybrid(boolean allowHybrid);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setClientDataHash(String clientDataHash);
+    method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setRelyingParty(String relyingParty);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setRequestJson(String requestJson);
   }
@@ -87,10 +96,10 @@
     method public suspend Object? clearCredentialState(androidx.credentials.ClearCredentialStateRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public void clearCredentialStateAsync(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void,androidx.credentials.exceptions.ClearCredentialException> callback);
     method public static androidx.credentials.CredentialManager create(android.content.Context context);
-    method public suspend Object? executeCreateCredential(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, kotlin.coroutines.Continuation<? super androidx.credentials.CreateCredentialResponse>);
-    method public void executeCreateCredentialAsync(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
-    method public suspend Object? executeGetCredential(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
-    method public void executeGetCredentialAsync(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
+    method public suspend Object? createCredential(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, kotlin.coroutines.Continuation<? super androidx.credentials.CreateCredentialResponse>);
+    method public void createCredentialAsync(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
+    method public suspend Object? getCredential(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
+    method public void getCredentialAsync(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
     field public static final androidx.credentials.CredentialManager.Companion Companion;
   }
 
@@ -103,6 +112,9 @@
     method public void onResult(R? result);
   }
 
+  public abstract class CredentialOption {
+  }
+
   public interface CredentialProvider {
     method public boolean isAvailableOnDevice();
     method public void onClearCredential(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void,androidx.credentials.exceptions.ClearCredentialException> callback);
@@ -118,24 +130,21 @@
     property public final String type;
   }
 
-  public abstract class GetCredentialOption {
-  }
-
   public final class GetCredentialRequest {
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.GetCredentialOption> getCredentialOptions, optional boolean isAutoSelectAllowed);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.GetCredentialOption> getCredentialOptions);
-    method public java.util.List<androidx.credentials.GetCredentialOption> getGetCredentialOptions();
+    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional boolean isAutoSelectAllowed);
+    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
+    method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
     method public boolean isAutoSelectAllowed();
-    property public final java.util.List<androidx.credentials.GetCredentialOption> getCredentialOptions;
+    property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
     property public final boolean isAutoSelectAllowed;
   }
 
   public static final class GetCredentialRequest.Builder {
     ctor public GetCredentialRequest.Builder();
-    method public androidx.credentials.GetCredentialRequest.Builder addGetCredentialOption(androidx.credentials.GetCredentialOption getCredentialOption);
+    method public androidx.credentials.GetCredentialRequest.Builder addCredentialOption(androidx.credentials.CredentialOption credentialOption);
     method public androidx.credentials.GetCredentialRequest build();
     method public androidx.credentials.GetCredentialRequest.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.GetCredentialRequest.Builder setGetCredentialOptions(java.util.List<? extends androidx.credentials.GetCredentialOption> getCredentialOptions);
+    method public androidx.credentials.GetCredentialRequest.Builder setCredentialOptions(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
   }
 
   public final class GetCredentialResponse {
@@ -144,40 +153,40 @@
     property public final androidx.credentials.Credential credential;
   }
 
-  public class GetCustomCredentialOption extends androidx.credentials.GetCredentialOption {
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
+  public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
+    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
     method public final android.os.Bundle getCandidateQueryData();
     method public final android.os.Bundle getRequestData();
-    method public final boolean getRequireSystemProvider();
     method public final String getType();
+    method public final boolean isSystemProviderRequired();
     property public final android.os.Bundle candidateQueryData;
+    property public final boolean isSystemProviderRequired;
     property public final android.os.Bundle requestData;
-    property public final boolean requireSystemProvider;
     property public final String type;
   }
 
-  public final class GetPasswordOption extends androidx.credentials.GetCredentialOption {
+  public final class GetPasswordOption extends androidx.credentials.CredentialOption {
     ctor public GetPasswordOption();
   }
 
-  public final class GetPublicKeyCredentialOption extends androidx.credentials.GetCredentialOption {
-    ctor public GetPublicKeyCredentialOption(String requestJson, optional boolean allowHybrid);
+  public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
+    ctor public GetPublicKeyCredentialOption(String requestJson, optional boolean preferImmediatelyAvailableCredentials);
     ctor public GetPublicKeyCredentialOption(String requestJson);
-    method public boolean getAllowHybrid();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String requestJson;
   }
 
-  public final class GetPublicKeyCredentialOptionPrivileged extends androidx.credentials.GetCredentialOption {
-    ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean allowHybrid);
+  public final class GetPublicKeyCredentialOptionPrivileged extends androidx.credentials.CredentialOption {
+    ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
     ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash);
-    method public boolean getAllowHybrid();
     method public String getClientDataHash();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRelyingParty();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
     property public final String clientDataHash;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String relyingParty;
     property public final String requestJson;
   }
@@ -185,8 +194,8 @@
   public static final class GetPublicKeyCredentialOptionPrivileged.Builder {
     ctor public GetPublicKeyCredentialOptionPrivileged.Builder(String requestJson, String relyingParty, String clientDataHash);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged build();
-    method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setAllowHybrid(boolean allowHybrid);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setClientDataHash(String clientDataHash);
+    method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setRelyingParty(String relyingParty);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setRequestJson(String requestJson);
   }
diff --git a/credentials/credentials/build.gradle b/credentials/credentials/build.gradle
index ef5161a..0c8c3fb 100644
--- a/credentials/credentials/build.gradle
+++ b/credentials/credentials/build.gradle
@@ -15,7 +15,6 @@
  */
 
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -28,6 +27,7 @@
     api(libs.kotlinStdlib)
     implementation(libs.kotlinCoroutinesCore)
 
+    androidTestImplementation("androidx.activity:activity:1.2.0")
     androidTestImplementation(libs.junit)
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testCore)
diff --git a/credentials/credentials/src/androidTest/AndroidManifest.xml b/credentials/credentials/src/androidTest/AndroidManifest.xml
index 4995896..79077dd 100644
--- a/credentials/credentials/src/androidTest/AndroidManifest.xml
+++ b/credentials/credentials/src/androidTest/AndroidManifest.xml
@@ -16,4 +16,9 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <application>
+        <activity
+            android:name="androidx.credentials.TestActivity"
+            android:exported="false"/>
+    </application>
 </manifest>
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java
new file mode 100644
index 0000000..b00bdb0
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials;
+
+import static androidx.credentials.internal.FrameworkImplHelper.getFinalCreateCredentialData;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CreateCredentialRequestDisplayInfoJavaTest {
+    private Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
+
+    @Test
+    public void constructor_nullUserId_throws() {
+        assertThrows(
+                NullPointerException.class,
+                () -> new CreateCredentialRequest.DisplayInfo(null)
+        );
+    }
+
+    @Test
+    public void constructor_emptyUserId_throws() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new CreateCredentialRequest.DisplayInfo("")
+        );
+    }
+
+    @Test
+    public void constructWithUserIdOnly_success() {
+        String expectedUserId = "userId";
+
+        CreateCredentialRequest.DisplayInfo displayInfo =
+                new CreateCredentialRequest.DisplayInfo(expectedUserId);
+
+        assertThat(displayInfo.getUserId()).isEqualTo(expectedUserId);
+        assertThat(displayInfo.getUserDisplayName()).isNull();
+        assertThat(displayInfo.getCredentialTypeIcon()).isNull();
+    }
+
+    @Test
+    public void constructWithUserIdAndDisplayName_success() {
+        String expectedUserId = "userId";
+        String expectedDisplayName = "displayName";
+
+        CreateCredentialRequest.DisplayInfo displayInfo =
+                new CreateCredentialRequest.DisplayInfo(expectedUserId,
+                        expectedDisplayName);
+
+        assertThat(displayInfo.getUserId()).isEqualTo(expectedUserId);
+        assertThat(displayInfo.getUserDisplayName()).isEqualTo(expectedDisplayName);
+        assertThat(displayInfo.getCredentialTypeIcon()).isNull();
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    public void constructFromBundle_success() {
+        String expectedUserId = "userId";
+        CreatePasswordRequest request = new CreatePasswordRequest(expectedUserId, "password");
+
+        CreateCredentialRequest.DisplayInfo displayInfo =
+                CreateCredentialRequest.DisplayInfo.parseFromCredentialDataBundle(
+                        getFinalCreateCredentialData(
+                                request, mContext)
+                );
+
+        assertThat(displayInfo.getUserId()).isEqualTo(expectedUserId);
+        assertThat(displayInfo.getUserDisplayName()).isNull();
+        assertThat(displayInfo.getCredentialTypeIcon().getResId()).isEqualTo(
+                R.drawable.ic_password);
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt
new file mode 100644
index 0000000..2fb3e1d
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials
+
+import androidx.credentials.CreateCredentialRequest.DisplayInfo
+import androidx.credentials.CreateCredentialRequest.DisplayInfo.Companion.parseFromCredentialDataBundle
+import androidx.credentials.internal.FrameworkImplHelper.Companion.getFinalCreateCredentialData
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CreateCredentialRequestDisplayInfoTest {
+
+    private val mContext = InstrumentationRegistry.getInstrumentation().context
+
+    @Test
+    fun constructor_emptyUserId_throws() {
+        assertThrows(
+            IllegalArgumentException::class.java
+        ) { DisplayInfo("") }
+    }
+
+    @Test
+    fun constructWithUserIdOnly_success() {
+        val expectedUserId = "userId"
+
+        val displayInfo = DisplayInfo(expectedUserId)
+
+        assertThat(displayInfo.userId).isEqualTo(expectedUserId)
+        assertThat(displayInfo.userDisplayName).isNull()
+        assertThat(displayInfo.credentialTypeIcon).isNull()
+    }
+
+    @Test
+    fun constructWithUserIdAndDisplayName_success() {
+        val expectedUserId = "userId"
+        val expectedDisplayName = "displayName"
+
+        val displayInfo = DisplayInfo(
+            expectedUserId,
+            expectedDisplayName
+        )
+
+        assertThat(displayInfo.userId).isEqualTo(expectedUserId)
+        assertThat(displayInfo.userDisplayName).isEqualTo(expectedDisplayName)
+        assertThat(displayInfo.credentialTypeIcon).isNull()
+    }
+
+    @SdkSuppress(minSdkVersion = 28)
+    @Test
+    fun constructFromBundle_success() {
+        val expectedUserId = "userId"
+        val request = CreatePasswordRequest(expectedUserId, "password")
+
+        val displayInfo = parseFromCredentialDataBundle(
+            getFinalCreateCredentialData(
+                request, mContext
+            )
+        )
+
+        assertThat(displayInfo!!.userId).isEqualTo(expectedUserId)
+        assertThat(displayInfo.userDisplayName).isNull()
+        assertThat(displayInfo.credentialTypeIcon?.resId).isEqualTo(
+            R.drawable.ic_password
+        )
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestJavaTest.java
index 1615117..b5be84f 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestJavaTest.java
@@ -29,15 +29,26 @@
     public void constructor_nullType_throws() {
         assertThrows("Expected null type to throw NPE",
                 NullPointerException.class,
-                () -> new CreateCustomCredentialRequest(null, new Bundle(), new Bundle(), false)
+                () -> new CreateCustomCredentialRequest(null, new Bundle(), new Bundle(), false,
+                        new CreateCredentialRequest.DisplayInfo("userId"))
         );
     }
 
     @Test
-    public void constructor_nullBundle_throws() {
-        assertThrows("Expected null bundle to throw NPE",
+    public void constructor_nullCredentialData_throws() {
+        assertThrows(
                 NullPointerException.class,
-                () -> new CreateCustomCredentialRequest("T", null, new Bundle(), true)
+                () -> new CreateCustomCredentialRequest("T", null, new Bundle(), true,
+                        new CreateCredentialRequest.DisplayInfo("userId"))
+        );
+    }
+
+    @Test
+    public void constructor_nullCandidateQueryData_throws() {
+        assertThrows(
+                NullPointerException.class,
+                () -> new CreateCustomCredentialRequest("T", new Bundle(), null, true,
+                        new CreateCredentialRequest.DisplayInfo("userId"))
         );
     }
 
@@ -45,34 +56,47 @@
     public void constructor_emptyType_throws() {
         assertThrows("Expected empty type to throw IAE",
                 IllegalArgumentException.class,
-                () -> new CreateCustomCredentialRequest("", new Bundle(), new Bundle(), false)
+                () -> new CreateCustomCredentialRequest("", new Bundle(), new Bundle(), false,
+                        new CreateCredentialRequest.DisplayInfo("userId"))
         );
     }
 
     @Test
     public void constructor_nonEmptyTypeNonNullBundle_success() {
-        new CreateCustomCredentialRequest("T", new Bundle(), new Bundle(), true);
+        new CreateCustomCredentialRequest("T", new Bundle(), new Bundle(), true,
+                new CreateCredentialRequest.DisplayInfo("userId"));
     }
 
     @Test
-    public void getter_frameworkProperties() {
+    public void getter() {
         String expectedType = "TYPE";
         Bundle expectedCredentialDataBundle = new Bundle();
         expectedCredentialDataBundle.putString("Test", "Test");
         Bundle expectedCandidateQueryDataBundle = new Bundle();
         expectedCandidateQueryDataBundle.putBoolean("key", true);
-
+        CreateCredentialRequest.DisplayInfo expectedDisplayInfo =
+                new CreateCredentialRequest.DisplayInfo("userId");
         boolean expectedSystemProvider = true;
-        CreateCustomCredentialRequest option = new CreateCustomCredentialRequest(expectedType,
+
+        CreateCustomCredentialRequest request = new CreateCustomCredentialRequest(expectedType,
                 expectedCredentialDataBundle,
                 expectedCandidateQueryDataBundle,
-                expectedSystemProvider);
+                expectedSystemProvider,
+                expectedDisplayInfo);
 
-        assertThat(option.getType()).isEqualTo(expectedType);
-        assertThat(TestUtilsKt.equals(option.getCredentialData(), expectedCredentialDataBundle))
+        assertThat(request.getType()).isEqualTo(expectedType);
+        assertThat(TestUtilsKt.equals(request.getCredentialData(), expectedCredentialDataBundle))
                 .isTrue();
-        assertThat(TestUtilsKt.equals(option.getCandidateQueryData(),
+        assertThat(TestUtilsKt.equals(request.getCandidateQueryData(),
                 expectedCandidateQueryDataBundle)).isTrue();
-        assertThat(option.requireSystemProvider()).isEqualTo(expectedSystemProvider);
+        assertThat(request.isSystemProviderRequired()).isEqualTo(expectedSystemProvider);
+        assertThat(request.getDisplayInfo$credentials_debug()).isEqualTo(expectedDisplayInfo);
+    }
+
+    @Test
+    public void constructionWithNullRequestDisplayInfo_throws() {
+        assertThrows(
+                NullPointerException.class, () -> new CreateCustomCredentialRequest("type",
+                        new Bundle(), new Bundle(), false, /* requestDisplayInfo= */null));
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestTest.kt
index 75b2b80..5978e6d 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestTest.kt
@@ -17,55 +17,53 @@
 package androidx.credentials
 
 import android.os.Bundle
+import androidx.credentials.CreateCredentialRequest.DisplayInfo
 import com.google.common.truth.Truth.assertThat
-import org.junit.Assert
+import org.junit.Assert.assertThrows
 import org.junit.Test
 
 class CreateCustomCredentialRequestTest {
     @Test
     fun constructor_emptyType_throws() {
-        Assert.assertThrows(
+        assertThrows(
             "Expected empty type to throw IAE",
             IllegalArgumentException::class.java
         ) {
             CreateCustomCredentialRequest(
-                "",
-                Bundle(),
-                Bundle(),
-                true
+                "", Bundle(), Bundle(), false,
+                DisplayInfo("userId")
             )
         }
     }
 
     @Test
-    fun constructor_nonEmptyTypeNonNullBundle_success() {
-        CreateCustomCredentialRequest("T", Bundle(), Bundle(), false)
-    }
-
-    @Test
-    fun getter_frameworkProperties() {
+    fun getter() {
         val expectedType = "TYPE"
         val expectedCredentialDataBundle = Bundle()
         expectedCredentialDataBundle.putString("Test", "Test")
         val expectedCandidateQueryDataBundle = Bundle()
         expectedCandidateQueryDataBundle.putBoolean("key", true)
-
+        val expectedDisplayInfo = DisplayInfo("userId")
         val expectedSystemProvider = true
-        val option = CreateCustomCredentialRequest(
+
+        val request = CreateCustomCredentialRequest(
             expectedType,
-            expectedCredentialDataBundle, expectedCandidateQueryDataBundle,
-            expectedSystemProvider
+            expectedCredentialDataBundle,
+            expectedCandidateQueryDataBundle,
+            expectedSystemProvider,
+            expectedDisplayInfo
         )
 
-        assertThat(option.type).isEqualTo(expectedType)
-        assertThat(equals(option.credentialData, expectedCredentialDataBundle))
+        assertThat(request.type).isEqualTo(expectedType)
+        assertThat(equals(request.credentialData, expectedCredentialDataBundle))
             .isTrue()
         assertThat(
             equals(
-                option.candidateQueryData,
+                request.candidateQueryData,
                 expectedCandidateQueryDataBundle
             )
         ).isTrue()
-        assertThat(option.requireSystemProvider).isEqualTo(expectedSystemProvider)
+        assertThat(request.isSystemProviderRequired).isEqualTo(expectedSystemProvider)
+        assertThat(request.displayInfo).isEqualTo(expectedDisplayInfo)
     }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
index a873552..63951b4 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
@@ -16,14 +16,20 @@
 
 package androidx.credentials;
 
+import static androidx.credentials.internal.FrameworkImplHelper.getFinalCreateCredentialData;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertThrows;
 
+import android.content.Context;
+import android.graphics.drawable.Icon;
 import android.os.Bundle;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -31,6 +37,8 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class CreatePasswordRequestJavaTest {
+    private Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
+
     @Test
     public void constructor_nullId_throws() {
         assertThrows(
@@ -69,6 +77,8 @@
         assertThat(request.getPassword()).isEqualTo(passwordExpected);
     }
 
+    @SdkSuppress(minSdkVersion = 28)
+    @SuppressWarnings("deprecation") // bundle.get(key)
     @Test
     public void getter_frameworkProperties() {
         String idExpected = "id";
@@ -80,18 +90,41 @@
         CreatePasswordRequest request = new CreatePasswordRequest(idExpected, passwordExpected);
 
         assertThat(request.getType()).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL);
-        assertThat(TestUtilsKt.equals(request.getCredentialData(), expectedData)).isTrue();
+        CreateCredentialRequest.DisplayInfo displayInfo =
+                request.getDisplayInfo$credentials_debug();
+        assertThat(displayInfo.getUserDisplayName()).isNull();
+        assertThat(displayInfo.getUserId()).isEqualTo(idExpected);
         assertThat(TestUtilsKt.equals(request.getCandidateQueryData(), Bundle.EMPTY)).isTrue();
-        assertThat(request.getRequireSystemProvider()).isFalse();
+        assertThat(request.isSystemProviderRequired()).isFalse();
+        Bundle credentialData =
+                getFinalCreateCredentialData(
+                        request, mContext);
+        assertThat(credentialData.keySet())
+                .hasSize(expectedData.size() + /* added request info */ 1);
+        for (String key : expectedData.keySet()) {
+            assertThat(credentialData.get(key)).isEqualTo(credentialData.get(key));
+        }
+        Bundle displayInfoBundle =
+                credentialData.getBundle(
+                        CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_REQUEST_DISPLAY_INFO);
+        assertThat(displayInfoBundle.keySet()).hasSize(2);
+        assertThat(displayInfoBundle.getString(
+                CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_USER_ID)).isEqualTo(idExpected);
+        assertThat(((Icon) (displayInfoBundle.getParcelable(
+                CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_CREDENTIAL_TYPE_ICON))).getResId()
+        ).isEqualTo(R.drawable.ic_password);
     }
 
+    @SdkSuppress(minSdkVersion = 28)
     @Test
     public void frameworkConversion_success() {
-        CreatePasswordRequest request = new CreatePasswordRequest("id", "password");
+        String idExpected = "id";
+        CreatePasswordRequest request = new CreatePasswordRequest(idExpected, "password");
 
         CreateCredentialRequest convertedRequest = CreateCredentialRequest.createFrom(
-                request.getType(), request.getCredentialData(),
-                request.getCandidateQueryData(), request.getRequireSystemProvider()
+                request.getType(), getFinalCreateCredentialData(
+                        request, mContext),
+                request.getCandidateQueryData(), request.isSystemProviderRequired()
         );
 
         assertThat(convertedRequest).isInstanceOf(CreatePasswordRequest.class);
@@ -99,5 +132,11 @@
                 (CreatePasswordRequest) convertedRequest;
         assertThat(convertedCreatePasswordRequest.getPassword()).isEqualTo(request.getPassword());
         assertThat(convertedCreatePasswordRequest.getId()).isEqualTo(request.getId());
+        CreateCredentialRequest.DisplayInfo displayInfo =
+                convertedCreatePasswordRequest.getDisplayInfo$credentials_debug();
+        assertThat(displayInfo.getUserDisplayName()).isNull();
+        assertThat(displayInfo.getUserId()).isEqualTo(idExpected);
+        assertThat(displayInfo.getCredentialTypeIcon().getResId())
+                .isEqualTo(R.drawable.ic_password);
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
index 2e3279c..4329126 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
@@ -16,10 +16,14 @@
 
 package androidx.credentials
 
+import android.graphics.drawable.Icon
 import android.os.Bundle
 import androidx.credentials.CreateCredentialRequest.Companion.createFrom
+import androidx.credentials.internal.FrameworkImplHelper.Companion.getFinalCreateCredentialData
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
 import androidx.testutils.assertThrows
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -28,6 +32,9 @@
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class CreatePasswordRequestTest {
+
+    private val mContext = InstrumentationRegistry.getInstrumentation().context
+
     @Test
     fun constructor_emptyPassword_throws() {
         assertThrows<IllegalArgumentException> {
@@ -49,29 +56,56 @@
         assertThat(request.password).isEqualTo(passwordExpected)
     }
 
+    @SdkSuppress(minSdkVersion = 28)
+    @Suppress("DEPRECATION") // bundle.get(key)
     @Test
     fun getter_frameworkProperties() {
         val idExpected = "id"
         val passwordExpected = "pwd"
-        val expectedData = Bundle()
-        expectedData.putString(CreatePasswordRequest.BUNDLE_KEY_ID, idExpected)
-        expectedData.putString(CreatePasswordRequest.BUNDLE_KEY_PASSWORD, passwordExpected)
+        val expectedCredentialData = Bundle()
+        expectedCredentialData.putString(CreatePasswordRequest.BUNDLE_KEY_ID, idExpected)
+        expectedCredentialData.putString(
+            CreatePasswordRequest.BUNDLE_KEY_PASSWORD,
+            passwordExpected
+        )
 
         val request = CreatePasswordRequest(idExpected, passwordExpected)
 
         assertThat(request.type).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL)
-        assertThat(equals(request.credentialData, expectedData)).isTrue()
         assertThat(equals(request.candidateQueryData, Bundle.EMPTY)).isTrue()
-        assertThat(request.requireSystemProvider).isFalse()
+        assertThat(request.isSystemProviderRequired).isFalse()
+        assertThat(request.displayInfo.userDisplayName).isNull()
+        assertThat(request.displayInfo.userId).isEqualTo(idExpected)
+        val credentialData = getFinalCreateCredentialData(
+            request, mContext
+        )
+        assertThat(credentialData.keySet())
+            .hasSize(expectedCredentialData.size() + /* added request info */ 1)
+        for (key in expectedCredentialData.keySet()) {
+            assertThat(expectedCredentialData.get(key)).isEqualTo(credentialData.get(key))
+        }
+        val displayInfoBundle =
+            credentialData.getBundle(
+                CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_REQUEST_DISPLAY_INFO)!!
+        assertThat(displayInfoBundle.keySet()).hasSize(2)
+        assertThat(displayInfoBundle.getString(
+            CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_USER_ID)).isEqualTo(idExpected)
+        assertThat((displayInfoBundle.getParcelable(
+            CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_CREDENTIAL_TYPE_ICON) as Icon?)!!.resId
+        ).isEqualTo(R.drawable.ic_password)
     }
 
+    @SdkSuppress(minSdkVersion = 28)
     @Test
     fun frameworkConversion_success() {
-        val request = CreatePasswordRequest("id", "password")
+        val idExpected = "id"
+        val request = CreatePasswordRequest(idExpected, "password")
 
         val convertedRequest = createFrom(
-            request.type, request.credentialData,
-            request.candidateQueryData, request.requireSystemProvider
+            request.type, getFinalCreateCredentialData(
+                request, mContext
+            ),
+            request.candidateQueryData, request.isSystemProviderRequired
         )
 
         assertThat(convertedRequest).isInstanceOf(
@@ -80,5 +114,9 @@
         val convertedCreatePasswordRequest = convertedRequest as CreatePasswordRequest
         assertThat(convertedCreatePasswordRequest.password).isEqualTo(request.password)
         assertThat(convertedCreatePasswordRequest.id).isEqualTo(request.id)
+        assertThat(convertedCreatePasswordRequest.displayInfo.userDisplayName).isNull()
+        assertThat(convertedCreatePasswordRequest.displayInfo.userId).isEqualTo(idExpected)
+        assertThat(convertedCreatePasswordRequest.displayInfo.credentialTypeIcon?.resId)
+            .isEqualTo(R.drawable.ic_password)
     }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
index c7abdcb..838217a 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
@@ -16,17 +16,22 @@
 
 package androidx.credentials;
 
-import static androidx.credentials.CreatePublicKeyCredentialRequest.BUNDLE_KEY_ALLOW_HYBRID;
+import static androidx.credentials.CreatePublicKeyCredentialRequest.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS;
 import static androidx.credentials.CreatePublicKeyCredentialRequest.BUNDLE_KEY_REQUEST_JSON;
+import static androidx.credentials.internal.FrameworkImplHelper.getFinalCreateCredentialData;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertThrows;
 
+import android.content.Context;
+import android.graphics.drawable.Icon;
 import android.os.Bundle;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -34,6 +39,16 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class CreatePublicKeyCredentialRequestJavaTest {
+    private static final String TEST_USERNAME = "test-user-name@gmail.com";
+    private static final String TEST_USER_DISPLAYNAME = "Test User";
+    private static final String TEST_REQUEST_JSON = String.format("{\"rp\":{\"name\":true,"
+                    + "\"id\":\"app-id\"},\"user\":{\"name\":\"%s\",\"id\":\"id-value\","
+                    + "\"displayName\":\"%s\",\"icon\":true}, \"challenge\":true,"
+                    + "\"pubKeyCredParams\":true,\"excludeCredentials\":true,"
+                    + "\"attestation\":true}", TEST_USERNAME,
+            TEST_USER_DISPLAYNAME);
+
+    private Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
 
     @Test
     public void constructor_emptyJson_throwsIllegalArgumentException() {
@@ -44,6 +59,14 @@
     }
 
     @Test
+    public void constructor_jsonMissingUserName_throwsIllegalArgumentException() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new CreatePublicKeyCredentialRequest("json")
+        );
+    }
+
+    @Test
     public void constructor_nullJson_throwsNullPointerException() {
         assertThrows("Expected null Json to throw NPE",
                 NullPointerException.class,
@@ -52,33 +75,35 @@
     }
 
     @Test
-    public void constructor_success()  {
+    public void constructor_success() {
         new CreatePublicKeyCredentialRequest(
-                "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}");
+                "{\"user\":{\"name\":{\"lol\":\"Value\"}}}");
     }
 
     @Test
-    public void constructor_setsAllowHybridToTrueByDefault()  {
+    public void constructor_setsPreferImmediatelyAvailableCredentialsToFalseByDefault() {
         CreatePublicKeyCredentialRequest createPublicKeyCredentialRequest =
-                new CreatePublicKeyCredentialRequest(
-                        "JSON");
-        boolean allowHybridActual = createPublicKeyCredentialRequest.allowHybrid();
-        assertThat(allowHybridActual).isTrue();
+                new CreatePublicKeyCredentialRequest(TEST_REQUEST_JSON);
+        boolean preferImmediatelyAvailableCredentialsActual =
+                createPublicKeyCredentialRequest.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isFalse();
     }
 
     @Test
-    public void constructor_setsAllowHybridToFalse()  {
-        boolean allowHybridExpected = false;
+    public void constructor_setPreferImmediatelyAvailableCredentialsToTrue() {
+        boolean preferImmediatelyAvailableCredentialsExpected = true;
         CreatePublicKeyCredentialRequest createPublicKeyCredentialRequest =
-                new CreatePublicKeyCredentialRequest("testJson",
-                        allowHybridExpected);
-        boolean allowHybridActual = createPublicKeyCredentialRequest.allowHybrid();
-        assertThat(allowHybridActual).isEqualTo(allowHybridExpected);
+                new CreatePublicKeyCredentialRequest(TEST_REQUEST_JSON,
+                        preferImmediatelyAvailableCredentialsExpected);
+        boolean preferImmediatelyAvailableCredentialsActual =
+                createPublicKeyCredentialRequest.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+                preferImmediatelyAvailableCredentialsExpected);
     }
 
     @Test
     public void getter_requestJson_success() {
-        String testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
+        String testJsonExpected = "{\"user\":{\"name\":{\"lol\":\"Value\"}}}";
         CreatePublicKeyCredentialRequest createPublicKeyCredentialReq =
                 new CreatePublicKeyCredentialRequest(testJsonExpected);
 
@@ -86,10 +111,12 @@
         assertThat(testJsonActual).isEqualTo(testJsonExpected);
     }
 
+    @SdkSuppress(minSdkVersion = 28)
+    @SuppressWarnings("deprecation") // bundle.get(key)
     @Test
     public void getter_frameworkProperties_success() {
-        String requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
-        boolean allowHybridExpected = false;
+        String requestJsonExpected = TEST_REQUEST_JSON;
+        boolean preferImmediatelyAvailableCredentialsExpected = false;
         Bundle expectedData = new Bundle();
         expectedData.putString(
                 PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
@@ -98,31 +125,59 @@
         expectedData.putString(
                 BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
         expectedData.putBoolean(
-                BUNDLE_KEY_ALLOW_HYBRID, allowHybridExpected);
+                BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+                preferImmediatelyAvailableCredentialsExpected);
 
-        CreatePublicKeyCredentialRequest request =
-                new CreatePublicKeyCredentialRequest(requestJsonExpected, allowHybridExpected);
+        CreatePublicKeyCredentialRequest request = new CreatePublicKeyCredentialRequest(
+                requestJsonExpected, preferImmediatelyAvailableCredentialsExpected);
 
         assertThat(request.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
-        assertThat(TestUtilsKt.equals(request.getCredentialData(), expectedData)).isTrue();
         assertThat(TestUtilsKt.equals(request.getCandidateQueryData(), expectedData)).isTrue();
-        assertThat(request.getRequireSystemProvider()).isFalse();
+        assertThat(request.isSystemProviderRequired()).isFalse();
+        Bundle credentialData = getFinalCreateCredentialData(
+                request, mContext);
+        assertThat(credentialData.keySet())
+                .hasSize(expectedData.size() + /* added request info */ 1);
+        for (String key : expectedData.keySet()) {
+            assertThat(credentialData.get(key)).isEqualTo(credentialData.get(key));
+        }
+        Bundle displayInfoBundle =
+                credentialData.getBundle(
+                        CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_REQUEST_DISPLAY_INFO);
+        assertThat(displayInfoBundle.keySet()).hasSize(3);
+        assertThat(displayInfoBundle.getString(
+                CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_USER_ID)).isEqualTo(TEST_USERNAME);
+        assertThat(displayInfoBundle.getString(
+                CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_USER_DISPLAY_NAME
+        )).isEqualTo(TEST_USER_DISPLAYNAME);
+        assertThat(((Icon) (displayInfoBundle.getParcelable(
+                CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_CREDENTIAL_TYPE_ICON))).getResId()
+        ).isEqualTo(R.drawable.ic_passkey);
     }
 
+    @SdkSuppress(minSdkVersion = 28)
     @Test
     public void frameworkConversion_success() {
         CreatePublicKeyCredentialRequest request =
-                new CreatePublicKeyCredentialRequest("json", true);
+                new CreatePublicKeyCredentialRequest(TEST_REQUEST_JSON, true);
 
         CreateCredentialRequest convertedRequest = CreateCredentialRequest.createFrom(
-                request.getType(), request.getCredentialData(),
-                request.getCandidateQueryData(), request.getRequireSystemProvider()
+                request.getType(), getFinalCreateCredentialData(
+                        request, mContext),
+                request.getCandidateQueryData(), request.isSystemProviderRequired()
         );
 
         assertThat(convertedRequest).isInstanceOf(CreatePublicKeyCredentialRequest.class);
         CreatePublicKeyCredentialRequest convertedSubclassRequest =
                 (CreatePublicKeyCredentialRequest) convertedRequest;
         assertThat(convertedSubclassRequest.getRequestJson()).isEqualTo(request.getRequestJson());
-        assertThat(convertedSubclassRequest.allowHybrid()).isEqualTo(request.allowHybrid());
+        assertThat(convertedSubclassRequest.preferImmediatelyAvailableCredentials())
+                .isEqualTo(request.preferImmediatelyAvailableCredentials());
+        CreateCredentialRequest.DisplayInfo displayInfo =
+                convertedRequest.getDisplayInfo$credentials_debug();
+        assertThat(displayInfo.getUserDisplayName()).isEqualTo(TEST_USER_DISPLAYNAME);
+        assertThat(displayInfo.getUserId()).isEqualTo(TEST_USERNAME);
+        assertThat(displayInfo.getCredentialTypeIcon().getResId())
+                .isEqualTo(R.drawable.ic_passkey);
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedJavaTest.java
index 623ae8d..85901a3 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedJavaTest.java
@@ -16,17 +16,22 @@
 
 package androidx.credentials;
 
-import static androidx.credentials.CreatePublicKeyCredentialRequest.BUNDLE_KEY_ALLOW_HYBRID;
+import static androidx.credentials.CreatePublicKeyCredentialRequest.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS;
 import static androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_CLIENT_DATA_HASH;
 import static androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_RELYING_PARTY;
 import static androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_REQUEST_JSON;
+import static androidx.credentials.internal.FrameworkImplHelper.getFinalCreateCredentialData;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.content.Context;
+import android.graphics.drawable.Icon;
 import android.os.Bundle;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -38,57 +43,72 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class CreatePublicKeyCredentialRequestPrivilegedJavaTest {
+    private static final String TEST_USERNAME = "test-user-name@gmail.com";
+    private static final String TEST_USER_DISPLAYNAME = "Test User";
+    private static final String TEST_REQUEST_JSON = String.format("{\"rp\":{\"name\":true,"
+                    + "\"id\":\"app-id\"},\"user\":{\"name\":\"%s\",\"id\":\"id-value\","
+                    + "\"displayName\":\"%s\",\"icon\":true}, \"challenge\":true,"
+                    + "\"pubKeyCredParams\":true,\"excludeCredentials\":true,"
+                    + "\"attestation\":true}", TEST_USERNAME,
+            TEST_USER_DISPLAYNAME);
+    private Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
 
     @Test
     public void constructor_success() {
         new CreatePublicKeyCredentialRequestPrivileged(
-                "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
+                "{\"user\":{\"name\":{\"lol\":\"Value\"}}}",
                 "relyingParty", "ClientDataHash");
     }
 
     @Test
-    public void constructor_setsAllowHybridToTrueByDefault() {
+    public void constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
         CreatePublicKeyCredentialRequestPrivileged createPublicKeyCredentialRequestPrivileged =
                 new CreatePublicKeyCredentialRequestPrivileged(
-                        "JSON", "relyingParty", "HASH");
-        boolean allowHybridActual = createPublicKeyCredentialRequestPrivileged.allowHybrid();
-        assertThat(allowHybridActual).isTrue();
+                        TEST_REQUEST_JSON, "relyingParty", "HASH");
+        boolean preferImmediatelyAvailableCredentialsActual =
+                createPublicKeyCredentialRequestPrivileged.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isFalse();
     }
 
     @Test
-    public void constructor_setsAllowHybridToFalse() {
-        boolean allowHybridExpected = false;
+    public void constructor_setPreferImmediatelyAvailableCredentialsToTrue() {
+        boolean preferImmediatelyAvailableCredentialsExpected = true;
         CreatePublicKeyCredentialRequestPrivileged createPublicKeyCredentialRequestPrivileged =
-                new CreatePublicKeyCredentialRequestPrivileged("JSON",
+                new CreatePublicKeyCredentialRequestPrivileged(TEST_REQUEST_JSON,
                         "relyingParty",
                         "HASH",
-                        allowHybridExpected);
-        boolean allowHybridActual = createPublicKeyCredentialRequestPrivileged.allowHybrid();
-        assertThat(allowHybridActual).isEqualTo(allowHybridExpected);
+                        preferImmediatelyAvailableCredentialsExpected);
+        boolean preferImmediatelyAvailableCredentialsActual =
+                createPublicKeyCredentialRequestPrivileged.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+                preferImmediatelyAvailableCredentialsExpected);
     }
 
     @Test
-    public void builder_build_defaultAllowHybrid_true() {
+    public void builder_build_defaultPreferImmediatelyAvailableCredentials_false() {
         CreatePublicKeyCredentialRequestPrivileged defaultPrivilegedRequest = new
-                CreatePublicKeyCredentialRequestPrivileged.Builder("{\"Data\":5}",
+                CreatePublicKeyCredentialRequestPrivileged.Builder(TEST_REQUEST_JSON,
                 "relyingParty", "HASH").build();
-        assertThat(defaultPrivilegedRequest.allowHybrid()).isTrue();
+        assertThat(defaultPrivilegedRequest.preferImmediatelyAvailableCredentials()).isFalse();
     }
 
     @Test
-    public void builder_build_nonDefaultAllowHybrid_false() {
-        boolean allowHybridExpected = false;
+    public void builder_build_nonDefaultPreferImmediatelyAvailableCredentials_true() {
+        boolean preferImmediatelyAvailableCredentialsExpected = true;
         CreatePublicKeyCredentialRequestPrivileged createPublicKeyCredentialRequestPrivileged =
-                new CreatePublicKeyCredentialRequestPrivileged.Builder("JSON",
+                new CreatePublicKeyCredentialRequestPrivileged.Builder(TEST_REQUEST_JSON,
                         "relyingParty", "HASH")
-                        .setAllowHybrid(allowHybridExpected).build();
-        boolean allowHybridActual = createPublicKeyCredentialRequestPrivileged.allowHybrid();
-        assertThat(allowHybridActual).isEqualTo(allowHybridExpected);
+                        .setPreferImmediatelyAvailableCredentials(
+                                preferImmediatelyAvailableCredentialsExpected).build();
+        boolean preferImmediatelyAvailableCredentialsActual =
+                createPublicKeyCredentialRequestPrivileged.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+                preferImmediatelyAvailableCredentialsExpected);
     }
 
     @Test
     public void getter_requestJson_success() {
-        String testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
+        String testJsonExpected = "{\"user\":{\"name\":{\"lol\":\"Value\"}}}";
         CreatePublicKeyCredentialRequestPrivileged createPublicKeyCredentialReqPriv =
                 new CreatePublicKeyCredentialRequestPrivileged(testJsonExpected,
                         "relyingParty", "HASH");
@@ -98,14 +118,13 @@
 
     @Test
     public void getter_relyingParty_success() {
-        String testrelyingPartyExpected = "relyingParty";
+        String testRelyingPartyExpected = "relyingParty";
         CreatePublicKeyCredentialRequestPrivileged createPublicKeyCredentialRequestPrivileged =
                 new CreatePublicKeyCredentialRequestPrivileged(
-                        "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
-                        testrelyingPartyExpected, "X342%4dfd7&");
-        String testrelyingPartyActual = createPublicKeyCredentialRequestPrivileged
+                        TEST_REQUEST_JSON, testRelyingPartyExpected, "X342%4dfd7&");
+        String testRelyingPartyActual = createPublicKeyCredentialRequestPrivileged
                 .getRelyingParty();
-        assertThat(testrelyingPartyActual).isEqualTo(testrelyingPartyExpected);
+        assertThat(testRelyingPartyActual).isEqualTo(testRelyingPartyExpected);
     }
 
     @Test
@@ -113,49 +132,72 @@
         String clientDataHashExpected = "X342%4dfd7&";
         CreatePublicKeyCredentialRequestPrivileged createPublicKeyCredentialRequestPrivileged =
                 new CreatePublicKeyCredentialRequestPrivileged(
-                        "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
-                         "relyingParty", clientDataHashExpected);
+                        TEST_REQUEST_JSON, "relyingParty", clientDataHashExpected);
         String clientDataHashActual =
                 createPublicKeyCredentialRequestPrivileged.getClientDataHash();
         assertThat(clientDataHashActual).isEqualTo(clientDataHashExpected);
     }
 
+    @SdkSuppress(minSdkVersion = 28)
+    @SuppressWarnings("deprecation") // bundle.get(key)
     @Test
     public void getter_frameworkProperties_success() {
-        String requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
+        String requestJsonExpected = TEST_REQUEST_JSON;
         String relyingPartyExpected = "relyingParty";
         String clientDataHashExpected = "X342%4dfd7&";
-        boolean allowHybridExpected = false;
+        boolean preferImmediatelyAvailableCredentialsExpected = false;
         Bundle expectedData = new Bundle();
         expectedData.putString(
                 PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
                 CreatePublicKeyCredentialRequestPrivileged
-                        .BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIVILEGED);
+                        .BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV);
         expectedData.putString(BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
         expectedData.putString(BUNDLE_KEY_RELYING_PARTY, relyingPartyExpected);
         expectedData.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHashExpected);
-        expectedData.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybridExpected);
+        expectedData.putBoolean(
+                BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+                preferImmediatelyAvailableCredentialsExpected);
 
         CreatePublicKeyCredentialRequestPrivileged request =
                 new CreatePublicKeyCredentialRequestPrivileged(
                         requestJsonExpected, relyingPartyExpected, clientDataHashExpected,
-                        allowHybridExpected);
+                        preferImmediatelyAvailableCredentialsExpected);
 
         assertThat(request.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
-        assertThat(TestUtilsKt.equals(request.getCredentialData(), expectedData)).isTrue();
         assertThat(TestUtilsKt.equals(request.getCandidateQueryData(), expectedData)).isTrue();
-        assertThat(request.getRequireSystemProvider()).isFalse();
+        assertThat(request.isSystemProviderRequired()).isFalse();
+        Bundle credentialData = getFinalCreateCredentialData(
+                request, mContext);
+        assertThat(credentialData.keySet())
+                .hasSize(expectedData.size() + /* added request info */ 1);
+        for (String key : expectedData.keySet()) {
+            assertThat(credentialData.get(key)).isEqualTo(credentialData.get(key));
+        }
+        Bundle displayInfoBundle =
+                credentialData.getBundle(
+                        CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_REQUEST_DISPLAY_INFO);
+        assertThat(displayInfoBundle.keySet()).hasSize(3);
+        assertThat(displayInfoBundle.getString(
+                CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_USER_ID)).isEqualTo(TEST_USERNAME);
+        assertThat(displayInfoBundle.getString(
+                CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_USER_DISPLAY_NAME)).isEqualTo(
+                TEST_USER_DISPLAYNAME);
+        assertThat(((Icon) (displayInfoBundle.getParcelable(
+                CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_CREDENTIAL_TYPE_ICON))).getResId()
+        ).isEqualTo(R.drawable.ic_passkey);
     }
 
+    @SdkSuppress(minSdkVersion = 28)
     @Test
     public void frameworkConversion_success() {
         CreatePublicKeyCredentialRequestPrivileged request =
                 new CreatePublicKeyCredentialRequestPrivileged(
-                        "json", "rp", "clientDataHash", true);
+                        TEST_REQUEST_JSON, "rp", "clientDataHash", true);
 
         CreateCredentialRequest convertedRequest = CreateCredentialRequest.createFrom(
-                request.getType(), request.getCredentialData(),
-                request.getCandidateQueryData(), request.getRequireSystemProvider()
+                request.getType(), getFinalCreateCredentialData(
+                        request, mContext),
+                request.getCandidateQueryData(), request.isSystemProviderRequired()
         );
 
         assertThat(convertedRequest).isInstanceOf(CreatePublicKeyCredentialRequestPrivileged.class);
@@ -165,6 +207,13 @@
         assertThat(convertedSubclassRequest.getRelyingParty()).isEqualTo(request.getRelyingParty());
         assertThat(convertedSubclassRequest.getClientDataHash())
                 .isEqualTo(request.getClientDataHash());
-        assertThat(convertedSubclassRequest.allowHybrid()).isEqualTo(request.allowHybrid());
+        assertThat(convertedSubclassRequest.preferImmediatelyAvailableCredentials()).isEqualTo(
+                request.preferImmediatelyAvailableCredentials());
+        CreateCredentialRequest.DisplayInfo displayInfo =
+                convertedRequest.getDisplayInfo$credentials_debug();
+        assertThat(displayInfo.getUserDisplayName()).isEqualTo(TEST_USER_DISPLAYNAME);
+        assertThat(displayInfo.getUserId()).isEqualTo(TEST_USERNAME);
+        assertThat(displayInfo.getCredentialTypeIcon().getResId())
+                .isEqualTo(R.drawable.ic_passkey);
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedTest.kt
index d766191..63bee02 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedTest.kt
@@ -16,10 +16,15 @@
 
 package androidx.credentials
 
+import android.graphics.drawable.Icon
 import android.os.Bundle
+import android.os.Parcelable
 import androidx.credentials.CreateCredentialRequest.Companion.createFrom
+import androidx.credentials.internal.FrameworkImplHelper.Companion.getFinalCreateCredentialData
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -30,62 +35,82 @@
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class CreatePublicKeyCredentialRequestPrivilegedTest {
+    private val mContext = InstrumentationRegistry.getInstrumentation().context
+
+    companion object Constant {
+        private const val TEST_USERNAME = "test-user-name@gmail.com"
+        private const val TEST_USER_DISPLAYNAME = "Test User"
+        private const val TEST_REQUEST_JSON = "{\"rp\":{\"name\":true,\"id\":\"app-id\"}," +
+            "\"user\":{\"name\":\"$TEST_USERNAME\",\"id\":\"id-value\",\"displayName" +
+            "\":\"$TEST_USER_DISPLAYNAME\",\"icon\":true}, \"challenge\":true," +
+            "\"pubKeyCredParams\":true,\"excludeCredentials\":true," + "\"attestation\":true}"
+    }
 
     @Test
     fun constructor_success() {
         CreatePublicKeyCredentialRequestPrivileged(
-            "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
-            "RelyingParty", "ClientDataHash"
+            TEST_REQUEST_JSON, "RelyingParty", "ClientDataHash"
         )
     }
 
     @Test
-    fun constructor_setsAllowHybridToTrueByDefault() {
+    fun constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
         val createPublicKeyCredentialRequestPrivileged = CreatePublicKeyCredentialRequestPrivileged(
-            "JSON", "RelyingParty", "HASH"
+            TEST_REQUEST_JSON, "RelyingParty", "HASH"
         )
-        val allowHybridActual = createPublicKeyCredentialRequestPrivileged.allowHybrid
-        assertThat(allowHybridActual).isTrue()
+        val preferImmediatelyAvailableCredentialsActual =
+            createPublicKeyCredentialRequestPrivileged.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual).isFalse()
     }
 
     @Test
-    fun constructor_setsAllowHybridToFalse() {
-        val allowHybridExpected = false
+    fun constructor_setPreferImmediatelyAvailableCredentialsToTrue() {
+        val preferImmediatelyAvailableCredentialsExpected = true
         val createPublicKeyCredentialRequestPrivileged = CreatePublicKeyCredentialRequestPrivileged(
-            "testJson",
-            "RelyingParty", "Hash", allowHybridExpected
+            TEST_REQUEST_JSON,
+            "RelyingParty",
+            "Hash",
+            preferImmediatelyAvailableCredentialsExpected
         )
-        val allowHybridActual = createPublicKeyCredentialRequestPrivileged.allowHybrid
-        assertThat(allowHybridActual).isEqualTo(allowHybridExpected)
+        val preferImmediatelyAvailableCredentialsActual =
+            createPublicKeyCredentialRequestPrivileged.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+            preferImmediatelyAvailableCredentialsExpected
+        )
     }
 
     @Test
-    fun builder_build_defaultAllowHybrid_true() {
+    fun builder_build_defaultPreferImmediatelyAvailableCredentials_false() {
         val defaultPrivilegedRequest = CreatePublicKeyCredentialRequestPrivileged.Builder(
-            "{\"Data\":5}",
-            "RelyingParty", "HASH"
+            TEST_REQUEST_JSON, "RelyingParty", "HASH"
         ).build()
-        assertThat(defaultPrivilegedRequest.allowHybrid).isTrue()
+        assertThat(defaultPrivilegedRequest.preferImmediatelyAvailableCredentials).isFalse()
     }
 
     @Test
-    fun builder_build_nonDefaultAllowHybrid_false() {
-        val allowHybridExpected = false
+    fun builder_build_nonDefaultPreferImmediatelyAvailableCredentials_true() {
+        val preferImmediatelyAvailableCredentialsExpected = true
         val createPublicKeyCredentialRequestPrivileged = CreatePublicKeyCredentialRequestPrivileged
             .Builder(
-                "testJson",
-                "RelyingParty", "Hash"
-            ).setAllowHybrid(allowHybridExpected).build()
-        val allowHybridActual = createPublicKeyCredentialRequestPrivileged.allowHybrid
-        assertThat(allowHybridActual).isEqualTo(allowHybridExpected)
+                TEST_REQUEST_JSON, "RelyingParty", "Hash"
+            )
+            .setPreferImmediatelyAvailableCredentials(preferImmediatelyAvailableCredentialsExpected)
+            .build()
+        val preferImmediatelyAvailableCredentialsActual =
+            createPublicKeyCredentialRequestPrivileged.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+            preferImmediatelyAvailableCredentialsExpected
+        )
     }
 
     @Test
     fun getter_requestJson_success() {
-        val testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
+        val testJsonExpected = "{\"user\":{\"name\":{\"lol\":\"Value\"}}}"
         val createPublicKeyCredentialReqPriv =
-            CreatePublicKeyCredentialRequestPrivileged(testJsonExpected, "RelyingParty",
-                "HASH")
+            CreatePublicKeyCredentialRequestPrivileged(
+                testJsonExpected, "RelyingParty",
+                "HASH"
+            )
         val testJsonActual = createPublicKeyCredentialReqPriv.requestJson
         assertThat(testJsonActual).isEqualTo(testJsonExpected)
     }
@@ -95,8 +120,7 @@
         val testRelyingPartyExpected = "RelyingParty"
         val createPublicKeyCredentialRequestPrivileged =
             CreatePublicKeyCredentialRequestPrivileged(
-                "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
-                testRelyingPartyExpected, "X342%4dfd7&"
+                TEST_REQUEST_JSON, testRelyingPartyExpected, "X342%4dfd7&"
             )
         val testRelyingPartyActual = createPublicKeyCredentialRequestPrivileged.relyingParty
         assertThat(testRelyingPartyActual).isEqualTo(testRelyingPartyExpected)
@@ -107,62 +131,94 @@
         val clientDataHashExpected = "X342%4dfd7&"
         val createPublicKeyCredentialRequestPrivileged =
             CreatePublicKeyCredentialRequestPrivileged(
-                "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
-                "RelyingParty", clientDataHashExpected
+                TEST_REQUEST_JSON, "RelyingParty", clientDataHashExpected
             )
         val clientDataHashActual = createPublicKeyCredentialRequestPrivileged.clientDataHash
         assertThat(clientDataHashActual).isEqualTo(clientDataHashExpected)
     }
 
+    @SdkSuppress(minSdkVersion = 28)
+    @Suppress("DEPRECATION") // bundle.get(key)
     @Test
     fun getter_frameworkProperties_success() {
-        val requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
+        val requestJsonExpected = TEST_REQUEST_JSON
         val relyingPartyExpected = "RelyingParty"
         val clientDataHashExpected = "X342%4dfd7&"
-        val allowHybridExpected = false
+        val preferImmediatelyAvailableCredentialsExpected = false
         val expectedData = Bundle()
         expectedData.putString(
             PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
             CreatePublicKeyCredentialRequestPrivileged
-                .BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIVILEGED
+                .BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV
         )
         expectedData.putString(
             CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_REQUEST_JSON,
             requestJsonExpected
         )
-        expectedData.putString(CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_RELYING_PARTY,
-            relyingPartyExpected)
+        expectedData.putString(
+            CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_RELYING_PARTY,
+            relyingPartyExpected
+        )
         expectedData.putString(
             CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_CLIENT_DATA_HASH,
             clientDataHashExpected
         )
         expectedData.putBoolean(
-            CreatePublicKeyCredentialRequest.BUNDLE_KEY_ALLOW_HYBRID,
-            allowHybridExpected
+            CreatePublicKeyCredentialRequest.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+            preferImmediatelyAvailableCredentialsExpected
         )
 
         val request = CreatePublicKeyCredentialRequestPrivileged(
             requestJsonExpected,
             relyingPartyExpected,
             clientDataHashExpected,
-            allowHybridExpected
+            preferImmediatelyAvailableCredentialsExpected
         )
 
         assertThat(request.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
-        assertThat(equals(request.credentialData, expectedData)).isTrue()
         assertThat(equals(request.candidateQueryData, expectedData)).isTrue()
-        assertThat(request.requireSystemProvider).isFalse()
+        assertThat(request.isSystemProviderRequired).isFalse()
+        val credentialData = getFinalCreateCredentialData(
+            request, mContext
+        )
+        assertThat(credentialData.keySet())
+            .hasSize(expectedData.size() + /* added request info */1)
+        for (key in expectedData.keySet()) {
+            assertThat(credentialData[key]).isEqualTo(credentialData[key])
+        }
+        val displayInfoBundle = credentialData.getBundle(
+            CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_REQUEST_DISPLAY_INFO
+        )!!
+        assertThat(displayInfoBundle.keySet()).hasSize(3)
+        assertThat(
+            displayInfoBundle.getString(
+                CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_USER_ID
+            )
+        ).isEqualTo(TEST_USERNAME)
+        assertThat(
+            displayInfoBundle.getString(
+                CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_USER_DISPLAY_NAME
+            )
+        ).isEqualTo(TEST_USER_DISPLAYNAME)
+        assertThat(
+            (displayInfoBundle.getParcelable<Parcelable>(
+                CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_CREDENTIAL_TYPE_ICON
+            ) as Icon?)!!.resId
+        ).isEqualTo(R.drawable.ic_passkey)
     }
 
+    @SdkSuppress(minSdkVersion = 28)
     @Test
     fun frameworkConversion_success() {
         val request = CreatePublicKeyCredentialRequestPrivileged(
-            "json", "rp", "clientDataHash", true
+            TEST_REQUEST_JSON, "rp", "clientDataHash", true
         )
 
         val convertedRequest = createFrom(
-            request.type, request.credentialData,
-            request.candidateQueryData, request.requireSystemProvider
+            request.type, getFinalCreateCredentialData(
+                request, mContext
+            ),
+            request.candidateQueryData, request.isSystemProviderRequired
         )
 
         assertThat(convertedRequest).isInstanceOf(
@@ -174,6 +230,12 @@
         assertThat(convertedSubclassRequest.relyingParty).isEqualTo(request.relyingParty)
         assertThat(convertedSubclassRequest.clientDataHash)
             .isEqualTo(request.clientDataHash)
-        assertThat(convertedSubclassRequest.allowHybrid).isEqualTo(request.allowHybrid)
+        assertThat(convertedSubclassRequest.preferImmediatelyAvailableCredentials)
+            .isEqualTo(request.preferImmediatelyAvailableCredentials)
+        val displayInfo = convertedRequest.displayInfo
+        assertThat(displayInfo.userDisplayName).isEqualTo(TEST_USER_DISPLAYNAME)
+        assertThat(displayInfo.userId).isEqualTo(TEST_USERNAME)
+        assertThat(displayInfo.credentialTypeIcon?.resId)
+            .isEqualTo(R.drawable.ic_passkey)
     }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
index d133451..70a4f6b 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
@@ -16,68 +16,87 @@
 
 package androidx.credentials
 
+import android.graphics.drawable.Icon
 import android.os.Bundle
+import android.os.Parcelable
 import androidx.credentials.CreateCredentialRequest.Companion.createFrom
-import androidx.credentials.CreatePublicKeyCredentialRequest.Companion.BUNDLE_KEY_ALLOW_HYBRID
+import androidx.credentials.CreatePublicKeyCredentialRequest.Companion.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS
 import androidx.credentials.CreatePublicKeyCredentialRequest.Companion.BUNDLE_KEY_REQUEST_JSON
+import androidx.credentials.internal.FrameworkImplHelper.Companion.getFinalCreateCredentialData
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
 import com.google.common.truth.Truth.assertThat
-import org.junit.Assert
+import org.junit.Assert.assertThrows
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class CreatePublicKeyCredentialRequestTest {
+    private val mContext = InstrumentationRegistry.getInstrumentation().context
+    companion object Constant {
+        private const val TEST_USERNAME = "test-user-name@gmail.com"
+        private const val TEST_USER_DISPLAYNAME = "Test User"
+        private const val TEST_REQUEST_JSON = "{\"rp\":{\"name\":true,\"id\":\"app-id\"}," +
+            "\"user\":{\"name\":\"$TEST_USERNAME\",\"id\":\"id-value\",\"displayName" +
+            "\":\"$TEST_USER_DISPLAYNAME\",\"icon\":true}, \"challenge\":true," +
+            "\"pubKeyCredParams\":true,\"excludeCredentials\":true," + "\"attestation\":true}"
+    }
 
     @Test
     fun constructor_emptyJson_throwsIllegalArgumentException() {
-        Assert.assertThrows(
+        assertThrows(
             "Expected empty Json to throw error",
             IllegalArgumentException::class.java
         ) { CreatePublicKeyCredentialRequest("") }
     }
 
     @Test
-    fun constructor_success() {
-        CreatePublicKeyCredentialRequest(
-            "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
-        )
+    fun constructor_jsonMissingUserName_throwsIllegalArgumentException() {
+        assertThrows(
+            IllegalArgumentException::class.java
+        ) { CreatePublicKeyCredentialRequest("json") }
     }
 
     @Test
-    fun constructor_setsAllowHybridToTrueByDefault() {
+    fun constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
         val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest(
-            "JSON"
+            TEST_REQUEST_JSON
         )
-        val allowHybridActual = createPublicKeyCredentialRequest.allowHybrid
-        assertThat(allowHybridActual).isTrue()
+        val preferImmediatelyAvailableCredentialsActual =
+            createPublicKeyCredentialRequest.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual).isFalse()
     }
 
     @Test
-    fun constructor_setsAllowHybridToFalse() {
-        val allowHybridExpected = false
+    fun constructor_setPreferImmediatelyAvailableCredentialsToTrue() {
+        val preferImmediatelyAvailableCredentialsExpected = true
         val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest(
-            "testJson",
-            allowHybridExpected
+            TEST_REQUEST_JSON,
+            preferImmediatelyAvailableCredentialsExpected
         )
-        val allowHybridActual = createPublicKeyCredentialRequest.allowHybrid
-        assertThat(allowHybridActual).isEqualTo(allowHybridExpected)
+        val preferImmediatelyAvailableCredentialsActual =
+            createPublicKeyCredentialRequest.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual)
+            .isEqualTo(preferImmediatelyAvailableCredentialsExpected)
     }
 
     @Test
     fun getter_requestJson_success() {
-        val testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
+        val testJsonExpected = "{\"user\":{\"name\":{\"lol\":\"Value\"}}}"
         val createPublicKeyCredentialReq = CreatePublicKeyCredentialRequest(testJsonExpected)
         val testJsonActual = createPublicKeyCredentialReq.requestJson
         assertThat(testJsonActual).isEqualTo(testJsonExpected)
     }
 
+    @SdkSuppress(minSdkVersion = 28)
+    @Suppress("DEPRECATION") // bundle.get(key)
     @Test
     fun getter_frameworkProperties_success() {
-        val requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
-        val allowHybridExpected = false
+        val requestJsonExpected = TEST_REQUEST_JSON
+        val preferImmediatelyAvailableCredentialsExpected = false
         val expectedData = Bundle()
         expectedData.putString(
             PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
@@ -88,27 +107,57 @@
             BUNDLE_KEY_REQUEST_JSON, requestJsonExpected
         )
         expectedData.putBoolean(
-            BUNDLE_KEY_ALLOW_HYBRID, allowHybridExpected
+            BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+            preferImmediatelyAvailableCredentialsExpected
         )
 
         val request = CreatePublicKeyCredentialRequest(
             requestJsonExpected,
-            allowHybridExpected
+            preferImmediatelyAvailableCredentialsExpected
         )
 
         assertThat(request.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
-        assertThat(equals(request.credentialData, expectedData)).isTrue()
         assertThat(equals(request.candidateQueryData, expectedData)).isTrue()
-        assertThat(request.requireSystemProvider).isFalse()
+        assertThat(request.isSystemProviderRequired).isFalse()
+        val credentialData = getFinalCreateCredentialData(
+            request, mContext
+        )
+        assertThat(credentialData.keySet())
+            .hasSize(expectedData.size() + /* added request info */1)
+        for (key in expectedData.keySet()) {
+            assertThat(credentialData[key]).isEqualTo(credentialData[key])
+        }
+        val displayInfoBundle = credentialData.getBundle(
+            CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_REQUEST_DISPLAY_INFO
+        )!!
+        assertThat(displayInfoBundle.keySet()).hasSize(3)
+        assertThat(
+            displayInfoBundle.getString(
+                CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_USER_ID
+            )
+        ).isEqualTo(TEST_USERNAME)
+        assertThat(
+            displayInfoBundle.getString(
+                CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_USER_DISPLAY_NAME
+            )
+        ).isEqualTo(TEST_USER_DISPLAYNAME)
+        assertThat(
+            (displayInfoBundle.getParcelable<Parcelable>(
+                CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_CREDENTIAL_TYPE_ICON
+            ) as Icon?)!!.resId
+        ).isEqualTo(R.drawable.ic_passkey)
     }
 
+    @SdkSuppress(minSdkVersion = 28)
     @Test
     fun frameworkConversion_success() {
-        val request = CreatePublicKeyCredentialRequest("json", true)
+        val request = CreatePublicKeyCredentialRequest(TEST_REQUEST_JSON, true)
 
         val convertedRequest = createFrom(
-            request.type, request.credentialData,
-            request.candidateQueryData, request.requireSystemProvider
+            request.type, getFinalCreateCredentialData(
+                request, mContext
+            ),
+            request.candidateQueryData, request.isSystemProviderRequired
         )
 
         assertThat(convertedRequest).isInstanceOf(
@@ -116,6 +165,12 @@
         )
         val convertedSubclassRequest = convertedRequest as CreatePublicKeyCredentialRequest
         assertThat(convertedSubclassRequest.requestJson).isEqualTo(request.requestJson)
-        assertThat(convertedSubclassRequest.allowHybrid).isEqualTo(request.allowHybrid)
+        assertThat(convertedSubclassRequest.preferImmediatelyAvailableCredentials)
+            .isEqualTo(request.preferImmediatelyAvailableCredentials)
+        val displayInfo = convertedRequest.displayInfo
+        assertThat(displayInfo.userDisplayName).isEqualTo(TEST_USER_DISPLAYNAME)
+        assertThat(displayInfo.userId).isEqualTo(TEST_USERNAME)
+        assertThat(displayInfo.credentialTypeIcon?.resId)
+            .isEqualTo(R.drawable.ic_passkey)
     }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerJavaTest.java
index d01bf20..85246a1 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerJavaTest.java
@@ -16,6 +16,8 @@
 
 package androidx.credentials;
 
+import static androidx.credentials.TestUtilsKt.isPostFrameworkApiLevel;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.app.Activity;
@@ -29,6 +31,7 @@
 import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException;
 import androidx.credentials.exceptions.GetCredentialException;
 import androidx.credentials.exceptions.GetCredentialProviderConfigurationException;
+import androidx.test.core.app.ActivityScenario;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -37,6 +40,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 
 @RunWith(AndroidJUnit4.class)
@@ -53,39 +58,52 @@
     }
 
     @Test
-    public void testCreateCredentialAsyc_successCallbackThrows() {
+    public void testCreateCredentialAsyc_successCallbackThrows() throws InterruptedException {
         if (Looper.myLooper() == null) {
             Looper.prepare();
         }
+        CountDownLatch latch = new CountDownLatch(1);
         AtomicReference<CreateCredentialException> loadedResult = new AtomicReference<>();
-        mCredentialManager.executeCreateCredentialAsync(
-                new CreatePasswordRequest("test-user-id", "test-password"),
-                new Activity(),
-                null,
-                Runnable::run,
-                new CredentialManagerCallback<CreateCredentialResponse,
-                        CreateCredentialException>() {
-                    @Override
-                    public void onError(@NonNull CreateCredentialException e) {
-                        loadedResult.set(e);
-                    }
-                    @Override
-                    public void onResult(@NonNull CreateCredentialResponse result) {}
-            });
+        ActivityScenario<TestActivity> activityScenario =
+                ActivityScenario.launch(TestActivity.class);
+        activityScenario.onActivity(activity -> {
+            mCredentialManager.createCredentialAsync(
+                    new CreatePasswordRequest("test-user-id", "test-password"),
+                    activity,
+                    null,
+                    Runnable::run,
+                    new CredentialManagerCallback<CreateCredentialResponse,
+                            CreateCredentialException>() {
+                        @Override
+                        public void onError(@NonNull CreateCredentialException e) {
+                            loadedResult.set(e);
+                            latch.countDown();
+                        }
+
+                        @Override
+                        public void onResult(@NonNull CreateCredentialResponse result) {}
+                    });
+        });
+
+        latch.await(100L, TimeUnit.MILLISECONDS);
         assertThat(loadedResult.get().getClass()).isEqualTo(
                 CreateCredentialProviderConfigurationException.class);
-        // TODO("Add manifest tests and separate tests for pre and post U API Levels")
+        // TODO("Add manifest tests and possibly further separate these tests by API Level
+        //  - maybe a rule perhaps?")
     }
 
+
     @Test
-    public void testGetCredentialAsyc_successCallbackThrows() {
+    public void testGetCredentialAsyc_successCallbackThrows() throws InterruptedException {
         if (Looper.myLooper() == null) {
             Looper.prepare();
         }
+        CountDownLatch latch = new CountDownLatch(1);
         AtomicReference<GetCredentialException> loadedResult = new AtomicReference<>();
-        mCredentialManager.executeGetCredentialAsync(
+
+        mCredentialManager.getCredentialAsync(
                 new GetCredentialRequest.Builder()
-                        .addGetCredentialOption(new GetPasswordOption())
+                        .addCredentialOption(new GetPasswordOption())
                         .build(),
                 new Activity(),
                 null,
@@ -95,21 +113,26 @@
                 @Override
                 public void onError(@NonNull GetCredentialException e) {
                     loadedResult.set(e);
+                    latch.countDown();
                 }
 
                 @Override
                 public void onResult(@NonNull GetCredentialResponse result) {}
             });
+
+        latch.await(100L, TimeUnit.MILLISECONDS);
         assertThat(loadedResult.get().getClass()).isEqualTo(
                 GetCredentialProviderConfigurationException.class);
-        // TODO("Add manifest tests and separate tests for pre and post U API Levels")
+        // TODO("Add manifest tests and possibly further separate these tests - maybe a rule
+        //  perhaps?")
     }
 
     @Test
-    public void testClearCredentialSessionAsync_throws() {
-        if (Looper.myLooper() == null) {
-            Looper.prepare();
+    public void testClearCredentialSessionAsync_throws() throws InterruptedException {
+        if (isPostFrameworkApiLevel()) {
+            return; // TODO(Support!)
         }
+        CountDownLatch latch = new CountDownLatch(1);
         AtomicReference<ClearCredentialException> loadedResult = new AtomicReference<>();
 
         mCredentialManager.clearCredentialStateAsync(
@@ -121,13 +144,16 @@
                     @Override
                     public void onError(@NonNull ClearCredentialException e) {
                         loadedResult.set(e);
+                        latch.countDown();
                     }
 
                     @Override
                     public void onResult(@NonNull Void result) {}
                 });
+
+        latch.await(100L, TimeUnit.MILLISECONDS);
         assertThat(loadedResult.get().getClass()).isEqualTo(
                 ClearCredentialProviderConfigurationException.class);
-        // TODO(Add manifest tests and separate tests for pre and post U API Levels")
+        // TODO(Add manifest tests and split this once postU is implemented for clearCreds")
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerTest.kt
index ef0cebf..162d6df 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerTest.kt
@@ -24,12 +24,15 @@
 import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException
 import androidx.credentials.exceptions.GetCredentialException
 import androidx.credentials.exceptions.GetCredentialProviderConfigurationException
+import androidx.test.core.app.ActivityScenario
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.testutils.assertThrows
 import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
 import java.util.concurrent.Executor
+import java.util.concurrent.TimeUnit
 import java.util.concurrent.atomic.AtomicReference
 import kotlinx.coroutines.runBlocking
 import org.junit.Before
@@ -53,13 +56,16 @@
         if (Looper.myLooper() == null) {
             Looper.prepare()
         }
-        assertThrows<CreateCredentialProviderConfigurationException> {
-            credentialManager.executeCreateCredential(
-                CreatePasswordRequest("test-user-id", "test-password"),
-                Activity()
-            )
+        if (!isPostFrameworkApiLevel()) {
+            assertThrows<CreateCredentialProviderConfigurationException> {
+                credentialManager.createCredential(
+                    CreatePasswordRequest("test-user-id", "test-password"),
+                    Activity()
+                )
+            }
         }
-        // TODO(Add manifest tests and separate tests for pre and post U API Levels")
+        // TODO("Add manifest tests and possibly further separate these tests by API Level
+        //  - maybe a rule perhaps?")
     }
 
     @Test
@@ -68,12 +74,16 @@
             Looper.prepare()
         }
         val request = GetCredentialRequest.Builder()
-            .addGetCredentialOption(GetPasswordOption())
+            .addCredentialOption(GetPasswordOption())
             .build()
-        assertThrows<GetCredentialProviderConfigurationException> {
-            credentialManager.executeGetCredential(request, Activity())
+
+        if (!isPostFrameworkApiLevel()) {
+            assertThrows<GetCredentialProviderConfigurationException> {
+                credentialManager.getCredential(request, Activity())
+            }
         }
-        // TODO(Add manifest tests and separate tests for pre and post U API Levels")
+        // TODO("Add manifest tests and possibly further separate these tests by API Level
+        //  - maybe a rule perhaps?")
     }
 
     @Test
@@ -81,10 +91,14 @@
         if (Looper.myLooper() == null) {
             Looper.prepare()
         }
-        assertThrows<ClearCredentialProviderConfigurationException> {
-            credentialManager.clearCredentialState(ClearCredentialStateRequest())
+
+        if (!isPostFrameworkApiLevel()) {
+            assertThrows<ClearCredentialProviderConfigurationException> {
+                credentialManager.clearCredentialState(ClearCredentialStateRequest())
+            }
         }
-        // TODO(Add manifest tests and separate tests for pre and post U API Levels")
+        // TODO("Add manifest tests and possibly further separate these tests by API Level
+        //  - maybe a rule perhaps?")
     }
 
     @Test
@@ -92,22 +106,35 @@
         if (Looper.myLooper() == null) {
             Looper.prepare()
         }
+        val latch = CountDownLatch(1)
         val loadedResult: AtomicReference<CreateCredentialException> = AtomicReference()
-        credentialManager.executeCreateCredentialAsync(
-            request = CreatePasswordRequest("test-user-id", "test-password"),
-            activity = Activity(),
-            cancellationSignal = null,
-            executor = Runnable::run,
-            callback = object : CredentialManagerCallback<CreateCredentialResponse,
-                CreateCredentialException> {
-                override fun onResult(result: CreateCredentialResponse) {}
-                override fun onError(e: CreateCredentialException) { loadedResult.set(e) }
-            }
+        val activityScenario = ActivityScenario.launch(
+            TestActivity::class.java
         )
-        assertThat(loadedResult.get().type).isEqualTo(
-            CreateCredentialProviderConfigurationException
-            .TYPE_CREATE_CREDENTIAL_PROVIDER_CONFIGURATION_EXCEPTION)
-        // TODO(Add manifest tests and separate tests for pre and post U API Levels")
+
+        activityScenario.onActivity { activity ->
+            credentialManager.createCredentialAsync(
+                CreatePasswordRequest("test-user-id", "test-password"),
+                activity,
+                null, Executor { obj: Runnable -> obj.run() },
+                object : CredentialManagerCallback<CreateCredentialResponse,
+                    CreateCredentialException> {
+                    override fun onResult(result: CreateCredentialResponse) {}
+                    override fun onError(e: CreateCredentialException) {
+                        loadedResult.set(e)
+                        latch.countDown()
+                    }
+                })
+        }
+
+        latch.await(100L, TimeUnit.MILLISECONDS)
+        if (!isPostFrameworkApiLevel()) {
+            assertThat(loadedResult.get().javaClass).isEqualTo(
+                CreateCredentialProviderConfigurationException::class.java
+            )
+        }
+        // TODO("Add manifest tests and possibly further separate these tests by API Level
+        //  - maybe a rule perhaps?")
     }
 
     @Test
@@ -115,10 +142,12 @@
         if (Looper.myLooper() == null) {
             Looper.prepare()
         }
+        val latch = CountDownLatch(1)
         val loadedResult: AtomicReference<GetCredentialException> = AtomicReference()
-        credentialManager.executeGetCredentialAsync(
+
+        credentialManager.getCredentialAsync(
             request = GetCredentialRequest.Builder()
-                .addGetCredentialOption(GetPasswordOption())
+                .addCredentialOption(GetPasswordOption())
                 .build(),
             activity = Activity(),
             cancellationSignal = null,
@@ -126,13 +155,21 @@
             callback = object : CredentialManagerCallback<GetCredentialResponse,
                 GetCredentialException> {
                 override fun onResult(result: GetCredentialResponse) {}
-                override fun onError(e: GetCredentialException) { loadedResult.set(e) }
+                override fun onError(e: GetCredentialException) {
+                    loadedResult.set(e)
+                    latch.countDown()
+                }
             }
         )
-        assertThat(loadedResult.get().type).isEqualTo(
-            GetCredentialProviderConfigurationException
-            .TYPE_GET_CREDENTIAL_PROVIDER_CONFIGURATION_EXCEPTION)
-        // TODO(Add manifest tests and separate tests for pre and post U API Levels")
+
+        latch.await(100L, TimeUnit.MILLISECONDS)
+        if (!isPostFrameworkApiLevel()) {
+            assertThat(loadedResult.get().javaClass).isEqualTo(
+                GetCredentialProviderConfigurationException::class.java
+            )
+        }
+        // TODO("Add manifest tests and possibly further separate these tests - maybe a rule
+        //  perhaps?")
     }
 
     @Test
@@ -140,19 +177,27 @@
         if (Looper.myLooper() == null) {
             Looper.prepare()
         }
+        if (isPostFrameworkApiLevel()) {
+            return // TODO(Support!)
+        }
+        val latch = CountDownLatch(1)
         val loadedResult: AtomicReference<ClearCredentialException> = AtomicReference()
 
         credentialManager.clearCredentialStateAsync(
             ClearCredentialStateRequest(),
             null, Executor { obj: Runnable -> obj.run() },
             object : CredentialManagerCallback<Void?, ClearCredentialException> {
-                override fun onError(e: ClearCredentialException) { loadedResult.set(e) }
+                override fun onError(e: ClearCredentialException) {
+                    loadedResult.set(e)
+                    latch.countDown()
+                }
                 override fun onResult(result: Void?) {}
             })
 
+        latch.await(100L, TimeUnit.MILLISECONDS)
         assertThat(loadedResult.get().type).isEqualTo(
             ClearCredentialProviderConfigurationException
             .TYPE_CLEAR_CREDENTIAL_PROVIDER_CONFIGURATION_EXCEPTION)
-        // TODO(Add manifest tests and separate tests for pre and post U API Levels")
+        // TODO(Add manifest tests and split this once postU is implemented for clearCreds")
     }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestJavaTest.java
new file mode 100644
index 0000000..58db045
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestJavaTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.credentials;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class GetCredentialRequestJavaTest {
+    @Test
+    public void constructor_emptyCredentialOptions_throws() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new GetCredentialRequest(new ArrayList<>()));
+
+    }
+
+    @Test
+    public void constructor() {
+        boolean expectedIsAutoSelectAllowed = true;
+        ArrayList<CredentialOption> expectedCredentialOptions = new ArrayList<>();
+        expectedCredentialOptions.add(new GetPasswordOption());
+        expectedCredentialOptions.add(new GetPublicKeyCredentialOption("json"));
+
+        GetCredentialRequest request = new GetCredentialRequest(expectedCredentialOptions,
+                expectedIsAutoSelectAllowed);
+
+        assertThat(request.isAutoSelectAllowed()).isEqualTo(expectedIsAutoSelectAllowed);
+        assertThat(request.getCredentialOptions()).hasSize(expectedCredentialOptions.size());
+        for (int i = 0; i < expectedCredentialOptions.size(); i++) {
+            assertThat(request.getCredentialOptions().get(i)).isEqualTo(
+                    expectedCredentialOptions.get(i));
+        }
+    }
+
+    @Test
+    public void constructor_defaultAutoSelect() {
+        ArrayList<CredentialOption> options = new ArrayList<>();
+        options.add(new GetPasswordOption());
+
+        GetCredentialRequest request = new GetCredentialRequest(options);
+
+        assertThat(request.isAutoSelectAllowed()).isFalse();
+    }
+
+    @Test
+    public void builder_addCredentialOption() {
+        boolean expectedIsAutoSelectAllowed = true;
+        ArrayList<CredentialOption> expectedCredentialOptions = new ArrayList<>();
+        expectedCredentialOptions.add(new GetPasswordOption());
+        expectedCredentialOptions.add(new GetPublicKeyCredentialOption("json"));
+
+        GetCredentialRequest request = new GetCredentialRequest.Builder()
+                .addCredentialOption(expectedCredentialOptions.get(0))
+                .addCredentialOption(expectedCredentialOptions.get(1))
+                .setAutoSelectAllowed(expectedIsAutoSelectAllowed)
+                .build();
+
+        assertThat(request.isAutoSelectAllowed()).isEqualTo(expectedIsAutoSelectAllowed);
+        assertThat(request.getCredentialOptions()).hasSize(expectedCredentialOptions.size());
+        for (int i = 0; i < expectedCredentialOptions.size(); i++) {
+            assertThat(request.getCredentialOptions().get(i)).isEqualTo(
+                    expectedCredentialOptions.get(i));
+        }
+    }
+
+    @Test
+    public void builder_setCredentialOptions() {
+        boolean expectedIsAutoSelectAllowed = true;
+        ArrayList<CredentialOption> expectedCredentialOptions = new ArrayList<>();
+        expectedCredentialOptions.add(new GetPasswordOption());
+        expectedCredentialOptions.add(new GetPublicKeyCredentialOption("json"));
+
+        GetCredentialRequest request = new GetCredentialRequest.Builder()
+                .setCredentialOptions(expectedCredentialOptions)
+                .setAutoSelectAllowed(expectedIsAutoSelectAllowed)
+                .build();
+
+        assertThat(request.isAutoSelectAllowed()).isEqualTo(expectedIsAutoSelectAllowed);
+        assertThat(request.getCredentialOptions()).hasSize(expectedCredentialOptions.size());
+        for (int i = 0; i < expectedCredentialOptions.size(); i++) {
+            assertThat(request.getCredentialOptions().get(i)).isEqualTo(
+                    expectedCredentialOptions.get(i));
+        }
+    }
+
+    @Test
+    public void builder_defaultAutoSelect() {
+        GetCredentialRequest request = new GetCredentialRequest.Builder()
+                .addCredentialOption(new GetPasswordOption())
+                .build();
+
+        assertThat(request.isAutoSelectAllowed()).isFalse();
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt
new file mode 100644
index 0000000..45eed29
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.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.credentials
+
+import com.google.common.truth.Truth.assertThat
+
+import org.junit.Assert.assertThrows
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class GetCredentialRequestTest {
+
+    @Test
+    fun constructor_emptyCredentialOptions_throws() {
+        assertThrows(
+            IllegalArgumentException::class.java
+        ) { GetCredentialRequest(ArrayList()) }
+    }
+
+    @Test
+    fun constructor() {
+        val expectedIsAutoSelectAllowed = true
+        val expectedCredentialOptions = ArrayList<CredentialOption>()
+        expectedCredentialOptions.add(GetPasswordOption())
+        expectedCredentialOptions.add(GetPublicKeyCredentialOption("json"))
+
+        val request = GetCredentialRequest(
+            expectedCredentialOptions,
+            expectedIsAutoSelectAllowed
+        )
+
+        assertThat(request.isAutoSelectAllowed).isEqualTo(expectedIsAutoSelectAllowed)
+        assertThat(request.credentialOptions).hasSize(expectedCredentialOptions.size)
+        for (i in expectedCredentialOptions.indices) {
+            assertThat(request.credentialOptions[i]).isEqualTo(
+                expectedCredentialOptions[i]
+            )
+        }
+    }
+
+    @Test
+    fun constructor_defaultAutoSelect() {
+        val options = ArrayList<CredentialOption>()
+        options.add(GetPasswordOption())
+
+        val request = GetCredentialRequest(options)
+
+        assertThat(request.isAutoSelectAllowed).isFalse()
+    }
+
+    @Test
+    fun builder_addCredentialOption() {
+        val expectedIsAutoSelectAllowed = true
+        val expectedCredentialOptions = ArrayList<CredentialOption>()
+        expectedCredentialOptions.add(GetPasswordOption())
+        expectedCredentialOptions.add(GetPublicKeyCredentialOption("json"))
+
+        val request = GetCredentialRequest.Builder()
+            .addCredentialOption(expectedCredentialOptions[0])
+            .addCredentialOption(expectedCredentialOptions[1])
+            .setAutoSelectAllowed(expectedIsAutoSelectAllowed)
+            .build()
+
+        assertThat(request.isAutoSelectAllowed).isEqualTo(expectedIsAutoSelectAllowed)
+        assertThat(request.credentialOptions).hasSize(expectedCredentialOptions.size)
+        for (i in expectedCredentialOptions.indices) {
+            assertThat(request.credentialOptions[i]).isEqualTo(
+                expectedCredentialOptions[i]
+            )
+        }
+    }
+
+    @Test
+    fun builder_setCredentialOptions() {
+        val expectedIsAutoSelectAllowed = true
+        val expectedCredentialOptions = ArrayList<CredentialOption>()
+        expectedCredentialOptions.add(GetPasswordOption())
+        expectedCredentialOptions.add(GetPublicKeyCredentialOption("json"))
+
+        val request = GetCredentialRequest.Builder()
+            .setCredentialOptions(expectedCredentialOptions)
+            .setAutoSelectAllowed(expectedIsAutoSelectAllowed)
+            .build()
+
+        assertThat(request.isAutoSelectAllowed).isEqualTo(expectedIsAutoSelectAllowed)
+        assertThat(request.credentialOptions).hasSize(expectedCredentialOptions.size)
+        for (i in expectedCredentialOptions.indices) {
+            assertThat(request.credentialOptions[i]).isEqualTo(
+                expectedCredentialOptions[i]
+            )
+        }
+    }
+
+    @Test
+    fun builder_defaultAutoSelect() {
+        val request = GetCredentialRequest.Builder()
+            .addCredentialOption(GetPasswordOption())
+            .build()
+
+        assertThat(request.isAutoSelectAllowed).isFalse()
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionJavaTest.java
index 3b02d52..24893d9 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionJavaTest.java
@@ -79,6 +79,6 @@
         assertThat(TestUtilsKt.equals(option.getRequestData(), expectedBundle)).isTrue();
         assertThat(TestUtilsKt.equals(option.getCandidateQueryData(),
                 expectedCandidateQueryDataBundle)).isTrue();
-        assertThat(option.requireSystemProvider()).isEqualTo(expectedSystemProvider);
+        assertThat(option.isSystemProviderRequired()).isEqualTo(expectedSystemProvider);
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionTest.kt
index d6f3ea8..d33cc62 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionTest.kt
@@ -72,6 +72,6 @@
                 expectedCandidateQueryDataBundle
             )
         ).isTrue()
-        assertThat(option.requireSystemProvider).isEqualTo(expectedSystemProvider)
+        assertThat(option.isSystemProviderRequired).isEqualTo(expectedSystemProvider)
     }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionJavaTest.java
index 80dfcec..dcfc8dff 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionJavaTest.java
@@ -36,16 +36,16 @@
         assertThat(option.getType()).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL);
         assertThat(TestUtilsKt.equals(option.getRequestData(), Bundle.EMPTY)).isTrue();
         assertThat(TestUtilsKt.equals(option.getRequestData(), Bundle.EMPTY)).isTrue();
-        assertThat(option.getRequireSystemProvider()).isFalse();
+        assertThat(option.isSystemProviderRequired()).isFalse();
     }
 
     @Test
     public void frameworkConversion_success() {
         GetPasswordOption option = new GetPasswordOption();
 
-        GetCredentialOption convertedOption = GetCredentialOption.createFrom(
+        CredentialOption convertedOption = CredentialOption.createFrom(
                 option.getType(), option.getRequestData(), option.getCandidateQueryData(),
-                option.getRequireSystemProvider());
+                option.isSystemProviderRequired());
 
         assertThat(convertedOption).isInstanceOf(GetPasswordOption.class);
     }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionTest.kt
index 7b3201a..e9337ab 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionTest.kt
@@ -17,7 +17,7 @@
 package androidx.credentials
 
 import android.os.Bundle
-import androidx.credentials.GetCredentialOption.Companion.createFrom
+import androidx.credentials.CredentialOption.Companion.createFrom
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -34,7 +34,7 @@
         assertThat(option.type).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL)
         assertThat(equals(option.requestData, Bundle.EMPTY)).isTrue()
         assertThat(equals(option.requestData, Bundle.EMPTY)).isTrue()
-        assertThat(option.requireSystemProvider).isFalse()
+        assertThat(option.isSystemProviderRequired).isFalse()
     }
 
     @Test
@@ -42,7 +42,10 @@
         val option = GetPasswordOption()
 
         val convertedOption = createFrom(
-            option.type, option.requestData, option.candidateQueryData, option.requireSystemProvider
+            option.type,
+            option.requestData,
+            option.candidateQueryData,
+            option.isSystemProviderRequired
         )
 
         assertThat(convertedOption).isInstanceOf(GetPasswordOption::class.java)
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java
index dad1fe3..338baf0 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java
@@ -16,7 +16,7 @@
 
 package androidx.credentials;
 
-import static androidx.credentials.GetPublicKeyCredentialOption.BUNDLE_KEY_ALLOW_HYBRID;
+import static androidx.credentials.GetPublicKeyCredentialOption.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS;
 import static androidx.credentials.GetPublicKeyCredentialOption.BUNDLE_KEY_REQUEST_JSON;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -53,28 +53,31 @@
     }
 
     @Test
-    public void constructor_success()  {
+    public void constructor_success() {
         new GetPublicKeyCredentialOption(
-                        "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}");
+                "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}");
     }
 
     @Test
-    public void constructor_setsAllowHybridToTrueByDefault() {
+    public void constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
         GetPublicKeyCredentialOption getPublicKeyCredentialOpt =
                 new GetPublicKeyCredentialOption(
                         "JSON");
-        boolean allowHybridActual = getPublicKeyCredentialOpt.allowHybrid();
-        assertThat(allowHybridActual).isTrue();
+        boolean preferImmediatelyAvailableCredentialsActual =
+                getPublicKeyCredentialOpt.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isFalse();
     }
 
     @Test
-    public void constructor_setsAllowHybridToFalse() {
-        boolean allowHybridExpected = false;
+    public void constructor_setPreferImmediatelyAvailableCredentialsToTrue() {
+        boolean preferImmediatelyAvailableCredentialsExpected = true;
         GetPublicKeyCredentialOption getPublicKeyCredentialOpt =
                 new GetPublicKeyCredentialOption(
-                        "JSON", allowHybridExpected);
-        boolean allowHybridActual = getPublicKeyCredentialOpt.allowHybrid();
-        assertThat(allowHybridActual).isEqualTo(allowHybridExpected);
+                        "JSON", preferImmediatelyAvailableCredentialsExpected);
+        boolean preferImmediatelyAvailableCredentialsActual =
+                getPublicKeyCredentialOpt.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+                preferImmediatelyAvailableCredentialsExpected);
     }
 
     @Test
@@ -89,21 +92,23 @@
     @Test
     public void getter_frameworkProperties_success() {
         String requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
-        boolean allowHybridExpected = false;
+        boolean preferImmediatelyAvailableCredentialsExpected = false;
         Bundle expectedData = new Bundle();
         expectedData.putString(
                 PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
                 GetPublicKeyCredentialOption.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION);
         expectedData.putString(BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
-        expectedData.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybridExpected);
+        expectedData.putBoolean(
+                BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+                preferImmediatelyAvailableCredentialsExpected);
 
-        GetPublicKeyCredentialOption option =
-                new GetPublicKeyCredentialOption(requestJsonExpected, allowHybridExpected);
+        GetPublicKeyCredentialOption option = new GetPublicKeyCredentialOption(
+                requestJsonExpected, preferImmediatelyAvailableCredentialsExpected);
 
         assertThat(option.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
         assertThat(TestUtilsKt.equals(option.getRequestData(), expectedData)).isTrue();
         assertThat(TestUtilsKt.equals(option.getCandidateQueryData(), expectedData)).isTrue();
-        assertThat(option.getRequireSystemProvider()).isFalse();
+        assertThat(option.isSystemProviderRequired()).isFalse();
     }
 
     @Test
@@ -111,14 +116,15 @@
         GetPublicKeyCredentialOption option =
                 new GetPublicKeyCredentialOption("json", true);
 
-        GetCredentialOption convertedOption = GetCredentialOption.createFrom(
+        CredentialOption convertedOption = CredentialOption.createFrom(
                 option.getType(), option.getRequestData(),
-                option.getCandidateQueryData(), option.getRequireSystemProvider());
+                option.getCandidateQueryData(), option.isSystemProviderRequired());
 
         assertThat(convertedOption).isInstanceOf(GetPublicKeyCredentialOption.class);
         GetPublicKeyCredentialOption convertedSubclassOption =
                 (GetPublicKeyCredentialOption) convertedOption;
         assertThat(convertedSubclassOption.getRequestJson()).isEqualTo(option.getRequestJson());
-        assertThat(convertedSubclassOption.allowHybrid()).isEqualTo(option.allowHybrid());
+        assertThat(convertedSubclassOption.preferImmediatelyAvailableCredentials()).isEqualTo(
+                option.preferImmediatelyAvailableCredentials());
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedJavaTest.java
index 760eead..d7e694c 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedJavaTest.java
@@ -16,8 +16,8 @@
 
 package androidx.credentials;
 
-import static androidx.credentials.GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_ALLOW_HYBRID;
 import static androidx.credentials.GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_CLIENT_DATA_HASH;
+import static androidx.credentials.GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS;
 import static androidx.credentials.GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_RELYING_PARTY;
 import static androidx.credentials.GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_REQUEST_JSON;
 
@@ -42,46 +42,56 @@
     @Test
     public void constructor_success() {
         new GetPublicKeyCredentialOptionPrivileged(
-                        "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
-                        "RelyingParty", "ClientDataHash");
+                "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
+                "RelyingParty", "ClientDataHash");
     }
 
     @Test
-    public void constructor_setsAllowHybridToTrueByDefault() {
+    public void constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
         GetPublicKeyCredentialOptionPrivileged getPublicKeyCredentialOptionPrivileged =
                 new GetPublicKeyCredentialOptionPrivileged(
                         "JSON", "RelyingParty", "HASH");
-        boolean allowHybridActual = getPublicKeyCredentialOptionPrivileged.allowHybrid();
-        assertThat(allowHybridActual).isTrue();
+        boolean preferImmediatelyAvailableCredentialsActual =
+                getPublicKeyCredentialOptionPrivileged.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isFalse();
     }
 
     @Test
-    public void constructor_setsAllowHybridToFalse() {
-        boolean allowHybridExpected = false;
+    public void constructor_setPreferImmediatelyAvailableCredentialsTrue() {
+        boolean preferImmediatelyAvailableCredentialsExpected = true;
         GetPublicKeyCredentialOptionPrivileged getPublicKeyCredentialOptionPrivileged =
-                new GetPublicKeyCredentialOptionPrivileged("testJson",
-                        "RelyingParty", "Hash", allowHybridExpected);
-        boolean getAllowHybridActual = getPublicKeyCredentialOptionPrivileged.allowHybrid();
-        assertThat(getAllowHybridActual).isEqualTo(allowHybridExpected);
+                new GetPublicKeyCredentialOptionPrivileged(
+                        "testJson",
+                        "RelyingParty",
+                        "Hash",
+                        preferImmediatelyAvailableCredentialsExpected
+                );
+        boolean preferImmediatelyAvailableCredentialsActual =
+                getPublicKeyCredentialOptionPrivileged.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+                preferImmediatelyAvailableCredentialsExpected);
     }
 
     @Test
-    public void builder_build_defaultAllowHybrid_success() {
+    public void builder_build_defaultPreferImmediatelyAvailableCredentials_success() {
         GetPublicKeyCredentialOptionPrivileged defaultPrivilegedRequest = new
                 GetPublicKeyCredentialOptionPrivileged.Builder("{\"Data\":5}",
                 "RelyingParty", "HASH").build();
-        assertThat(defaultPrivilegedRequest.allowHybrid()).isTrue();
+        assertThat(defaultPrivilegedRequest.preferImmediatelyAvailableCredentials()).isFalse();
     }
 
     @Test
-    public void builder_build_nonDefaultAllowHybrid_success() {
-        boolean allowHybridExpected = false;
+    public void builder_build_nonDefaultPreferImmediatelyAvailableCredentials_success() {
+        boolean preferImmediatelyAvailableCredentialsExpected = true;
         GetPublicKeyCredentialOptionPrivileged getPublicKeyCredentialOptionPrivileged =
                 new GetPublicKeyCredentialOptionPrivileged.Builder("testJson",
                         "RelyingParty", "Hash")
-                        .setAllowHybrid(allowHybridExpected).build();
-        boolean getAllowHybridActual = getPublicKeyCredentialOptionPrivileged.allowHybrid();
-        assertThat(getAllowHybridActual).isEqualTo(allowHybridExpected);
+                        .setPreferImmediatelyAvailableCredentials(
+                                preferImmediatelyAvailableCredentialsExpected).build();
+        boolean preferImmediatelyAvailableCredentialsActual =
+                getPublicKeyCredentialOptionPrivileged.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+                preferImmediatelyAvailableCredentialsExpected);
     }
 
     @Test
@@ -121,7 +131,7 @@
         String requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
         String relyingPartyExpected = "RelyingParty";
         String clientDataHashExpected = "X342%4dfd7&";
-        boolean allowHybridExpected = false;
+        boolean preferImmediatelyAvailableCredentialsExpected = false;
         Bundle expectedData = new Bundle();
         expectedData.putString(
                 PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
@@ -130,17 +140,19 @@
         expectedData.putString(BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
         expectedData.putString(BUNDLE_KEY_RELYING_PARTY, relyingPartyExpected);
         expectedData.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHashExpected);
-        expectedData.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybridExpected);
+        expectedData.putBoolean(
+                BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+                preferImmediatelyAvailableCredentialsExpected);
 
         GetPublicKeyCredentialOptionPrivileged option =
                 new GetPublicKeyCredentialOptionPrivileged(
                         requestJsonExpected, relyingPartyExpected, clientDataHashExpected,
-                        allowHybridExpected);
+                        preferImmediatelyAvailableCredentialsExpected);
 
         assertThat(option.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
         assertThat(TestUtilsKt.equals(option.getRequestData(), expectedData)).isTrue();
         assertThat(TestUtilsKt.equals(option.getCandidateQueryData(), expectedData)).isTrue();
-        assertThat(option.getRequireSystemProvider()).isFalse();
+        assertThat(option.isSystemProviderRequired()).isFalse();
     }
 
     @Test
@@ -148,15 +160,16 @@
         GetPublicKeyCredentialOptionPrivileged option =
                 new GetPublicKeyCredentialOptionPrivileged("json", "rp", "clientDataHash", true);
 
-        GetCredentialOption convertedOption = GetCredentialOption.createFrom(
+        CredentialOption convertedOption = CredentialOption.createFrom(
                 option.getType(), option.getRequestData(),
-                option.getCandidateQueryData(), option.getRequireSystemProvider());
+                option.getCandidateQueryData(), option.isSystemProviderRequired());
 
         assertThat(convertedOption).isInstanceOf(GetPublicKeyCredentialOptionPrivileged.class);
         GetPublicKeyCredentialOptionPrivileged convertedSubclassOption =
                 (GetPublicKeyCredentialOptionPrivileged) convertedOption;
         assertThat(convertedSubclassOption.getRequestJson()).isEqualTo(option.getRequestJson());
-        assertThat(convertedSubclassOption.allowHybrid()).isEqualTo(option.allowHybrid());
+        assertThat(convertedSubclassOption.preferImmediatelyAvailableCredentials()).isEqualTo(
+                option.preferImmediatelyAvailableCredentials());
         assertThat(convertedSubclassOption.getClientDataHash())
                 .isEqualTo(option.getClientDataHash());
         assertThat(convertedSubclassOption.getRelyingParty()).isEqualTo(option.getRelyingParty());
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedTest.kt
index dec9de2..7033d90 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedTest.kt
@@ -17,7 +17,7 @@
 package androidx.credentials
 
 import android.os.Bundle
-import androidx.credentials.GetCredentialOption.Companion.createFrom
+import androidx.credentials.CredentialOption.Companion.createFrom
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -40,51 +40,65 @@
     }
 
     @Test
-    fun constructor_setsAllowHybridToTrueByDefault() {
+    fun constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
         val getPublicKeyCredentialOptionPrivileged = GetPublicKeyCredentialOptionPrivileged(
             "JSON", "RelyingParty", "HASH"
         )
-        val allowHybridActual = getPublicKeyCredentialOptionPrivileged.allowHybrid
-        assertThat(allowHybridActual).isTrue()
+        val preferImmediatelyAvailableCredentialsActual =
+            getPublicKeyCredentialOptionPrivileged.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual).isFalse()
     }
 
     @Test
-    fun constructor_setsAllowHybridFalse() {
-        val allowHybridExpected = false
+    fun constructor_setPreferImmediatelyAvailableCredentialsTrue() {
+        val preferImmediatelyAvailableCredentialsExpected = true
         val getPublicKeyCredentialOptPriv = GetPublicKeyCredentialOptionPrivileged(
-            "JSON", "RelyingParty", "HASH", allowHybridExpected
+            "JSON",
+            "RelyingParty",
+            "HASH",
+            preferImmediatelyAvailableCredentialsExpected
         )
-        val getAllowHybridActual = getPublicKeyCredentialOptPriv.allowHybrid
-        assertThat(getAllowHybridActual).isEqualTo(allowHybridExpected)
+        val preferImmediatelyAvailableCredentialsActual =
+            getPublicKeyCredentialOptPriv.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+            preferImmediatelyAvailableCredentialsExpected
+        )
     }
 
     @Test
-    fun builder_build_nonDefaultAllowHybrid_false() {
-        val allowHybridExpected = false
+    fun builder_build_nonDefaultPreferImmediatelyAvailableCredentials_true() {
+        val preferImmediatelyAvailableCredentialsExpected = true
         val getPublicKeyCredentialOptionPrivileged = GetPublicKeyCredentialOptionPrivileged
             .Builder(
                 "testJson",
                 "RelyingParty", "Hash",
-            ).setAllowHybrid(allowHybridExpected).build()
-        val getAllowHybridActual = getPublicKeyCredentialOptionPrivileged.allowHybrid
-        assertThat(getAllowHybridActual).isEqualTo(allowHybridExpected)
+            )
+            .setPreferImmediatelyAvailableCredentials(preferImmediatelyAvailableCredentialsExpected)
+            .build()
+        val preferImmediatelyAvailableCredentialsActual =
+            getPublicKeyCredentialOptionPrivileged.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+            preferImmediatelyAvailableCredentialsExpected
+        )
     }
 
     @Test
-    fun builder_build_defaultAllowHybrid_true() {
+    fun builder_build_defaultPreferImmediatelyAvailableCredentials_false() {
         val defaultPrivilegedRequest = GetPublicKeyCredentialOptionPrivileged.Builder(
             "{\"Data\":5}",
             "RelyingParty", "HASH"
         ).build()
-        assertThat(defaultPrivilegedRequest.allowHybrid).isTrue()
+        assertThat(defaultPrivilegedRequest.preferImmediatelyAvailableCredentials).isFalse()
     }
 
     @Test
     fun getter_requestJson_success() {
         val testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
         val getPublicKeyCredentialOptionPrivileged =
-            GetPublicKeyCredentialOptionPrivileged(testJsonExpected, "RelyingParty",
-                "HASH")
+            GetPublicKeyCredentialOptionPrivileged(
+                testJsonExpected, "RelyingParty",
+                "HASH"
+            )
         val testJsonActual = getPublicKeyCredentialOptionPrivileged.requestJson
         assertThat(testJsonActual).isEqualTo(testJsonExpected)
     }
@@ -116,7 +130,7 @@
         val requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
         val relyingPartyExpected = "RelyingParty"
         val clientDataHashExpected = "X342%4dfd7&"
-        val allowHybridExpected = false
+        val preferImmediatelyAvailableCredentialsExpected = false
         val expectedData = Bundle()
         expectedData.putString(
             PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
@@ -127,26 +141,29 @@
             GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_REQUEST_JSON,
             requestJsonExpected
         )
-        expectedData.putString(GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_RELYING_PARTY,
-            relyingPartyExpected)
+        expectedData.putString(
+            GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_RELYING_PARTY,
+            relyingPartyExpected
+        )
         expectedData.putString(
             GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_CLIENT_DATA_HASH,
             clientDataHashExpected
         )
         expectedData.putBoolean(
-            GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_ALLOW_HYBRID,
-            allowHybridExpected
+            GetPublicKeyCredentialOptionPrivileged
+                .BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+            preferImmediatelyAvailableCredentialsExpected
         )
 
         val option = GetPublicKeyCredentialOptionPrivileged(
             requestJsonExpected, relyingPartyExpected, clientDataHashExpected,
-            allowHybridExpected
+            preferImmediatelyAvailableCredentialsExpected
         )
 
         assertThat(option.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
         assertThat(equals(option.requestData, expectedData)).isTrue()
         assertThat(equals(option.candidateQueryData, expectedData)).isTrue()
-        assertThat(option.requireSystemProvider).isFalse()
+        assertThat(option.isSystemProviderRequired).isFalse()
     }
 
     @Test
@@ -154,7 +171,10 @@
         val option = GetPublicKeyCredentialOptionPrivileged("json", "rp", "clientDataHash", true)
 
         val convertedOption = createFrom(
-            option.type, option.requestData, option.candidateQueryData, option.requireSystemProvider
+            option.type,
+            option.requestData,
+            option.candidateQueryData,
+            option.isSystemProviderRequired
         )
 
         assertThat(convertedOption).isInstanceOf(
@@ -162,7 +182,8 @@
         )
         val convertedSubclassOption = convertedOption as GetPublicKeyCredentialOptionPrivileged
         assertThat(convertedSubclassOption.requestJson).isEqualTo(option.requestJson)
-        assertThat(convertedSubclassOption.allowHybrid).isEqualTo(option.allowHybrid)
+        assertThat(convertedSubclassOption.preferImmediatelyAvailableCredentials)
+            .isEqualTo(option.preferImmediatelyAvailableCredentials)
         assertThat(convertedSubclassOption.clientDataHash)
             .isEqualTo(option.clientDataHash)
         assertThat(convertedSubclassOption.relyingParty).isEqualTo(option.relyingParty)
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt
index 6693856b..cc2b192 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt
@@ -17,7 +17,7 @@
 package androidx.credentials
 
 import android.os.Bundle
-import androidx.credentials.GetCredentialOption.Companion.createFrom
+import androidx.credentials.CredentialOption.Companion.createFrom
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -45,22 +45,25 @@
     }
 
     @Test
-    fun constructor_setsAllowHybridToTrueByDefault() {
+    fun constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
         val getPublicKeyCredentialOpt = GetPublicKeyCredentialOption(
             "JSON"
         )
-        val allowHybridActual = getPublicKeyCredentialOpt.allowHybrid
-        assertThat(allowHybridActual).isTrue()
+        val preferImmediatelyAvailableCredentialsActual =
+            getPublicKeyCredentialOpt.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual).isFalse()
     }
 
     @Test
-    fun constructor_setsAllowHybridFalse() {
-        val allowHybridExpected = false
+    fun constructor_setPreferImmediatelyAvailableCredentialsTrue() {
+        val preferImmediatelyAvailableCredentialsExpected = true
         val getPublicKeyCredentialOpt = GetPublicKeyCredentialOption(
-            "JSON", allowHybridExpected
+            "JSON", preferImmediatelyAvailableCredentialsExpected
         )
-        val allowHybridActual = getPublicKeyCredentialOpt.allowHybrid
-        assertThat(allowHybridActual).isEqualTo(allowHybridExpected)
+        val preferImmediatelyAvailableCredentialsActual =
+            getPublicKeyCredentialOpt.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual)
+            .isEqualTo(preferImmediatelyAvailableCredentialsExpected)
     }
 
     @Test
@@ -74,7 +77,7 @@
     @Test
     fun getter_frameworkProperties_success() {
         val requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
-        val allowHybridExpected = false
+        val preferImmediatelyAvailableCredentialsExpected = false
         val expectedData = Bundle()
         expectedData.putString(
             PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
@@ -85,16 +88,18 @@
             requestJsonExpected
         )
         expectedData.putBoolean(
-            GetPublicKeyCredentialOption.BUNDLE_KEY_ALLOW_HYBRID,
-            allowHybridExpected
+            GetPublicKeyCredentialOption.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+            preferImmediatelyAvailableCredentialsExpected
         )
 
-        val option = GetPublicKeyCredentialOption(requestJsonExpected, allowHybridExpected)
+        val option = GetPublicKeyCredentialOption(
+            requestJsonExpected, preferImmediatelyAvailableCredentialsExpected
+        )
 
         assertThat(option.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
         assertThat(equals(option.requestData, expectedData)).isTrue()
         assertThat(equals(option.candidateQueryData, expectedData)).isTrue()
-        assertThat(option.requireSystemProvider).isFalse()
+        assertThat(option.isSystemProviderRequired).isFalse()
     }
 
     @Test
@@ -102,7 +107,10 @@
         val option = GetPublicKeyCredentialOption("json", true)
 
         val convertedOption = createFrom(
-            option.type, option.requestData, option.candidateQueryData, option.requireSystemProvider
+            option.type,
+            option.requestData,
+            option.candidateQueryData,
+            option.isSystemProviderRequired
         )
 
         assertThat(convertedOption).isInstanceOf(
@@ -110,6 +118,7 @@
         )
         val convertedSubclassOption = convertedOption as GetPublicKeyCredentialOption
         assertThat(convertedSubclassOption.requestJson).isEqualTo(option.requestJson)
-        assertThat(convertedSubclassOption.allowHybrid).isEqualTo(option.allowHybrid)
+        assertThat(convertedSubclassOption.preferImmediatelyAvailableCredentials)
+            .isEqualTo(option.preferImmediatelyAvailableCredentials)
     }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialJavaTest.java
index 2edf0d3..659dd33 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialJavaTest.java
@@ -77,10 +77,10 @@
         expectedData.putString(PasswordCredential.BUNDLE_KEY_ID, idExpected);
         expectedData.putString(PasswordCredential.BUNDLE_KEY_PASSWORD, passwordExpected);
 
-        CreatePasswordRequest credential = new CreatePasswordRequest(idExpected, passwordExpected);
+        PasswordCredential credential = new PasswordCredential(idExpected, passwordExpected);
 
         assertThat(credential.getType()).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL);
-        assertThat(TestUtilsKt.equals(credential.getCredentialData(), expectedData)).isTrue();
+        assertThat(TestUtilsKt.equals(credential.getData(), expectedData)).isTrue();
     }
 
     @Test
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/TestActivity.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/TestActivity.kt
new file mode 100644
index 0000000..07b5b9a
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/TestActivity.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials
+
+import androidx.core.app.ComponentActivity
+
+/**
+ * This is a test activity used by the Robolectric Activity Scenario tests. It acts
+ * as a calling activity in our test cases.
+ */
+class TestActivity : ComponentActivity()
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
index 4567380..6b1ae3b 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
@@ -16,6 +16,7 @@
 
 package androidx.credentials
 
+import android.os.Build
 import android.os.Bundle
 
 /** True if the two Bundles contain the same elements, and false otherwise. */
@@ -41,4 +42,15 @@
         }
     }
     return true
+}
+
+/** Used to maintain compatibility across API levels. */
+const val MAX_CRED_MAN_PRE_FRAMEWORK_API_LEVEL = Build.VERSION_CODES.TIRAMISU
+
+/** True if the device running the test is post framework api level,
+ * false if pre framework api level. */
+fun isPostFrameworkApiLevel(): Boolean {
+    return !((Build.VERSION.SDK_INT <= MAX_CRED_MAN_PRE_FRAMEWORK_API_LEVEL) &&
+        !(Build.VERSION.SDK_INT == MAX_CRED_MAN_PRE_FRAMEWORK_API_LEVEL &&
+            Build.VERSION.PREVIEW_SDK_INT > 0))
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt
index b323a5a..e351e8f 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt
@@ -16,16 +16,20 @@
 
 package androidx.credentials
 
+import android.graphics.drawable.Icon
 import android.os.Bundle
+import android.text.TextUtils
+import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
-import androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Companion.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIVILEGED
+import androidx.annotation.VisibleForTesting
+import androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Companion.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV
 import androidx.credentials.PublicKeyCredential.Companion.BUNDLE_KEY_SUBTYPE
 import androidx.credentials.internal.FrameworkClassParsingException
 
 /**
  * Base request class for registering a credential.
  *
- * An application can construct a subtype request and call [CredentialManager.executeCreateCredential] to
+ * An application can construct a subtype request and call [CredentialManager.createCredential] to
  * launch framework UI flows to collect consent and any other metadata needed from the user to
  * register a new user credential.
  */
@@ -41,32 +45,138 @@
     open val candidateQueryData: Bundle,
     /** @hide */
     @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    open val requireSystemProvider: Boolean
+    open val isSystemProviderRequired: Boolean,
+    /** @hide */
+    internal val displayInfo: DisplayInfo,
 ) {
+    /**
+     * Information that may be used for display purposes when rendering UIs to collect the user
+     * consent and choice.
+     *
+     * @property userId the user id of the created credential
+     * @property userDisplayName an optional display name in addition to the [userId] that may be
+     * displayed next to the `userId` during the user consent to help your user better understand
+     * the credential being created.
+     */
+    class DisplayInfo internal /** @hide */ constructor(
+        val userId: String,
+        val userDisplayName: String?,
+        /** @hide */
+        val credentialTypeIcon: Icon?
+    ) {
+
+        /**
+         * Constructs a [DisplayInfo].
+         *
+         * @param userId the user id of the created credential
+         * @param userDisplayName an optional display name in addition to the [userId] that may be
+         * displayed next to the `userId` during the user consent to help your user better
+         * understand the credential being created.
+         * @throws IllegalArgumentException If [userId] is empty
+         */
+        @JvmOverloads constructor(userId: String, userDisplayName: String? = null) : this(
+            userId,
+            userDisplayName,
+            null
+        )
+
+        init {
+            require(userId.isNotEmpty()) { "userId should not be empty" }
+        }
+
+        /** @hide */
+        @RequiresApi(23)
+        fun toBundle(): Bundle {
+            val bundle = Bundle()
+            bundle.putString(BUNDLE_KEY_USER_ID, userId)
+            if (!TextUtils.isEmpty(userDisplayName)) {
+                bundle.putString(BUNDLE_KEY_USER_DISPLAY_NAME, userDisplayName)
+            }
+            // Today the type icon is determined solely within this library right before the
+            // request is passed into the framework. Later if needed a new API can be added for
+            // custom SDKs to supply their own credential type icons.
+            return bundle
+        }
+
+        /** @hide */
+        companion object {
+            /** @hide */
+            const val BUNDLE_KEY_REQUEST_DISPLAY_INFO =
+                "androidx.credentials.BUNDLE_KEY_REQUEST_DISPLAY_INFO"
+
+            @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+            /** @hide */
+            const val BUNDLE_KEY_USER_ID =
+                "androidx.credentials.BUNDLE_KEY_USER_ID"
+
+            @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+            /** @hide */
+            const val BUNDLE_KEY_USER_DISPLAY_NAME =
+                "androidx.credentials.BUNDLE_KEY_USER_DISPLAY_NAME"
+
+            /** @hide */
+            const val BUNDLE_KEY_CREDENTIAL_TYPE_ICON =
+                "androidx.credentials.BUNDLE_KEY_CREDENTIAL_TYPE_ICON"
+
+            /**
+             * Returns a RequestDisplayInfo from a `credentialData` Bundle, or otherwise `null` if
+             * parsing fails.
+             *
+             * @hide
+             */
+            @JvmStatic
+            @RequiresApi(23)
+            @Suppress("DEPRECATION") // bundle.getParcelable(key)
+            fun parseFromCredentialDataBundle(from: Bundle): DisplayInfo? {
+                return try {
+                    val displayInfoBundle = from.getBundle(BUNDLE_KEY_REQUEST_DISPLAY_INFO)!!
+                    val userId = displayInfoBundle.getString(BUNDLE_KEY_USER_ID)
+                    val displayName = displayInfoBundle.getString(BUNDLE_KEY_USER_DISPLAY_NAME)
+                    val icon: Icon? =
+                        displayInfoBundle.getParcelable(BUNDLE_KEY_CREDENTIAL_TYPE_ICON)
+                    DisplayInfo(userId!!, displayName, icon)
+                } catch (e: Exception) {
+                    null
+                }
+            }
+        }
+    }
+
     /** @hide */
     companion object {
-        /** @hide */
+        /**
+         * Attempts to parse the raw data into one of [CreatePasswordRequest],
+         * [CreatePublicKeyCredentialRequest], [CreatePublicKeyCredentialRequestPrivileged], and
+         * [CreateCustomCredentialRequest]. Otherwise returns null.
+         *
+         * @hide
+         */
         @JvmStatic
+        @RequiresApi(23)
         fun createFrom(
             type: String,
             credentialData: Bundle,
             candidateQueryData: Bundle,
             requireSystemProvider: Boolean
-        ): CreateCredentialRequest {
+        ): CreateCredentialRequest? {
             return try {
                 when (type) {
                     PasswordCredential.TYPE_PASSWORD_CREDENTIAL ->
                         CreatePasswordRequest.createFrom(credentialData)
+
                     PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
                         when (credentialData.getString(BUNDLE_KEY_SUBTYPE)) {
                             CreatePublicKeyCredentialRequest
                                 .BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST ->
                                 CreatePublicKeyCredentialRequest.createFrom(credentialData)
-                            BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIVILEGED ->
+
+                            BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV ->
                                 CreatePublicKeyCredentialRequestPrivileged
                                     .createFrom(credentialData)
+
                             else -> throw FrameworkClassParsingException()
                         }
+
                     else -> throw FrameworkClassParsingException()
                 }
             } catch (e: FrameworkClassParsingException) {
@@ -76,7 +186,10 @@
                     type,
                     credentialData,
                     candidateQueryData,
-                    requireSystemProvider
+                    requireSystemProvider,
+                    DisplayInfo.parseFromCredentialDataBundle(
+                        credentialData
+                    ) ?: return null
                 )
             }
         }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialRequest.kt
index 9ad1dd8..507c0aaa 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialRequest.kt
@@ -22,28 +22,44 @@
  * Base custom create request class for registering a credential.
  *
  * An application can construct a subtype custom request and call
- * [CredentialManager.executeCreateCredential] to launch framework UI flows to collect consent and
+ * [CredentialManager.createCredential] to launch framework UI flows to collect consent and
  * any other metadata needed from the user to register a new user credential.
  *
- * @property type the credential type determined by the credential-type-specific subclass for custom
- * use cases
- * @property credentialData the full credential creation request data in the [Bundle] format for
+ * If you get a [CreateCustomCredentialRequest] instead of a type-safe request class such as
+ * [CreatePasswordRequest], [CreatePublicKeyCredentialRequest], etc., then you should check if you
+ * have any other library at interest that supports this custom [type] of credential request,
+ * and if so use its parsing utilities to resolve to a type-safe class within that library.
+ *
+ * Note: The Bundle keys for [credentialData] and [candidateQueryData] should not be in the form
+ * of androidx.credentials.*` as they are reserved for internal use by this androidx library.
+ *
+ * @property type the credential type determined by the credential-type-specific subclass for
  * custom use cases
- * @property candidateQueryData the partial request data in the [Bundle] format that will be sent to
- * the provider during the initial candidate query stage, which should not contain sensitive user
- * credential information
- * @property requireSystemProvider true if must only be fulfilled by a system provider and false
- * otherwise
+ * @property credentialData the data of this [CreateCustomCredentialRequest] in the [Bundle]
+ * format (note: bundle keys in the form of `androidx.credentials.*` are reserved for internal
+ * library use)
+ * @property candidateQueryData the partial request data in the [Bundle] format that will be sent
+ * to the provider during the initial candidate query stage, which should not contain sensitive
+ * user credential information (note: bundle keys in the form of `androidx.credentials.*` are
+ * reserved for internal library use)
+ * @property isSystemProviderRequired true if must only be fulfilled by a system provider and
+ * false otherwise
  * @throws IllegalArgumentException If [type] is empty
- * @throws NullPointerException If [type] or [credentialData] are null
+ * @throws NullPointerException If [type], [credentialData], or [candidateQueryData] is null
  */
 open class CreateCustomCredentialRequest(
     final override val type: String,
     final override val credentialData: Bundle,
     final override val candidateQueryData: Bundle,
-    @get:JvmName("requireSystemProvider")
-    final override val requireSystemProvider: Boolean
-) : CreateCredentialRequest(type, credentialData, candidateQueryData, requireSystemProvider) {
+    final override val isSystemProviderRequired: Boolean,
+    displayInfo: DisplayInfo,
+) : CreateCredentialRequest(
+    type,
+    credentialData,
+    candidateQueryData,
+    isSystemProviderRequired,
+    displayInfo
+) {
     init {
         require(type.isNotEmpty()) { "type should not be empty" }
     }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialResponse.kt b/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialResponse.kt
index 3cca4ac..caf0dac 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialResponse.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialResponse.kt
@@ -22,6 +22,14 @@
  * Base custom create response class for the credential creation operation made with the
  * [CreateCustomCredentialRequest].
  *
+ * If you get a [CreateCustomCredentialResponse] instead of a type-safe response class such as
+ * [CreatePasswordResponse], [CreatePublicKeyCredentialResponse], etc., then you should check if
+ * you have any other library at interest that supports this custom [type] of credential response,
+ * and if so use its parsing utilities to resolve to a type-safe class within that library.
+ *
+ * Note: The Bundle keys for [data] should not be in the form of androidx.credentials.*` as they
+ * are reserved for internal use by this androidx library.
+ *
  * @property type the credential type determined by the credential-type-specific subclass for custom
  * use cases
  * @property data the response data in the [Bundle] format for custom use cases
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordRequest.kt
index 682731e..530eb69 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordRequest.kt
@@ -17,6 +17,7 @@
 package androidx.credentials
 
 import android.os.Bundle
+import androidx.annotation.RequiresApi
 import androidx.annotation.VisibleForTesting
 import androidx.credentials.internal.FrameworkClassParsingException
 
@@ -25,21 +26,31 @@
  *
  * @property id the user id associated with the password
  * @property password the password
- * @throws NullPointerException If [id] is null
- * @throws NullPointerException If [password] is null
- * @throws IllegalArgumentException If [password] is empty
  */
-class CreatePasswordRequest constructor(
+class CreatePasswordRequest private constructor(
     val id: String,
     val password: String,
+    displayInfo: DisplayInfo,
 ) : CreateCredentialRequest(
     type = PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
     credentialData = toCredentialDataBundle(id, password),
-    // No credential data should be sent during the query phase.
-    candidateQueryData = Bundle(),
-    requireSystemProvider = false,
+    candidateQueryData = toCandidateDataBundle(),
+    isSystemProviderRequired = false,
+    displayInfo
 ) {
 
+    /**
+     * Constructs a [CreatePasswordRequest] to save the user password credential with their
+     * password provider.
+     *
+     * @param id the user id associated with the password
+     * @param password the password
+     * @throws NullPointerException If [id] is null
+     * @throws NullPointerException If [password] is null
+     * @throws IllegalArgumentException If [password] is empty
+     */
+    constructor(id: String, password: String) : this(id, password, DisplayInfo(id, null))
+
     init {
         require(password.isNotEmpty()) { "password should not be empty" }
     }
@@ -47,6 +58,7 @@
     /** @hide */
     companion object {
         internal const val BUNDLE_KEY_ID = "androidx.credentials.BUNDLE_KEY_ID"
+
         @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
         const val BUNDLE_KEY_PASSWORD = "androidx.credentials.BUNDLE_KEY_PASSWORD"
 
@@ -58,12 +70,23 @@
             return bundle
         }
 
+        // No credential data should be sent during the query phase.
         @JvmStatic
+        internal fun toCandidateDataBundle(): Bundle {
+            return Bundle()
+        }
+
+        @JvmStatic
+        @RequiresApi(23)
         internal fun createFrom(data: Bundle): CreatePasswordRequest {
             try {
                 val id = data.getString(BUNDLE_KEY_ID)
                 val password = data.getString(BUNDLE_KEY_PASSWORD)
-                return CreatePasswordRequest(id!!, password!!)
+                val displayInfo = DisplayInfo.parseFromCredentialDataBundle(data)
+                return if (displayInfo == null) CreatePasswordRequest(
+                    id!!,
+                    password!!
+                ) else CreatePasswordRequest(id!!, password!!, displayInfo)
             } catch (e: Exception) {
                 throw FrameworkClassParsingException()
             }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt
index c78e8b2..1e80938 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt
@@ -17,64 +17,129 @@
 package androidx.credentials
 
 import android.os.Bundle
-import androidx.annotation.VisibleForTesting
+import androidx.annotation.RequiresApi
 import androidx.credentials.PublicKeyCredential.Companion.BUNDLE_KEY_SUBTYPE
 import androidx.credentials.internal.FrameworkClassParsingException
+import org.json.JSONObject
 
 /**
  * A request to register a passkey from the user's public key credential provider.
  *
- * @property requestJson the privileged request in JSON format in the standard webauthn web json
- * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
- * @property allowHybrid defines whether hybrid credentials are allowed to fulfill this request,
- * true by default, with hybrid credentials defined
- * [here](https://w3c.github.io/webauthn/#dom-authenticatortransport-hybrid)
- * @throws NullPointerException If [requestJson] is null
- * @throws IllegalArgumentException If [requestJson] is empty
+ * @property requestJson the privileged request in JSON format in the [standard webauthn web json](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
+ * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
+ * immediately when there is no available passkey registration offering instead of falling back to
+ * discovering remote options, and false (default) otherwise
  */
-class CreatePublicKeyCredentialRequest @JvmOverloads constructor(
+class CreatePublicKeyCredentialRequest private constructor(
     val requestJson: String,
-    @get:JvmName("allowHybrid")
-    val allowHybrid: Boolean = true
+    @get:JvmName("preferImmediatelyAvailableCredentials")
+    val preferImmediatelyAvailableCredentials: Boolean,
+    displayInfo: DisplayInfo,
 ) : CreateCredentialRequest(
     type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
-    credentialData = toCredentialDataBundle(requestJson, allowHybrid),
+    credentialData = toCredentialDataBundle(requestJson, preferImmediatelyAvailableCredentials),
     // The whole request data should be passed during the query phase.
-    candidateQueryData = toCredentialDataBundle(requestJson, allowHybrid),
-    requireSystemProvider = false,
+    candidateQueryData = toCredentialDataBundle(requestJson, preferImmediatelyAvailableCredentials),
+    isSystemProviderRequired = false,
+    displayInfo,
 ) {
 
+    /**
+     * Constructs a [CreatePublicKeyCredentialRequest] to register a passkey from the user's public key credential provider.
+     *
+     * @param requestJson the privileged request in JSON format in the [standard webauthn web json](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
+     * @param preferImmediatelyAvailableCredentials true if you prefer the operation to return
+     * immediately when there is no available passkey registration offering instead of falling back to
+     * discovering remote options, and false (default) otherwise
+     * @throws NullPointerException If [requestJson] is null
+     * @throws IllegalArgumentException If [requestJson] is empty, or if it doesn't have a valid
+     * `user.name` defined according to the [webauthn spec](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson)
+     */
+    @JvmOverloads constructor(
+        requestJson: String,
+        preferImmediatelyAvailableCredentials: Boolean = false
+    ) : this(requestJson, preferImmediatelyAvailableCredentials, getRequestDisplayInfo(requestJson))
+
     init {
         require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
     }
 
     /** @hide */
     companion object {
-        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID"
+        internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
+            "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
         internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
-        @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
-        const val BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST =
+        internal const val BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST =
             "androidx.credentials.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST"
 
         @JvmStatic
-        internal fun toCredentialDataBundle(requestJson: String, allowHybrid: Boolean): Bundle {
+        internal fun getRequestDisplayInfo(requestJson: String): DisplayInfo {
+            return try {
+                val json = JSONObject(requestJson)
+                val user = json.getJSONObject("user")
+                val userName = user.getString("name")
+                val displayName: String? =
+                    if (user.isNull("displayName")) null else user.getString("displayName")
+                DisplayInfo(userName, displayName)
+            } catch (e: Exception) {
+                throw IllegalArgumentException("user.name must be defined in requestJson")
+            }
+        }
+
+        @JvmStatic
+        internal fun toCredentialDataBundle(
+            requestJson: String,
+            preferImmediatelyAvailableCredentials: Boolean
+        ): Bundle {
             val bundle = Bundle()
-            bundle.putString(BUNDLE_KEY_SUBTYPE,
-                BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST)
+            bundle.putString(
+                BUNDLE_KEY_SUBTYPE,
+                BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST
+            )
             bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
-            bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid)
+            bundle.putBoolean(
+                BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+                preferImmediatelyAvailableCredentials
+            )
+            return bundle
+        }
+
+        @JvmStatic
+        internal fun toCandidateDataBundle(
+            requestJson: String,
+            preferImmediatelyAvailableCredentials: Boolean
+        ): Bundle {
+            val bundle = Bundle()
+            bundle.putString(
+                BUNDLE_KEY_SUBTYPE,
+                BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST
+            )
+            bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
+            bundle.putBoolean(
+                BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+                preferImmediatelyAvailableCredentials
+            )
             return bundle
         }
 
         @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
-                                         // boolean value from being returned.
+        // boolean value from being returned.
         @JvmStatic
+        @RequiresApi(23)
         internal fun createFrom(data: Bundle): CreatePublicKeyCredentialRequest {
             try {
                 val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
-                val allowHybrid = data.get(BUNDLE_KEY_ALLOW_HYBRID)
-                return CreatePublicKeyCredentialRequest(requestJson!!, (allowHybrid!!) as Boolean)
+                val preferImmediatelyAvailableCredentials =
+                    data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
+                val displayInfo = DisplayInfo.parseFromCredentialDataBundle(data)
+                return if (displayInfo == null) CreatePublicKeyCredentialRequest(
+                    requestJson!!,
+                    (preferImmediatelyAvailableCredentials!!) as Boolean
+                ) else CreatePublicKeyCredentialRequest(
+                    requestJson!!,
+                    (preferImmediatelyAvailableCredentials!!) as Boolean,
+                    displayInfo
+                )
             } catch (e: Exception) {
                 throw FrameworkClassParsingException()
             }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivileged.kt b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivileged.kt
index 1600894..9c888b5 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivileged.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivileged.kt
@@ -17,42 +17,77 @@
 package androidx.credentials
 
 import android.os.Bundle
-import androidx.annotation.VisibleForTesting
+import androidx.annotation.RequiresApi
 import androidx.credentials.internal.FrameworkClassParsingException
 
 /**
  * A privileged request to register a passkey from the user’s public key credential provider, where
  * the caller can modify the rp. Only callers with privileged permission, e.g. user’s default
- * brower, caBLE, can use this. These permissions will be introduced in an upcoming release.
- * TODO("Add specific permission info/annotation")
+ * browser, caBLE, can use this. These permissions will be introduced in an upcoming release.
  *
- * @property requestJson the privileged request in JSON format in the standard webauthn web json
- * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
- * @property allowHybrid defines whether hybrid credentials are allowed to fulfill this request,
- * true by default, with hybrid credentials defined
- * [here](https://w3c.github.io/webauthn/#dom-authenticatortransport-hybrid)
- * @property relyingParty the expected true RP ID which will override the one in the [requestJson], where
- * rp is defined [here](https://w3c.github.io/webauthn/#rp-id)
- * @property clientDataHash a hash that is used to verify the [relyingParty] Identity
- * @throws NullPointerException If any of [requestJson], [relyingParty], or [clientDataHash] is
- * null
- * @throws IllegalArgumentException If any of [requestJson], [relyingParty], or [clientDataHash] is empty
+ * @property requestJson the privileged request in JSON format in the [standard webauthn web json](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
+ * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
+ * immediately when there is no available passkey registration offering instead of falling back to
+ * discovering remote options, and false (default) otherwise
+ * @property relyingParty the expected true RP ID which will override the one in the [requestJson],
+ * where rp is defined [here](https://w3c.github.io/webauthn/#rp-id)
  */
-class CreatePublicKeyCredentialRequestPrivileged @JvmOverloads constructor(
+// TODO("Add specific permission info/annotation")
+class CreatePublicKeyCredentialRequestPrivileged private constructor(
     val requestJson: String,
     val relyingParty: String,
     val clientDataHash: String,
-    @get:JvmName("allowHybrid")
-    val allowHybrid: Boolean = true
+    @get:JvmName("preferImmediatelyAvailableCredentials")
+    val preferImmediatelyAvailableCredentials: Boolean,
+    displayInfo: DisplayInfo,
 ) : CreateCredentialRequest(
     type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
-    credentialData = toCredentialDataBundle(requestJson, relyingParty, clientDataHash, allowHybrid),
+    credentialData = toCredentialDataBundle(
+        requestJson,
+        relyingParty,
+        clientDataHash,
+        preferImmediatelyAvailableCredentials
+    ),
     // The whole request data should be passed during the query phase.
     candidateQueryData = toCredentialDataBundle(
-        requestJson, relyingParty, clientDataHash, allowHybrid),
-    requireSystemProvider = false,
+        requestJson, relyingParty, clientDataHash, preferImmediatelyAvailableCredentials
+    ),
+    isSystemProviderRequired = false,
+    displayInfo,
 ) {
 
+    /**
+     * Constructs a privileged request to register a passkey from the user’s public key credential
+     * provider, where the caller can modify the rp. Only callers with privileged permission, e.g.
+     * user’s default browser, caBLE, can use this. These permissions will be introduced in an
+     * upcoming release.
+     *
+     * @param requestJson the privileged request in JSON format in the [standard webauthn web json](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
+     * @param preferImmediatelyAvailableCredentials true if you prefer the operation to return
+     * immediately when there is no available passkey registration offering instead of falling
+     * back to discovering remote options, and false (default) otherwise
+     * @param relyingParty the expected true RP ID which will override the one in the
+     * [requestJson], where rp is defined [here](https://w3c.github.io/webauthn/#rp-id)
+     * @param clientDataHash a hash that is used to verify the [relyingParty] Identity
+     * @throws NullPointerException If any of [requestJson], [relyingParty], or [clientDataHash] is
+     * null
+     * @throws IllegalArgumentException If any of [requestJson], [relyingParty], or
+     * [clientDataHash] is empty, or if [requestJson] doesn't have a valid user.name` defined
+     * according to the [webauthn spec](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson)
+     */
+    @JvmOverloads constructor(
+        requestJson: String,
+        relyingParty: String,
+        clientDataHash: String,
+        preferImmediatelyAvailableCredentials: Boolean = false
+    ) : this(
+        requestJson,
+        relyingParty,
+        clientDataHash,
+        preferImmediatelyAvailableCredentials,
+        CreatePublicKeyCredentialRequest.getRequestDisplayInfo(requestJson),
+    )
+
     init {
         require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
         require(relyingParty.isNotEmpty()) { "rp must not be empty" }
@@ -64,9 +99,9 @@
         private var requestJson: String,
         private var relyingParty: String,
         private var clientDataHash: String
-        ) {
+    ) {
 
-        private var allowHybrid: Boolean = true
+        private var preferImmediatelyAvailableCredentials: Boolean = false
 
         /**
          * Sets the privileged request in JSON format.
@@ -77,11 +112,17 @@
         }
 
         /**
-         * Sets whether hybrid credentials are allowed to fulfill this request, true by default.
+         * Sets to true if you prefer the operation to return immediately when there is no available
+         * passkey registration offering instead of falling back to discovering remote options, and
+         * false otherwise.
+         *
+         * The default value is false.
          */
         @Suppress("MissingGetterMatchingBuilder")
-        fun setAllowHybrid(allowHybrid: Boolean): Builder {
-            this.allowHybrid = allowHybrid
+        fun setPreferImmediatelyAvailableCredentials(
+            preferImmediatelyAvailableCredentials: Boolean
+        ): Builder {
+            this.preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
             return this
         }
 
@@ -103,24 +144,25 @@
 
         /** Builds a [CreatePublicKeyCredentialRequestPrivileged]. */
         fun build(): CreatePublicKeyCredentialRequestPrivileged {
-            return CreatePublicKeyCredentialRequestPrivileged(this.requestJson,
-                this.relyingParty, this.clientDataHash, this.allowHybrid)
+            return CreatePublicKeyCredentialRequestPrivileged(
+                this.requestJson,
+                this.relyingParty, this.clientDataHash, this.preferImmediatelyAvailableCredentials
+            )
         }
     }
 
     /** @hide */
     companion object {
-        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_RELYING_PARTY = "androidx.credentials.BUNDLE_KEY_RELYING_PARTY"
-        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_CLIENT_DATA_HASH =
+        internal const val BUNDLE_KEY_RELYING_PARTY =
+            "androidx.credentials.BUNDLE_KEY_RELYING_PARTY"
+        internal const val BUNDLE_KEY_CLIENT_DATA_HASH =
             "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
-        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID"
-        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
-        @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
-        const val BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIVILEGED =
+        internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
+            "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
+
+        internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
+
+        internal const val BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV =
             "androidx.credentials.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_" +
                 "PRIVILEGED"
 
@@ -129,34 +171,46 @@
             requestJson: String,
             relyingParty: String,
             clientDataHash: String,
-            allowHybrid: Boolean
+            preferImmediatelyAvailableCredentials: Boolean
         ): Bundle {
             val bundle = Bundle()
             bundle.putString(
                 PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
-                BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIVILEGED
+                BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV
             )
             bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
             bundle.putString(BUNDLE_KEY_RELYING_PARTY, relyingParty)
             bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
-            bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid)
+            bundle.putBoolean(
+                BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+                preferImmediatelyAvailableCredentials
+            )
             return bundle
         }
 
         @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
-                                         // boolean value from being returned.
+        // boolean value from being returned.
         @JvmStatic
+        @RequiresApi(23)
         internal fun createFrom(data: Bundle): CreatePublicKeyCredentialRequestPrivileged {
             try {
                 val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
                 val rp = data.getString(BUNDLE_KEY_RELYING_PARTY)
                 val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH)
-                val allowHybrid = data.get(BUNDLE_KEY_ALLOW_HYBRID)
-                return CreatePublicKeyCredentialRequestPrivileged(
+                val preferImmediatelyAvailableCredentials =
+                    data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
+                val displayInfo = DisplayInfo.parseFromCredentialDataBundle(data)
+                return if (displayInfo == null) CreatePublicKeyCredentialRequestPrivileged(
                     requestJson!!,
                     rp!!,
                     clientDataHash!!,
-                    (allowHybrid!!) as Boolean,
+                    (preferImmediatelyAvailableCredentials!!) as Boolean,
+                ) else CreatePublicKeyCredentialRequestPrivileged(
+                    requestJson!!,
+                    rp!!,
+                    clientDataHash!!,
+                    (preferImmediatelyAvailableCredentials!!) as Boolean,
+                    displayInfo,
                 )
             } catch (e: Exception) {
                 throw FrameworkClassParsingException()
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialManager.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialManager.kt
index 89a8ac6..53396d9 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialManager.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialManager.kt
@@ -100,10 +100,9 @@
      *
      * @param request the request for getting the credential
      * @param activity the activity used to potentially launch any UI needed
-     * @throws UnsupportedOperationException Since the api is unimplemented
+     * @throws GetCredentialException If the request fails
      */
-    // TODO(helenqin): support failure flow.
-    suspend fun executeGetCredential(
+    suspend fun getCredential(
         request: GetCredentialRequest,
         activity: Activity,
     ): GetCredentialResponse = suspendCancellableCoroutine { continuation ->
@@ -123,7 +122,7 @@
             }
         }
 
-        executeGetCredentialAsync(
+        getCredentialAsync(
             request,
             activity,
             canceller,
@@ -142,9 +141,9 @@
      *
      * @param request the request for creating the credential
      * @param activity the activity used to potentially launch any UI needed
-     * @throws UnsupportedOperationException Since the api is unimplemented
+     * @throws CreateCredentialException If the request fails
      */
-    suspend fun executeCreateCredential(
+    suspend fun createCredential(
         request: CreateCredentialRequest,
         activity: Activity,
     ): CreateCredentialResponse = suspendCancellableCoroutine { continuation ->
@@ -164,7 +163,7 @@
             }
         }
 
-        executeCreateCredentialAsync(
+        createCredentialAsync(
             request,
             activity,
             canceller,
@@ -187,6 +186,7 @@
      * to let the provider clear any stored credential session.
      *
      * @param request the request for clearing the app user's credential state
+     * @throws ClearCredentialException If the request fails
      */
     suspend fun clearCredentialState(
         request: ClearCredentialStateRequest
@@ -226,9 +226,8 @@
      * @param cancellationSignal an optional signal that allows for cancelling this call
      * @param executor the callback will take place on this executor
      * @param callback the callback invoked when the request succeeds or fails
-     * @throws UnsupportedOperationException Since the api is unimplemented
      */
-    fun executeGetCredentialAsync(
+    fun getCredentialAsync(
         request: GetCredentialRequest,
         activity: Activity,
         cancellationSignal: CancellationSignal?,
@@ -241,7 +240,7 @@
             // TODO (Update with the right error code when ready)
             callback.onError(
                 GetCredentialProviderConfigurationException(
-                    "executeGetCredentialAsync no provider dependencies found - please ensure " +
+                    "getCredentialAsync no provider dependencies found - please ensure " +
                         "the desired provider dependencies are added")
             )
             return
@@ -261,9 +260,8 @@
      * @param cancellationSignal an optional signal that allows for cancelling this call
      * @param executor the callback will take place on this executor
      * @param callback the callback invoked when the request succeeds or fails
-     * @throws UnsupportedOperationException Since the api is unimplemented
      */
-    fun executeCreateCredentialAsync(
+    fun createCredentialAsync(
         request: CreateCredentialRequest,
         activity: Activity,
         cancellationSignal: CancellationSignal?,
@@ -275,7 +273,7 @@
         if (provider == null) {
             // TODO (Update with the right error code when ready)
             callback.onError(CreateCredentialProviderConfigurationException(
-                "executeCreateCredentialAsync no provider dependencies found - please ensure the " +
+                "createCredentialAsync no provider dependencies found - please ensure the " +
                     "desired provider dependencies are added"))
             return
         }
@@ -298,7 +296,6 @@
      * @param cancellationSignal an optional signal that allows for cancelling this call
      * @param executor the callback will take place on this executor
      * @param callback the callback invoked when the request succeeds or fails
-     * @throws UnsupportedOperationException Since the api is unimplemented
      */
     fun clearCredentialStateAsync(
         request: ClearCredentialStateRequest,
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt
new file mode 100644
index 0000000..555cd00
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.credentials
+
+import android.os.Bundle
+import androidx.annotation.RestrictTo
+import androidx.credentials.internal.FrameworkClassParsingException
+
+/**
+ * Base class for getting a specific type of credentials.
+ *
+ * [GetCredentialRequest] will be composed of a list of [CredentialOption] subclasses to indicate
+ * the specific credential types and configurations that your app accepts.
+ */
+abstract class CredentialOption internal constructor(
+    /** @hide */
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    open val type: String,
+    /** @hide */
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    open val requestData: Bundle,
+    /** @hide */
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    open val candidateQueryData: Bundle,
+    /** @hide */
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    open val isSystemProviderRequired: Boolean,
+) {
+    /** @hide */
+    companion object {
+        /** @hide */
+        @JvmStatic
+        fun createFrom(
+            type: String,
+            requestData: Bundle,
+            candidateQueryData: Bundle,
+            requireSystemProvider: Boolean
+        ): CredentialOption {
+            return try {
+                when (type) {
+                    PasswordCredential.TYPE_PASSWORD_CREDENTIAL ->
+                        GetPasswordOption.createFrom(requestData)
+                    PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
+                        when (requestData.getString(PublicKeyCredential.BUNDLE_KEY_SUBTYPE)) {
+                            GetPublicKeyCredentialOption
+                                .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION ->
+                                GetPublicKeyCredentialOption.createFrom(requestData)
+                            GetPublicKeyCredentialOptionPrivileged
+                                .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED ->
+                                GetPublicKeyCredentialOptionPrivileged.createFrom(requestData)
+                            else -> throw FrameworkClassParsingException()
+                        }
+                    else -> throw FrameworkClassParsingException()
+                }
+            } catch (e: FrameworkClassParsingException) {
+                // Parsing failed but don't crash the process. Instead just output a request with
+                // the raw framework values.
+                GetCustomCredentialOption(
+                    type, requestData, candidateQueryData, requireSystemProvider)
+            }
+        }
+    }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
index 2896622..6dcd0f2 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
@@ -84,8 +84,10 @@
         @Suppress("deprecation")
         private fun getAllowedProvidersFromManifest(context: Context): List<String> {
             val packageInfo = context.packageManager
-                .getPackageInfo(context.packageName, PackageManager.GET_META_DATA or
-                        PackageManager.GET_SERVICES)
+                .getPackageInfo(
+                    context.packageName, PackageManager.GET_META_DATA or
+                        PackageManager.GET_SERVICES
+                )
 
             val classNames = mutableListOf<String>()
             if (packageInfo.services != null) {
@@ -101,4 +103,4 @@
             return classNames.toList()
         }
     }
-}
\ No newline at end of file
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CustomCredential.kt b/credentials/credentials/src/main/java/androidx/credentials/CustomCredential.kt
index f4138ef..7550543 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CustomCredential.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CustomCredential.kt
@@ -21,6 +21,15 @@
 /**
  * Base class for a custom credential with which the user consented to authenticate to the app.
  *
+ * If you get a [CustomCredential] instead of a type-safe credential class such as
+ * [PasswordCredential], [PublicKeyCredential], etc., then you should check if
+ * you have any other library at interest that supports this custom [type] of credential,
+ * and if so use its parsing utilities to resolve to a type-safe class within that library.
+ *
+ *
+ * Note: The Bundle keys for [data] should not be in the form of `androidx.credentials.*` as they
+ * are reserved for internal use by this androidx library.
+ *
  * @property type the credential type determined by the credential-type-specific subclass for custom
  * use cases
  * @property data the credential data in the [Bundle] format for custom use cases
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetCredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/GetCredentialOption.kt
deleted file mode 100644
index e109b67..0000000
--- a/credentials/credentials/src/main/java/androidx/credentials/GetCredentialOption.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.credentials
-
-import android.os.Bundle
-import androidx.annotation.RestrictTo
-import androidx.credentials.internal.FrameworkClassParsingException
-
-/**
- * Base class for getting a specific type of credentials.
- *
- * [GetCredentialRequest] will be composed of a list of [GetCredentialOption] subclasses to indicate
- * the specific credential types and configurations that your app accepts.
- */
-abstract class GetCredentialOption internal constructor(
-    /** @hide */
-    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    open val type: String,
-    /** @hide */
-    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    open val requestData: Bundle,
-    /** @hide */
-    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    open val candidateQueryData: Bundle,
-    /** @hide */
-    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    open val requireSystemProvider: Boolean,
-) {
-    /** @hide */
-    companion object {
-        /** @hide */
-        @JvmStatic
-        fun createFrom(
-            type: String,
-            requestData: Bundle,
-            candidateQueryData: Bundle,
-            requireSystemProvider: Boolean
-        ): GetCredentialOption {
-            return try {
-                when (type) {
-                    PasswordCredential.TYPE_PASSWORD_CREDENTIAL ->
-                        GetPasswordOption.createFrom(requestData)
-                    PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
-                        when (requestData.getString(PublicKeyCredential.BUNDLE_KEY_SUBTYPE)) {
-                            GetPublicKeyCredentialOption
-                                .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION ->
-                                GetPublicKeyCredentialOption.createFrom(requestData)
-                            GetPublicKeyCredentialOptionPrivileged
-                                .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED ->
-                                GetPublicKeyCredentialOptionPrivileged.createFrom(requestData)
-                            else -> throw FrameworkClassParsingException()
-                        }
-                    else -> throw FrameworkClassParsingException()
-                }
-            } catch (e: FrameworkClassParsingException) {
-                // Parsing failed but don't crash the process. Instead just output a request with
-                // the raw framework values.
-                GetCustomCredentialOption(
-                    type, requestData, candidateQueryData, requireSystemProvider)
-            }
-        }
-    }
-}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt
index 3db5108..5dabedd 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt
@@ -19,39 +19,39 @@
 /**
  * Encapsulates a request to get a user credential.
  *
- * An application can construct such a request by adding one or more types of [GetCredentialOption],
- * and then call [CredentialManager.executeGetCredential] to launch framework UI flows to allow the user
+ * An application can construct such a request by adding one or more types of [CredentialOption],
+ * and then call [CredentialManager.getCredential] to launch framework UI flows to allow the user
  * to consent to using a previously saved credential for the given application.
  *
- * @property getCredentialOptions the list of [GetCredentialOption] from which the user can choose
+ * @property credentialOptions the list of [CredentialOption] from which the user can choose
  * one to authenticate to the app
  * @property isAutoSelectAllowed defines if a credential entry will be automatically chosen if it is
  * the only one, false by default
- * @throws IllegalArgumentException If [getCredentialOptions] is empty
+ * @throws IllegalArgumentException If [credentialOptions] is empty
  */
 class GetCredentialRequest @JvmOverloads constructor(
-    val getCredentialOptions: List<GetCredentialOption>,
+    val credentialOptions: List<CredentialOption>,
     val isAutoSelectAllowed: Boolean = false,
 ) {
 
     init {
-        require(getCredentialOptions.isNotEmpty()) { "credentialRequests should not be empty" }
+        require(credentialOptions.isNotEmpty()) { "credentialOptions should not be empty" }
     }
 
     /** A builder for [GetCredentialRequest]. */
     class Builder {
-        private var getCredentialOptions: MutableList<GetCredentialOption> = mutableListOf()
+        private var credentialOptions: MutableList<CredentialOption> = mutableListOf()
         private var autoSelectAllowed: Boolean = false
 
-        /** Adds a specific type of [GetCredentialOption]. */
-        fun addGetCredentialOption(getCredentialOption: GetCredentialOption): Builder {
-            getCredentialOptions.add(getCredentialOption)
+        /** Adds a specific type of [CredentialOption]. */
+        fun addCredentialOption(credentialOption: CredentialOption): Builder {
+            credentialOptions.add(credentialOption)
             return this
         }
 
-        /** Sets the list of [GetCredentialOption]. */
-        fun setGetCredentialOptions(getCredentialOptions: List<GetCredentialOption>): Builder {
-            this.getCredentialOptions = getCredentialOptions.toMutableList()
+        /** Sets the list of [CredentialOption]. */
+        fun setCredentialOptions(credentialOptions: List<CredentialOption>): Builder {
+            this.credentialOptions = credentialOptions.toMutableList()
             return this
         }
 
@@ -67,10 +67,10 @@
         /**
          * Builds a [GetCredentialRequest].
          *
-         * @throws IllegalArgumentException If [getCredentialOptions] is empty
+         * @throws IllegalArgumentException If [credentialOptions] is empty
          */
         fun build(): GetCredentialRequest {
-            return GetCredentialRequest(getCredentialOptions.toList(),
+            return GetCredentialRequest(credentialOptions.toList(),
                 autoSelectAllowed)
         }
     }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetCustomCredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/GetCustomCredentialOption.kt
index 2227eb7..546dae8 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetCustomCredentialOption.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetCustomCredentialOption.kt
@@ -21,13 +21,21 @@
 /**
  * Allows extending custom versions of GetCredentialOptions for unique use cases.
  *
+ * If you get a [GetCustomCredentialOption] instead of a type-safe option class such as
+ * [GetPasswordOption], [GetPublicKeyCredentialOption], etc., then you should check if
+ * you have any other library at interest that supports this custom [type] of credential option,
+ * and if so use its parsing utilities to resolve to a type-safe class within that library.
+ *
+ * Note: The Bundle keys for [requestData] and [candidateQueryData] should not be in the form of
+ * `androidx.credentials.*` as they are reserved for internal use by this androidx library.
+ *
  * @property type the credential type determined by the credential-type-specific subclass
  * generated for custom use cases
  * @property requestData the request data in the [Bundle] format, generated for custom use cases
  * @property candidateQueryData the partial request data in the [Bundle] format that will be sent to
  * the provider during the initial candidate query stage, which should not contain sensitive user
  * information
- * @property requireSystemProvider true if must only be fulfilled by a system provider and false
+ * @property isSystemProviderRequired true if must only be fulfilled by a system provider and false
  * otherwise
  * @throws IllegalArgumentException If [type] is empty
  * @throws NullPointerException If [requestData] or [type] is null
@@ -36,13 +44,12 @@
     final override val type: String,
     final override val requestData: Bundle,
     final override val candidateQueryData: Bundle,
-    @get:JvmName("requireSystemProvider")
-    final override val requireSystemProvider: Boolean
-) : GetCredentialOption(
+    final override val isSystemProviderRequired: Boolean
+) : CredentialOption(
     type,
     requestData,
     candidateQueryData,
-    requireSystemProvider
+    isSystemProviderRequired
 ) {
     init {
         require(type.isNotEmpty()) { "type should not be empty" }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetPasswordOption.kt b/credentials/credentials/src/main/java/androidx/credentials/GetPasswordOption.kt
index c49f6b8..2c182ea 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetPasswordOption.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetPasswordOption.kt
@@ -19,11 +19,11 @@
 import android.os.Bundle
 
 /** A request to retrieve the user's saved application password from their password provider. */
-class GetPasswordOption : GetCredentialOption(
+class GetPasswordOption : CredentialOption(
     type = PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
     requestData = Bundle(),
     candidateQueryData = Bundle(),
-    requireSystemProvider = false,
+    isSystemProviderRequired = false,
 ) {
     /** @hide */
     companion object {
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt
index 8a038d6..c8ae8f0 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt
@@ -17,7 +17,6 @@
 package androidx.credentials
 
 import android.os.Bundle
-import androidx.annotation.VisibleForTesting
 import androidx.credentials.internal.FrameworkClassParsingException
 
 /**
@@ -25,21 +24,21 @@
  *
  * @property requestJson the privileged request in JSON format in the standard webauthn web json
  * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
- * @property allowHybrid defines whether hybrid credentials are allowed to fulfill this request,
- * true by default, with hybrid credentials defined
- * [here](https://w3c.github.io/webauthn/#dom-authenticatortransport-hybrid)
+ * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
+ * immediately when there is no available credential instead of falling back to discovering remote
+ * credentials, and false (default) otherwise
  * @throws NullPointerException If [requestJson] is null
  * @throws IllegalArgumentException If [requestJson] is empty
  */
 class GetPublicKeyCredentialOption @JvmOverloads constructor(
     val requestJson: String,
-    @get:JvmName("allowHybrid")
-    val allowHybrid: Boolean = true,
-) : GetCredentialOption(
+    @get:JvmName("preferImmediatelyAvailableCredentials")
+    val preferImmediatelyAvailableCredentials: Boolean = false,
+) : CredentialOption(
     type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
-    requestData = toRequestDataBundle(requestJson, allowHybrid),
-    candidateQueryData = toRequestDataBundle(requestJson, allowHybrid),
-    requireSystemProvider = false
+    requestData = toRequestDataBundle(requestJson, preferImmediatelyAvailableCredentials),
+    candidateQueryData = toRequestDataBundle(requestJson, preferImmediatelyAvailableCredentials),
+    isSystemProviderRequired = false
 ) {
     init {
         require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
@@ -47,23 +46,25 @@
 
     /** @hide */
     companion object {
-        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID"
-        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
-        @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
-        const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION =
+        internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
+            "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
+        internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
+        internal const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION =
             "androidx.credentials.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION"
 
         @JvmStatic
-        internal fun toRequestDataBundle(requestJson: String, allowHybrid: Boolean): Bundle {
+        internal fun toRequestDataBundle(
+            requestJson: String,
+            preferImmediatelyAvailableCredentials: Boolean
+        ): Bundle {
             val bundle = Bundle()
             bundle.putString(
                 PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
                 BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION
             )
             bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
-            bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid)
+            bundle.putBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+                preferImmediatelyAvailableCredentials)
             return bundle
         }
 
@@ -73,8 +74,10 @@
         internal fun createFrom(data: Bundle): GetPublicKeyCredentialOption {
             try {
                 val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
-                val allowHybrid = data.get(BUNDLE_KEY_ALLOW_HYBRID)
-                return GetPublicKeyCredentialOption(requestJson!!, (allowHybrid!!) as Boolean)
+                val preferImmediatelyAvailableCredentials =
+                    data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
+                return GetPublicKeyCredentialOption(requestJson!!,
+                    (preferImmediatelyAvailableCredentials!!) as Boolean)
             } catch (e: Exception) {
                 throw FrameworkClassParsingException()
             }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOptionPrivileged.kt b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOptionPrivileged.kt
index 033cd65..4c30a64 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOptionPrivileged.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOptionPrivileged.kt
@@ -17,7 +17,6 @@
 package androidx.credentials
 
 import android.os.Bundle
-import androidx.annotation.VisibleForTesting
 import androidx.credentials.internal.FrameworkClassParsingException
 
 /**
@@ -28,27 +27,38 @@
  *
  * @property requestJson the privileged request in JSON format in the standard webauthn web json
  * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
- * @property allowHybrid defines whether hybrid credentials are allowed to fulfill this request,
- * true by default, with hybrid credentials defined
- * [here](https://w3c.github.io/webauthn/#dom-authenticatortransport-hybrid)
+ * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
+ * immediately when there is no available credential instead of falling back to discovering remote
+ * credentials, and false (default) otherwise
  * @property relyingParty the expected true RP ID which will override the one in the [requestJson],
  * where relyingParty is defined [here](https://w3c.github.io/webauthn/#rp-id) in more detail
  * @property clientDataHash a hash that is used to verify the [relyingParty] Identity
  * @throws NullPointerException If any of [requestJson], [relyingParty], or [clientDataHash]
  * is null
- * @throws IllegalArgumentException If any of [requestJson], [relyingParty], or [clientDataHash] is empty
+ * @throws IllegalArgumentException If any of [requestJson], [relyingParty], or [clientDataHash] is
+ * empty
  */
 class GetPublicKeyCredentialOptionPrivileged @JvmOverloads constructor(
     val requestJson: String,
     val relyingParty: String,
     val clientDataHash: String,
-    @get:JvmName("allowHybrid")
-    val allowHybrid: Boolean = true
-) : GetCredentialOption(
+    @get:JvmName("preferImmediatelyAvailableCredentials")
+    val preferImmediatelyAvailableCredentials: Boolean = false
+) : CredentialOption(
     type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
-    requestData = toBundle(requestJson, relyingParty, clientDataHash, allowHybrid),
-    candidateQueryData = toBundle(requestJson, relyingParty, clientDataHash, allowHybrid),
-    requireSystemProvider = false,
+    requestData = toBundle(
+        requestJson,
+        relyingParty,
+        clientDataHash,
+        preferImmediatelyAvailableCredentials
+    ),
+    candidateQueryData = toBundle(
+        requestJson,
+        relyingParty,
+        clientDataHash,
+        preferImmediatelyAvailableCredentials
+    ),
+    isSystemProviderRequired = false,
 ) {
 
     init {
@@ -62,9 +72,9 @@
         private var requestJson: String,
         private var relyingParty: String,
         private var clientDataHash: String
-        ) {
+    ) {
 
-        private var allowHybrid: Boolean = true
+        private var preferImmediatelyAvailableCredentials: Boolean = false
 
         /**
          * Sets the privileged request in JSON format.
@@ -75,11 +85,17 @@
         }
 
         /**
-         * Sets whether hybrid credentials are allowed to fulfill this request, true by default.
+         * Sets to true if you prefer the operation to return immediately when there is no available
+         * credential instead of falling back to discovering remote credentials, and false
+         * otherwise.
+         *
+         * The default value is false.
          */
         @Suppress("MissingGetterMatchingBuilder")
-        fun setAllowHybrid(allowHybrid: Boolean): Builder {
-            this.allowHybrid = allowHybrid
+        fun setPreferImmediatelyAvailableCredentials(
+            preferImmediatelyAvailableCredentials: Boolean
+        ): Builder {
+            this.preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
             return this
         }
 
@@ -101,24 +117,23 @@
 
         /** Builds a [GetPublicKeyCredentialOptionPrivileged]. */
         fun build(): GetPublicKeyCredentialOptionPrivileged {
-            return GetPublicKeyCredentialOptionPrivileged(this.requestJson,
-                this.relyingParty, this.clientDataHash, this.allowHybrid)
+            return GetPublicKeyCredentialOptionPrivileged(
+                this.requestJson,
+                this.relyingParty, this.clientDataHash, this.preferImmediatelyAvailableCredentials
+            )
         }
     }
 
     /** @hide */
     companion object {
-        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_RELYING_PARTY = "androidx.credentials.BUNDLE_KEY_RELYING_PARTY"
-        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_CLIENT_DATA_HASH =
+        internal const val BUNDLE_KEY_RELYING_PARTY =
+            "androidx.credentials.BUNDLE_KEY_RELYING_PARTY"
+        internal const val BUNDLE_KEY_CLIENT_DATA_HASH =
             "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
-        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID"
-        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
-        @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
-        const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED =
+        internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
+            "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
+        internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
+        internal const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED =
             "androidx.credentials.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION" +
                 "_PRIVILEGED"
 
@@ -127,7 +142,7 @@
             requestJson: String,
             relyingParty: String,
             clientDataHash: String,
-            allowHybrid: Boolean
+            preferImmediatelyAvailableCredentials: Boolean
         ): Bundle {
             val bundle = Bundle()
             bundle.putString(
@@ -137,24 +152,28 @@
             bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
             bundle.putString(BUNDLE_KEY_RELYING_PARTY, relyingParty)
             bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
-            bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid)
+            bundle.putBoolean(
+                BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+                preferImmediatelyAvailableCredentials
+            )
             return bundle
         }
 
         @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
-                                         // boolean value from being returned.
+        // boolean value from being returned.
         @JvmStatic
         internal fun createFrom(data: Bundle): GetPublicKeyCredentialOptionPrivileged {
             try {
                 val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
                 val rp = data.getString(BUNDLE_KEY_RELYING_PARTY)
                 val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH)
-                val allowHybrid = data.get(BUNDLE_KEY_ALLOW_HYBRID)
+                val preferImmediatelyAvailableCredentials =
+                    data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
                 return GetPublicKeyCredentialOptionPrivileged(
                     requestJson!!,
                     rp!!,
                     clientDataHash!!,
-                    (allowHybrid!!) as Boolean,
+                    (preferImmediatelyAvailableCredentials!!) as Boolean,
                 )
             } catch (e: Exception) {
                 throw FrameworkClassParsingException()
diff --git a/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt b/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
new file mode 100644
index 0000000..ccea9d2
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.internal
+
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import androidx.credentials.CreateCredentialRequest
+import androidx.credentials.CreatePasswordRequest
+import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.CreatePublicKeyCredentialRequestPrivileged
+import androidx.credentials.R
+
+/** @hide */
+@RequiresApi(23)
+class FrameworkImplHelper {
+    companion object {
+        /**
+         * Take the create request's `credentialData` and add SDK specific values to it.
+         *
+         * @hide
+         */
+        @JvmStatic
+        @RequiresApi(23)
+        fun getFinalCreateCredentialData(
+            request: CreateCredentialRequest,
+            activity: Context,
+        ): Bundle {
+            val createCredentialData = request.credentialData
+            val displayInfoBundle = request.displayInfo.toBundle()
+            displayInfoBundle.putParcelable(
+                CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_CREDENTIAL_TYPE_ICON,
+                Icon.createWithResource(
+                    activity,
+                    when (request) {
+                        is CreatePasswordRequest -> R.drawable.ic_password
+                        is CreatePublicKeyCredentialRequest -> R.drawable.ic_passkey
+                        is CreatePublicKeyCredentialRequestPrivileged -> R.drawable.ic_passkey
+                        else -> R.drawable.ic_other_sign_in
+                    }
+                )
+            )
+            createCredentialData.putBundle(
+                CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_REQUEST_DISPLAY_INFO,
+                displayInfoBundle
+            )
+            return createCredentialData
+        }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/res/drawable/ic_other_sign_in.xml b/credentials/credentials/src/main/res/drawable/ic_other_sign_in.xml
new file mode 100644
index 0000000..d1bb37e
--- /dev/null
+++ b/credentials/credentials/src/main/res/drawable/ic_other_sign_in.xml
@@ -0,0 +1,36 @@
+<!--
+  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.
+  -->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="VectorPath"
+    android:name="vector"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:name="path"
+        android:pathData="M 20 19 L 12 19 L 12 21 L 20 21 C 21.1 21 22 20.1 22 19 L 22 5 C 22 3.9 21.1 3 20 3 L 12 3 L 12 5 L 20 5 L 20 19 Z"
+        android:fillColor="#000"
+        android:strokeWidth="1"/>
+    <path
+        android:name="path_1"
+        android:pathData="M 12 7 L 10.6 8.4 L 13.2 11 L 8.85 11 C 8.42 9.55 7.09 8.5 5.5 8.5 C 3.57 8.5 2 10.07 2 12 C 2 13.93 3.57 15.5 5.5 15.5 C 7.09 15.5 8.42 14.45 8.85 13 L 13.2 13 L 10.6 15.6 L 12 17 L 17 12 L 12 7 Z M 5.5 13.5 C 4.67 13.5 4 12.83 4 12 C 4 11.17 4.67 10.5 5.5 10.5 C 6.33 10.5 7 11.17 7 12 C 7 12.83 6.33 13.5 5.5 13.5 Z"
+        android:fillColor="#000"
+        android:strokeWidth="1"/>
+</vector>
\ No newline at end of file
diff --git a/credentials/credentials/src/main/res/drawable/ic_passkey.xml b/credentials/credentials/src/main/res/drawable/ic_passkey.xml
new file mode 100644
index 0000000..9c4304e
--- /dev/null
+++ b/credentials/credentials/src/main/res/drawable/ic_passkey.xml
@@ -0,0 +1,32 @@
+<!--
+  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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="28dp"
+    android:height="24dp"
+    android:viewportWidth="28"
+    android:viewportHeight="24">
+    <path
+        android:pathData="M27.453,13.253C27.453,14.952 26.424,16.411 24.955,17.041L26.21,18.295L24.839,19.666L26.21,21.037L23.305,23.942L22.012,22.65L22.012,17.156C20.385,16.605 19.213,15.066 19.213,13.253C19.213,10.977 21.058,9.133 23.333,9.133C25.609,9.133 27.453,10.977 27.453,13.253ZM25.47,13.254C25.47,14.434 24.514,15.39 23.334,15.39C22.154,15.39 21.197,14.434 21.197,13.254C21.197,12.074 22.154,11.118 23.334,11.118C24.514,11.118 25.47,12.074 25.47,13.254Z"
+        android:fillColor="#00639B"
+        android:fillType="evenOdd"/>
+    <path
+        android:pathData="M17.85,5.768C17.85,8.953 15.268,11.536 12.083,11.536C8.897,11.536 6.315,8.953 6.315,5.768C6.315,2.582 8.897,0 12.083,0C15.268,0 17.85,2.582 17.85,5.768Z"
+        android:fillColor="#00639B"/>
+    <path
+        android:pathData="M0.547,20.15C0.547,16.32 8.23,14.382 12.083,14.382C13.59,14.382 15.684,14.679 17.674,15.269C18.116,16.454 18.952,17.447 20.022,18.089V23.071H0.547V20.15Z"
+        android:fillColor="#00639B"/>
+</vector>
\ No newline at end of file
diff --git a/credentials/credentials/src/main/res/drawable/ic_password.xml b/credentials/credentials/src/main/res/drawable/ic_password.xml
new file mode 100644
index 0000000..1fb71cf
--- /dev/null
+++ b/credentials/credentials/src/main/res/drawable/ic_password.xml
@@ -0,0 +1,31 @@
+<!--
+  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.
+  -->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="VectorPath"
+    android:name="vector"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:name="path"
+        android:pathData="M 8.71 10.29 C 8.52 10.1 8.28 10 8 10 L 7.75 10 L 7.75 8.75 C 7.75 7.98 7.48 7.33 6.95 6.8 C 6.42 6.27 5.77 6 5 6 C 4.23 6 3.58 6.27 3.05 6.8 C 2.52 7.33 2.25 7.98 2.25 8.75 L 2.25 10 L 2 10 C 1.72 10 1.48 10.1 1.29 10.29 C 1.1 10.48 1 10.72 1 11 L 1 16 C 1 16.28 1.1 16.52 1.29 16.71 C 1.48 16.9 1.72 17 2 17 L 8 17 C 8.28 17 8.52 16.9 8.71 16.71 C 8.9 16.52 9 16.28 9 16 L 9 11 C 9 10.72 8.9 10.48 8.71 10.29 Z M 6.25 10 L 3.75 10 L 3.75 8.75 C 3.75 8.4 3.87 8.1 4.11 7.86 C 4.35 7.62 4.65 7.5 5 7.5 C 5.35 7.5 5.65 7.62 5.89 7.86 C 6.13 8.1 6.25 8.4 6.25 8.75 L 6.25 10 Z M 10 14 L 23 14 L 23 16 L 10 16 Z M 21.5 9 C 21.102 9 20.721 9.158 20.439 9.439 C 20.158 9.721 20 10.102 20 10.5 C 20 10.898 20.158 11.279 20.439 11.561 C 20.721 11.842 21.102 12 21.5 12 C 21.898 12 22.279 11.842 22.561 11.561 C 22.842 11.279 23 10.898 23 10.5 C 23 10.102 22.842 9.721 22.561 9.439 C 22.279 9.158 21.898 9 21.5 9 Z M 16.5 9 C 16.102 9 15.721 9.158 15.439 9.439 C 15.158 9.721 15 10.102 15 10.5 C 15 10.898 15.158 11.279 15.439 11.561 C 15.721 11.842 16.102 12 16.5 12 C 16.898 12 17.279 11.842 17.561 11.561 C 17.842 11.279 18 10.898 18 10.5 C 18 10.102 17.842 9.721 17.561 9.439 C 17.279 9.158 16.898 9 16.5 9 Z M 11.5 9 C 11.102 9 10.721 9.158 10.439 9.439 C 10.158 9.721 10 10.102 10 10.5 C 10 10.898 10.158 11.279 10.439 11.561 C 10.721 11.842 11.102 12 11.5 12 C 11.898 12 12.279 11.842 12.561 11.561 C 12.842 11.279 13 10.898 13 10.5 C 13 10.102 12.842 9.721 12.561 9.439 C 12.279 9.158 11.898 9 11.5 9 Z"
+        android:fillColor="#000"
+        android:strokeWidth="1"/>
+</vector>
\ No newline at end of file
diff --git a/credentials/credentials/src/main/res/values-af/strings.xml b/credentials/credentials/src/main/res/values-af/strings.xml
new file mode 100644
index 0000000..324d169
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-af/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Wagwoordsleutel"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Wagwoord"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-am/strings.xml b/credentials/credentials/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000..becd4e28
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-am/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"የይለፍ ቁልፍ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"የይለፍ ቃል"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ar/strings.xml b/credentials/credentials/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000..0af8c23
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ar/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"مفتاح المرور"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"كلمة المرور"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-as/strings.xml b/credentials/credentials/src/main/res/values-as/strings.xml
new file mode 100644
index 0000000..54f51cb
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-as/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"পাছকী"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"পাছৱৰ্ড"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-az/strings.xml b/credentials/credentials/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000..8c1756f
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-az/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Giriş açarı"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Parol"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-b+sr+Latn/strings.xml b/credentials/credentials/src/main/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..9cf1ec4
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Pristupni kôd"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Lozinka"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-be/strings.xml b/credentials/credentials/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000..4bd1021
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-be/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Ключ доступу"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Пароль"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-bg/strings.xml b/credentials/credentials/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000..262756f
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-bg/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Код за достъп"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Парола"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-bn/strings.xml b/credentials/credentials/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000..ba552e9
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-bn/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"পাসকী"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"পাসওয়ার্ড"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-bs/strings.xml b/credentials/credentials/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000..d1bd317
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-bs/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Pristupni ključ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Lozinka"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ca/strings.xml b/credentials/credentials/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000..ee5660e
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ca/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Clau d\'accés"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Contrasenya"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-cs/strings.xml b/credentials/credentials/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000..6d618e8
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-cs/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Přístupový klíč"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Heslo"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-da/strings.xml b/credentials/credentials/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000..f8c9e29
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-da/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Adgangsnøgle"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Adgangskode"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-de/strings.xml b/credentials/credentials/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000..49d54d2
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-de/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passkey"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Passwort"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-el/strings.xml b/credentials/credentials/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000..5c2d73e
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-el/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Κλειδί πρόσβασης"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Κωδικός πρόσβασης"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-en-rAU/strings.xml b/credentials/credentials/src/main/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..b9f9ba1
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-en-rAU/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passkey"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Password"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-en-rCA/strings.xml b/credentials/credentials/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..b9f9ba1
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passkey"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Password"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-en-rGB/strings.xml b/credentials/credentials/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..b9f9ba1
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passkey"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Password"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-en-rIN/strings.xml b/credentials/credentials/src/main/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..b9f9ba1
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-en-rIN/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passkey"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Password"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-en-rXC/strings.xml b/credentials/credentials/src/main/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..ee94f40
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-en-rXC/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‏‎‏‎‎‎‎‏‏‎‏‎‏‎‏‎‏‎‎‏‏‎‎‏‏‏‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‎‏‎‏‎‏‎‏‏‏‏‏‏‎‏‏‎‎Passkey‎‏‎‎‏‎"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‎‎‏‎‎‎‎‎‏‏‎‎‎‏‎‎‏‎‎‏‎‏‏‎‏‏‏‎‎‏‏‎‏‎‏‎‏‏‎‏‏‏‏‏‏‏‏‎‏‎‎‎‏‏‎Password‎‏‎‎‏‎"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-es-rUS/strings.xml b/credentials/credentials/src/main/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..898ed7d9
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-es-rUS/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Llave de acceso"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Contraseña"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-es/strings.xml b/credentials/credentials/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000..898ed7d9
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-es/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Llave de acceso"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Contraseña"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-et/strings.xml b/credentials/credentials/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000..294c693
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-et/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Pääsuvõti"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Parool"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-eu/strings.xml b/credentials/credentials/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000..b1db172
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-eu/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Sarbide-gakoa"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Pasahitza"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-fa/strings.xml b/credentials/credentials/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000..f1b8f5f
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-fa/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"گذرکلید"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"گذرواژه"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-fi/strings.xml b/credentials/credentials/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000..8adce03
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-fi/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Avainkoodi"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Salasana"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-fr-rCA/strings.xml b/credentials/credentials/src/main/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..555784b
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-fr-rCA/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Clé d\'accès"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Mot de passe"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-fr/strings.xml b/credentials/credentials/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000..555784b
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-fr/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Clé d\'accès"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Mot de passe"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-gl/strings.xml b/credentials/credentials/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000..6bf8e92
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-gl/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Clave de acceso"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Contrasinal"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-gu/strings.xml b/credentials/credentials/src/main/res/values-gu/strings.xml
new file mode 100644
index 0000000..d63c6d9
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-gu/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"પાસકી"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"પાસવર્ડ"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-hi/strings.xml b/credentials/credentials/src/main/res/values-hi/strings.xml
new file mode 100644
index 0000000..bbcc98c
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-hi/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"पासकी"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"पासवर्ड"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-hr/strings.xml b/credentials/credentials/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000..6edcb91
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-hr/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Pristupni ključ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Zaporka"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-hu/strings.xml b/credentials/credentials/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000..c0e0cb3
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-hu/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Azonosítókulcs"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Jelszó"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-hy/strings.xml b/credentials/credentials/src/main/res/values-hy/strings.xml
new file mode 100644
index 0000000..617300a
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-hy/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Անցաբառ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Գաղտնաբառ"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-in/strings.xml b/credentials/credentials/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000..1683197
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-in/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Kunci sandi"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Sandi"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-is/strings.xml b/credentials/credentials/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000..e98a644
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-is/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Aðgangslykill"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Aðgangsorð"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-it/strings.xml b/credentials/credentials/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000..b9f9ba1
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-it/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passkey"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Password"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-iw/strings.xml b/credentials/credentials/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000..3ddedd6
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-iw/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"מפתח גישה"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"סיסמה"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ja/strings.xml b/credentials/credentials/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000..2e926be
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ja/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"パスキー"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"パスワード"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ka/strings.xml b/credentials/credentials/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000..40c308b
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ka/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"წვდომის გასაღები"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"პაროლი"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-kk/strings.xml b/credentials/credentials/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000..592eff2
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-kk/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Кіру кілті"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Құпия сөз"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-km/strings.xml b/credentials/credentials/src/main/res/values-km/strings.xml
new file mode 100644
index 0000000..0aa413a
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-km/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"កូដសម្ងាត់"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"ពាក្យសម្ងាត់"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-kn/strings.xml b/credentials/credentials/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000..c57f6209
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-kn/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"ಪಾಸ್‌ಕೀ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"ಪಾಸ್‌ವರ್ಡ್"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ko/strings.xml b/credentials/credentials/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000..dc494ec
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ko/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"패스키"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"비밀번호"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ky/strings.xml b/credentials/credentials/src/main/res/values-ky/strings.xml
new file mode 100644
index 0000000..3366129
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ky/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Мүмкүндүк алуу ачкычы"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Сырсөз"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-lo/strings.xml b/credentials/credentials/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000..d642614
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-lo/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"ກະແຈຜ່ານ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"ລະຫັດຜ່ານ"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-lt/strings.xml b/credentials/credentials/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000..ee87b96
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-lt/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passkey"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Slaptažodis"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-lv/strings.xml b/credentials/credentials/src/main/res/values-lv/strings.xml
new file mode 100644
index 0000000..9d79e3a
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-lv/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Piekļuves atslēga"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Parole"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-mk/strings.xml b/credentials/credentials/src/main/res/values-mk/strings.xml
new file mode 100644
index 0000000..96f6efe
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-mk/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Криптографски клуч"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Лозинка"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ml/strings.xml b/credentials/credentials/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000..e272342
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ml/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"പാസ്‌കീ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"പാസ്‌വേഡ്"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-mn/strings.xml b/credentials/credentials/src/main/res/values-mn/strings.xml
new file mode 100644
index 0000000..59e9ded
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-mn/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passkey"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Нууц үг"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-mr/strings.xml b/credentials/credentials/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000..bbcc98c
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-mr/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"पासकी"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"पासवर्ड"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ms/strings.xml b/credentials/credentials/src/main/res/values-ms/strings.xml
new file mode 100644
index 0000000..aacb31b
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ms/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Kunci laluan"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Kata Laluan"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-my/strings.xml b/credentials/credentials/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000..3ea63a4
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-my/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"လျှို့ဝှက်ကီး"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"စကားဝှက်"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-nb/strings.xml b/credentials/credentials/src/main/res/values-nb/strings.xml
new file mode 100644
index 0000000..a72318a
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-nb/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Tilgangsnøkkel"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Passord"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ne/strings.xml b/credentials/credentials/src/main/res/values-ne/strings.xml
new file mode 100644
index 0000000..bbcc98c
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ne/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"पासकी"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"पासवर्ड"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-nl/strings.xml b/credentials/credentials/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000..db3ba80
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-nl/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Toegangssleutel"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Wachtwoord"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-or/strings.xml b/credentials/credentials/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000..6f31314
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-or/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"ପାସକୀ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"ପାସୱାର୍ଡ"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-pa/strings.xml b/credentials/credentials/src/main/res/values-pa/strings.xml
new file mode 100644
index 0000000..ca4c10a
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-pa/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"ਪਾਸਕੀ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"ਪਾਸਵਰਡ"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-pl/strings.xml b/credentials/credentials/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000..7c4d4c1
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-pl/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Klucz"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Hasło"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-pt-rBR/strings.xml b/credentials/credentials/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..554e9b8
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Chave de acesso"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Senha"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-pt-rPT/strings.xml b/credentials/credentials/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..f405d93
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Chave de acesso"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Palavra-passe"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-pt/strings.xml b/credentials/credentials/src/main/res/values-pt/strings.xml
new file mode 100644
index 0000000..554e9b8
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-pt/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Chave de acesso"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Senha"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ro/strings.xml b/credentials/credentials/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000..9748df0
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ro/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Cheie de acces"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Parolă"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ru/strings.xml b/credentials/credentials/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000..d3e3ce4
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ru/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Ключ доступа"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Пароль"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-si/strings.xml b/credentials/credentials/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000..4eee3ad
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-si/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"මුරයතුර"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"මුරපදය"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-sk/strings.xml b/credentials/credentials/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000..f92dc9a
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-sk/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Prístupový kľúč"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Heslo"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-sl/strings.xml b/credentials/credentials/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000..38907ef
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-sl/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Ključ za dostop"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Geslo"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-sq/strings.xml b/credentials/credentials/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000..00f2ae3
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-sq/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Çelësi i kalimit"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Fjalëkalimi"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-sr/strings.xml b/credentials/credentials/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000..0e0be5e
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-sr/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Приступни кôд"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Лозинка"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-sv/strings.xml b/credentials/credentials/src/main/res/values-sv/strings.xml
new file mode 100644
index 0000000..808fd8c
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-sv/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Nyckel"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Lösenord"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-sw/strings.xml b/credentials/credentials/src/main/res/values-sw/strings.xml
new file mode 100644
index 0000000..a129b58
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-sw/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Ufunguo wa siri"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Nenosiri"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ta/strings.xml b/credentials/credentials/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000..458bcb4
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ta/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"கடவுக்குறியீடு"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"கடவுச்சொல்"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-te/strings.xml b/credentials/credentials/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000..67ad9ab
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-te/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"పాస్-కీ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"పాస్‌వర్డ్"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-th/strings.xml b/credentials/credentials/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000..e2b685f
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-th/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"พาสคีย์"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"รหัสผ่าน"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-tl/strings.xml b/credentials/credentials/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000..b9f9ba1
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-tl/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passkey"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Password"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-tr/strings.xml b/credentials/credentials/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000..f00b298
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-tr/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Şifre anahtarı"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Şifre"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-uk/strings.xml b/credentials/credentials/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000..4bd1021
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-uk/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Ключ доступу"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Пароль"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ur/strings.xml b/credentials/credentials/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000..3183ec3
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ur/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"پاس کی"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"پاس ورڈ"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-uz/strings.xml b/credentials/credentials/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000..7f1bb8c
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-uz/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Kod"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Parol"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-vi/strings.xml b/credentials/credentials/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000..28a4e5a
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-vi/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Mã xác thực"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Mật khẩu"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-zh-rCN/strings.xml b/credentials/credentials/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..8f3d028
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"通行密钥"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"密码"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-zh-rHK/strings.xml b/credentials/credentials/src/main/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..6111c53
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-zh-rHK/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"密鑰"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"密碼"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-zh-rTW/strings.xml b/credentials/credentials/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..884239a
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"密碼金鑰"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"密碼"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-zu/strings.xml b/credentials/credentials/src/main/res/values-zu/strings.xml
new file mode 100644
index 0000000..f14c8e8
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-zu/strings.xml
@@ -0,0 +1,22 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Ukhiye wokudlula"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Iphasiwedi"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values/strings.xml b/credentials/credentials/src/main/res/values/strings.xml
new file mode 100644
index 0000000..2fef094
--- /dev/null
+++ b/credentials/credentials/src/main/res/values/strings.xml
@@ -0,0 +1,23 @@
+<?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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Credential type label for passkey -->
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL">Passkey</string>
+    <!-- Credential type label for password -->
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL">Password</string>
+</resources>
\ No newline at end of file
diff --git a/datastore/datastore-core/build.gradle b/datastore/datastore-core/build.gradle
index 71aa61d..e534a6b 100644
--- a/datastore/datastore-core/build.gradle
+++ b/datastore/datastore-core/build.gradle
@@ -101,6 +101,9 @@
                 implementation(libs.kotlinTest)
                 implementation(project(":internal-testutils-kmp"))
                 implementation(project(":internal-testutils-datastore"))
+
+                // Workaround bug in 1.8.0, was supposed be fixed in RC2/final, but apparently not.
+                implementation(libs.kotlinTestJunit)
             }
         }
 
@@ -112,6 +115,9 @@
                 implementation(project(":internal-testutils-truth"))
                 implementation(libs.testRunner)
                 implementation(libs.testCore)
+
+                // Workaround bug in 1.8.0, was supposed be fixed in RC2/final, but apparently not.
+                implementation(libs.kotlinTestJunit)
             }
         }
 
diff --git a/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessDataStore.kt b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessDataStore.kt
index add408e..735bad81 100644
--- a/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessDataStore.kt
+++ b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessDataStore.kt
@@ -78,8 +78,13 @@
          * Final. ReadException can transition to another ReadException, Data or Final.
          * Data can transition to another Data or Final. Final will not change.
          */
-        // TODO(b/241290444): avoid coroutine switching by loading native lib during initialization
-        val latestVersionAtRead = withContext(scope.coroutineContext) { sharedCounter.getValue() }
+        // Only switch coroutine if sharedCounter is not initialized because initialization incurs
+        // disk IO
+        val latestVersionAtRead =
+            if (lazySharedCounter.isInitialized()) sharedCounter.getValue() else
+                withContext(scope.coroutineContext) {
+                    sharedCounter.getValue()
+                }
         val currentDownStreamFlowState = downstreamFlow.value
 
         if ((currentDownStreamFlowState !is Data) ||
@@ -138,7 +143,8 @@
     private val INVALID_VERSION = -1
     private var initTasks: List<suspend (api: InitializerApi<T>) -> Unit>? =
         initTasksList.toList()
-    private val sharedCounter: SharedCounter by lazy {
+
+    private val lazySharedCounter = lazy {
         SharedCounter.loadLib()
         SharedCounter.create {
             val versionFile = fileWithSuffix(VERSION_SUFFIX)
@@ -146,6 +152,8 @@
             versionFile
         }
     }
+    private val sharedCounter by lazySharedCounter
+
     private val threadLock = Mutex()
     private val storageConnection: StorageConnection<T> by lazy {
         storage.createConnection()
@@ -502,7 +510,8 @@
                         lock = lockFileStream.getChannel().tryLock(
                             /* position= */ 0L,
                             /* size= */ Long.MAX_VALUE,
-                            /* shared= */ true)
+                            /* shared= */ true
+                        )
                     } catch (ex: IOException) {
                         // TODO(b/255419657): Update the shared lock IOException handling logic for
                         // KMM.
diff --git a/datastore/datastore-core/src/jvmTest/kotlin/androidx/datastore/core/SingleProcessDataStoreStressTest.kt b/datastore/datastore-core/src/jvmTest/kotlin/androidx/datastore/core/SingleProcessDataStoreStressTest.kt
index ad91e3b..a4f5183 100644
--- a/datastore/datastore-core/src/jvmTest/kotlin/androidx/datastore/core/SingleProcessDataStoreStressTest.kt
+++ b/datastore/datastore-core/src/jvmTest/kotlin/androidx/datastore/core/SingleProcessDataStoreStressTest.kt
@@ -27,7 +27,7 @@
 import kotlin.test.Ignore
 import kotlin.test.Test
 import kotlin.time.ExperimentalTime
-import kotlin.time.seconds
+import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.Job
diff --git a/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
index b159b92..c7cb50e 100644
--- a/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
+++ b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
@@ -54,7 +54,7 @@
      * @param currentData the most recently persisted data
      * @return a Single of the updated data
      */
-    @Suppress("UPPER_BOUND_VIOLATED_BASED_ON_JAVA_ANNOTATIONS")
+    @Suppress("UPPER_BOUND_VIOLATED")
     public fun migrate(sharedPreferencesView: SharedPreferencesView, currentData: T): Single<T>
 }
 
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 3e8db97..b864500 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -76,7 +76,7 @@
 [0-9]+ problem.* found storing the configuration cache.*
 plus [0-9]+ more problems\. Please see the report for details\.
 # https://youtrack.jetbrains.com/issue/KT-43293 fixed in Kotlin 1.8.0
-\- Task \`((\:[a-z1-9-]+)+)?\:[a-zA-Z1-9]+\` of type \`org\.jetbrains(\.[a-zA-Z]+)+\.(CInteropCommonizerTask|KotlinNativeCompile|KotlinNativeLink|KotlinNativeHostTest|CInteropMetadataDependencyTransformationTask|GenerateProjectStructureMetadata|TransformKotlinGranularMetadata|NativeDistributionCommonizerTask)\`\: invocation of \'Task\.project\' at execution time is unsupported\.
+\- Task \`((\:[a-z1-9-]+)+)?\:[a-zA-Z1-9]+\` of type \`org\.jetbrains(\.[a-zA-Z]+)+\.(CInteropCommonizerTask|KotlinNativeCompile|KotlinNativeLink|KotlinNativeHostTest|CInteropMetadataDependencyTransformationTask|GenerateProjectStructureMetadata|TransformKotlinGranularMetadata|NativeDistributionCommonizerTask|transformNonJvmMainDependenciesMetadata|MetadataDependencyTransformationTask)\`\: invocation of \'Task\.project\' at execution time is unsupported\.
 \- Task \`((\:[a-z1-9-]+)+)?\:[a-zA-Z1-9]+\` of type \`org\.jetbrains\.kotlin\.gradle\.targets\.native\.internal\.(CInteropMetadataDependencyTransformationTask|NativeDistributionCommonizerTask)\`: cannot serialize object of type \'org\.gradle(\.[a-zA-Z]+)+\'\, a subtype of \'org\.gradle(\.[a-zA-Z]+)+\'\, as these are not supported with the configuration cache\.
 # https://youtrack.jetbrains.com/issue/KT-54627
 \- Task \`\:commonizeNativeDistribution\` of type \`org\.jetbrains\.kotlin\.gradle\.targets\.native\.internal\.NativeDistributionCommonizerTask\`\: error writing value of type \'java\.util\.concurrent\.locks\.ReentrantLock\'
diff --git a/development/importMaven/build.gradle.kts b/development/importMaven/build.gradle.kts
index 38f9bc1..86205a6 100644
--- a/development/importMaven/build.gradle.kts
+++ b/development/importMaven/build.gradle.kts
@@ -64,10 +64,6 @@
     testImplementation(importMavenLibs.okioFakeFilesystem)
 }
 
-tasks.withType<KotlinCompile> {
-    kotlinOptions.jvmTarget = "11"
-}
-
 // b/250726951 Gradle ProjectBuilder needs reflection access to java.lang.
 val jvmAddOpensArgs = listOf("--add-opens=java.base/java.lang=ALL-UNNAMED")
 tasks.withType<Test>() {
diff --git a/development/importMaven/src/main/kotlin/androidx/build/importMaven/ImportVersionCatalog.kt b/development/importMaven/src/main/kotlin/androidx/build/importMaven/ImportVersionCatalog.kt
index 93a8175..7c554df 100644
--- a/development/importMaven/src/main/kotlin/androidx/build/importMaven/ImportVersionCatalog.kt
+++ b/development/importMaven/src/main/kotlin/androidx/build/importMaven/ImportVersionCatalog.kt
@@ -46,7 +46,6 @@
             Interners.newStrongInterner(),
             Interners.newStrongInterner(),
             project.objects,
-            project.providers,
             { error("Not supported") },
             configurations
         )
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index ef767e5..99ec90b 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-rc01")
+    docs("androidx.arch.core:core-runtime:2.2.0-rc01")
+    docs("androidx.arch.core:core-testing:2.2.0-rc01")
     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.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.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-alpha05")
+    samples("androidx.compose.material3:material3-samples:1.1.0-alpha05")
+    docs("androidx.compose.material3:material3-window-size-class:1.1.0-alpha05")
+    samples("androidx.compose.material3:material3-window-size-class-samples:1.1.0-alpha05")
+    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..498ff9f 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -18,7 +18,9 @@
     docs(project(":ads:ads-identifier-testing"))
     kmpDocs(project(":annotation:annotation"))
     docs(project(":annotation:annotation-experimental"))
+    docs(project(":appactions:interaction:interaction-capabilities-core"))
     docs(project(":appactions:interaction:interaction-proto"))
+    docs(project(":appactions:interaction:interaction-service"))
     docs(project(":appcompat:appcompat"))
     docs(project(":appcompat:appcompat-resources"))
     docs(project(":appsearch:appsearch"))
@@ -249,6 +251,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/docs/api_guidelines/compat.md b/docs/api_guidelines/compat.md
index 3b2a251..933a754 100644
--- a/docs/api_guidelines/compat.md
+++ b/docs/api_guidelines/compat.md
@@ -245,22 +245,57 @@
 }
 ```
 
-### Inter-process communication {#inter-process-communication}
+### Inter-process communication {#ipc}
 
 Protocols and data structures used for IPC must support interoperability between
-different versions of libraries and should be treated similarly to public API.
+different versions of libraries and should be treated similarly to public API;
+however, AndroidX does not currently implement compatibility tracking for IPC.
 
-#### Data structures
+We recommend the following, in order of preference:
 
-**Do not** use `Parcelable` for any class that may be used for IPC or otherwise
-exposed as public API. The wire format used by `Parcelable` does not provide any
-compatibility guarantees and will result in crashes if fields are added or
-removed between library versions.
+1.  Stable AIDL if (1) your project lives partially in the Android platform and
+    has access to Stable AIDL build rules and (2) you need to support Android's
+    `Parcelable` data types. The AndroidX workflow **does not** provide Stable
+    AIDL compilation or compatibility checks, so these would need to happen in
+    the platform build and the resulting `.java` files would need to be copied
+    out.
+2.  Protobuf if (1) your project needs to persist data to disk or (2) you need
+    interoperability with systems already using Protobuf. Similar to Stable
+    AIDL, the AndroidX workflow **does not** provide built-in support Protobuf
+    compilation or compatibility checks. It is possible to use a Proto plug-in,
+    but you will be responsible for bundling the runtime and maintaining
+    compatibility on your own.
+3.  Bundle if you have a very simple data model that is unlikely to change in
+    the future. Bundle has the weakest type safety and compatibility guarantees
+    of any recommendation, and it has many caveats that make it a poor choice.
+4.  `VersionedParcelable` if your project is already using Versioned Parcelable
+    and is aware of its compatibility constraints.
+
+We are currently evaluating Square's [Wire](https://github.com/square/wire) and
+Google's [gRPC](https://grpc.io/) libraries for recommendation. If either of
+these libraries meets your team's needs based on your own research, feel free to
+use them.
 
 **Do not** design your own serialization mechanism or wire format for disk
 storage or inter-process communication. Preserving and verifying compatibility
 is difficult and error-prone.
 
+In all cases, **do not** expose your serialization mechanism in your API
+surface. Neither Stable AIDL nor Protobuf generate stable language APIs.
+
+#### Parcelable {#ipc-parcelable}
+
+**Do not** implement `Parcelable` for any class that may be used for IPC or
+otherwise exposed as public API. By default, `Parcelable` does not provide any
+compatibility guarantees and will result in crashes if fields are added or
+removed between library versions. If you are using Stable AIDL, you *may* use
+AIDL-defined parcelables for IPC but not public API.
+
+NOTE As of 2022/12/16, we are working on experimental support for compiling and
+tracking Stable AIDL definitions within the AndroidX workflow.
+
+#### Protobuf {#ipc-protobuf}
+
 Developers **should** use protocol buffers for most cases. See
 [Protobuf](#dependencies-protobuf) for more information on using protocol
 buffers in your library. **Do** use protocol buffers if your data structure is
@@ -268,6 +303,14 @@
 `Binder`s, or other platform-defined `Parcelable` data structures, they will
 need to be stored alongside the protobuf bytes in a `Bundle`.
 
+NOTE We are currently investigating the suitability of Square's
+[`wire` library](https://github.com/square/wire) for handling protocol buffers
+in Android libraries. If adopted, it will replace `proto` library dependencies.
+Libraries that expose their serialization mechanism in their API surface *will
+not be able to migrate*.
+
+#### Bundle {#ipc-bundle}
+
 Developers **may** use `Bundle` in simple cases that require sending `Binder`s,
 `FileDescriptor`s, or platform `Parcelable`s across IPC
 ([example](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java;l=820)).
@@ -290,19 +333,7 @@
     are responsible for providing their own system for guaranteeing wire format
     compatibility between versions.
 
-Developers **may** use `VersionedParcelable` in cases where they are already
-using the library and understand its limitations.
-
-In all cases, **do not** expose your serialization mechanism in your API
-surface.
-
-NOTE We are currently investigating the suitability of Square's
-[`wire` library](https://github.com/square/wire) for handling protocol buffers
-in Android libraries. If adopted, it will replace `proto` library dependencies.
-Libraries that expose their serialization mechanism in their API surface *will
-not be able to migrate*.
-
-#### Communication protocols
+#### Communication protocols {#ipc-protocol}
 
 Any communication prototcol, handshake, etc. must maintain compatibility
 consistent with SemVer guidelines. Consider how your protocol will handle
diff --git a/docs/api_guidelines/dependencies.md b/docs/api_guidelines/dependencies.md
index bd0bfe9..83bdad6 100644
--- a/docs/api_guidelines/dependencies.md
+++ b/docs/api_guidelines/dependencies.md
@@ -95,8 +95,9 @@
     more individual owners (e.g. NOT a group alias)
 *   Library **must** be approved by legal
 
-Please see Jetpack's [open-source policy page](open_source.md) for more details
-on using third-party libraries.
+Please see Jetpack's
+[open-source policy page](/company/teams/androidx/open_source.md) for more
+details on using third-party libraries.
 
 ### Types of dependencies {#dependencies-types}
 
@@ -145,8 +146,16 @@
 between artifacts
 ([example](https://android-review.googlesource.com/c/platform/frameworks/support/+/2086029)).
 
-`core/core-ktx/build.gradle`: `dependencies { // Atomic group constraints {
-implementation(project(":core:core")) } }`
+`core/core-ktx/build.gradle`:
+
+```
+dependencies {
+    // Atomic group
+    constraints {
+        implementation(project(":core:core"))
+    }
+}
+```
 
 In *extremely* rare cases, libraries may need to define a constraint on a
 project that is not in its `studiow` project set, ex. a constraint between the
@@ -155,10 +164,16 @@
 ([example](https://android-review.googlesource.com/c/platform/frameworks/support/+/2160202))
 to indicate the tip-of-tree version.
 
-`paging/paging-common.build.gradle`: `dependencies { // Atomic Group constraints
-{
-implementation("androidx.paging:paging-compose:${LibraryVersions.PAGING_COMPOSE}")
-}`
+`paging/paging-common/build.gradle`:
+
+```
+dependencies {
+    // Atomic group
+    constraints {
+        implementation("androidx.paging:paging-compose:${LibraryVersions.PAGING_COMPOSE}")
+    }
+}
+```
 
 ### System health {#dependencies-health}
 
@@ -184,16 +199,24 @@
 Kotlin's coroutine library adds around 100kB post-shrinking. New libraries that
 are written in Kotlin should prefer coroutines over `ListenableFuture`, but
 existing libraries must consider the size impact on their clients. See
-[Asynchronous work with return values](#async-return) for more details on using
-Kotlin coroutines in Jetpack libraries.
+[Asynchronous work with return values](/company/teams/androidx/api_guidelines#async-return)
+for more details on using Kotlin coroutines in Jetpack libraries.
 
 #### Guava {#dependencies-guava}
 
-The full Guava library is very large and *must not* be used. Libraries that
-would like to depend on Guava's `ListenableFuture` may instead depend on the
-standalone `com.google.guava:listenablefuture` artifact. See
-[Asynchronous work with return values](#async-return) for more details on using
-`ListenableFuture` in Jetpack libraries.
+The full Guava library is very large and should only be used in cases where
+there is a reasonable assumption that clients already depend on full Guava.
+
+For example, consider a library `androidx.foo:foo` implemented using Kotlin
+`suspend fun`s and an optional `androidx.foo:foo-guava` library that provides
+`ListenableFuture` interop wrappers with a direct dependency on
+`kotlinx.coroutines:kotlinx-coroutines-guava` and a transitive dependency on
+Guava.
+
+Libraries that only need `ListenableFuture` may instead depend on the standalone
+`com.google.guava:listenablefuture` artifact. See
+[Asynchronous work with return values](/company/teams/androidx/api_guidelines#async-return)
+for more details on using `ListenableFuture` in Jetpack libraries.
 
 #### Protobuf {#dependencies-protobuf}
 
diff --git a/docs/api_guidelines/deprecation.md b/docs/api_guidelines/deprecation.md
index 0ca80db..f5dc6d8 100644
--- a/docs/api_guidelines/deprecation.md
+++ b/docs/api_guidelines/deprecation.md
@@ -27,7 +27,14 @@
 
 APIs that are added during a pre-release cycle and marked as `@Deprecated`
 within the same cycle, e.g. added in `alpha01` and deprecated in `alpha06`,
-[must be removed](versioning.md#beta-checklist) before moving to `beta01`.
+[must be removed](/company/teams/androidx/versioning.md#beta-checklist) before
+moving to `beta01`.
+
+NOTE While some APIs can safely be removed without a deprecation cycle, a full
+cycle of deprecate (with replacement) and release prior to removal is *strongly
+recommended* for APIs that are likely to have clients, including APIs referenced
+by the Android platform build and `@RequiresOptIn` APIs that have shipped
+in a public beta.
 
 ### Soft removal (`@removed` or `DeprecationLevel.HIDDEN`)
 
diff --git a/docs/api_guidelines/misc.md b/docs/api_guidelines/misc.md
index 7ab2bd8..34bc0ee 100644
--- a/docs/api_guidelines/misc.md
+++ b/docs/api_guidelines/misc.md
@@ -435,6 +435,17 @@
 
 ## Kotlin-specific guidelines {#kotlin}
 
+Generally speaking, Kotlin code should follow the compatibility guidelines
+outlined at:
+
+-   The official Android Developers
+    [Kotlin-Java interop guide](https://developer.android.com/kotlin/interop)
+-   Android API guidelines for
+    [Kotlin-Java interop](https://android.googlesource.com/platform/developers/docs/+/refs/heads/master/api-guidelines/index.md#kotin-interop)
+-   Android API guidelines for
+    [asynchronous and non-blocking APIs](https://android.googlesource.com/platform/developers/docs/+/refs/heads/master/api-guidelines/async.md)
+-   Library-specific guidance outlined below
+
 ### Nullability
 
 #### Annotations on new Java APIs
diff --git a/docs/api_guidelines/modules.md b/docs/api_guidelines/modules.md
index 08e3433..7ec24f0 100644
--- a/docs/api_guidelines/modules.md
+++ b/docs/api_guidelines/modules.md
@@ -125,18 +125,23 @@
     library, e.g. `androidx.room:room-testing`
 *   `-core` for a low-level artifact that *may* contain public APIs but is
     primarily intended for use by other libraries in the group
-*   `-ktx` for an Kotlin artifact that exposes idiomatic Kotlin APIs as an
-    extension to a Java-only library (see
-    [additional -ktx guidance](#module-ktx))
+*   `-common` for a low-level, platform-agnostic Kotlin multi-platform artifact
+    intended for both client use and use by other libraries in the group
+*   `-ktx` for a Kotlin artifact that exposes idiomatic Kotlin APIs as an
+    extension to a Java-only library. Note that new modules should be written in
+    Kotlin rather than using `-ktx` artifacts.
 *   `-samples` for sample code which can be inlined in documentation (see
     [Sample code in Kotlin modules](#sample-code-in-kotlin-modules)
 *   `-<third-party>` for an artifact that integrates an optional third-party API
-    surface, e.g. `-proto` or `-rxjava2`. Note that a major version is included
-    in the sub-feature name for third-party API surfaces where the major version
-    indicates binary compatibility (only needed for post-1.x).
+    surface, e.g. `-proto`, `-guava`, or `-rxjava2`. This is common for Kotlin
+    libraries adapting their async APIs for Java clients. Note that a major
+    version is included in the sub-feature name (ex. `rxjava3`) for third-party
+    API surfaces where the major version indicates binary compatibility (only
+    needed for post-1.x).
 
 Artifacts **should not** use `-impl` or `-base` to indicate that a library is an
-implementation detail shared within the group. Instead, use `-core`.
+implementation detail shared within the group. Instead, use `-core` or `-common`
+as appropriate.
 
 #### Splitting existing modules
 
@@ -219,7 +224,18 @@
 
 There is one exception to the same-version policy: newly-added libraries within
 an atomic group may be "quarantined" from other libraries to allow for rapid
-iteration until they are API-stable.
+iteration until they are API-stable. For example:
+
+```groovy
+androidx {
+    name = "androidx.emoji2:emoji2-emojipicker"
+    mavenVersion = LibraryVersions.EMOJI2_QUARANTINE
+}
+```
+
+```groovy
+EMOJI2_QUARANTINE = "1.0.0-alpha01"
+```
 
 A quarantined library must stay within the `1.0.0-alphaXX` cycle until it is
 ready to conform to the same-version policy. While in quarantime, a library is
@@ -263,21 +279,24 @@
 external developers and should be considered a last-resort when backporting
 behavior is not feasible.
 
-### Kotlin extension `-ktx` libraries {#module-ktx}
+### Extension libraries (`-ktx`, `-guava`, etc.) {#module-ktx}
 
 New libraries should prefer Kotlin sources with built-in Java compatibility via
-`@JvmName` and other affordances of the Kotlin language; however, existing Java
-sourced libraries may benefit from extending their API surface with
-Kotlin-friendly APIs in a `-ktx` library.
+`@JvmName` and other affordances of the Kotlin language. They may optionally
+expose framework- or language-specific extension libraries like `-guava` or
+`-rxjava3`.
 
-A Kotlin extension library **may only** provide extensions for a single base
-library's API surface and its name **must** match the base library exactly. For
-example, `work:work-ktx` may only provide extensions for APIs exposed by
-`work:work`.
+Existing Java-sourced libraries may benefit from extending their API surface
+with Kotlin-friendly APIs in a `-ktx` extension library.
+
+Extension libraries **may only** provide extensions for a single base library's
+API surface and its name **must** match the base library exactly. For example,
+`work:work-ktx` may only provide extensions for APIs exposed by `work:work`.
 
 Additionally, an extension library **must** specify an `api`-type dependency on
 the base library and **must** be versioned and released identically to the base
 library.
 
-Kotlin extension libraries *should not* expose new functionality; they should
-only provide Kotlin-friendly versions of existing Java-facing functionality.
+Extension libraries *should not* expose new functionality; they should only
+provide language- or framework-friendly versions of existing library
+functionality.
diff --git a/docs/api_guidelines/resources.md b/docs/api_guidelines/resources.md
index 3290271..2220c81 100644
--- a/docs/api_guidelines/resources.md
+++ b/docs/api_guidelines/resources.md
@@ -183,7 +183,7 @@
  * never invoked.
  */
 public final class MetadataHolderService {
-  private MetadataHolderService() {}
+  public MetadataHolderService() {}
 
   @Override
   public IBinder onBind(Intent intent) {
diff --git a/docs/benchmarking.md b/docs/benchmarking.md
index f138231..ae5e3d6 100644
--- a/docs/benchmarking.md
+++ b/docs/benchmarking.md
@@ -9,7 +9,7 @@
 
 This page is for MICRO benchmarks measuring CPU performance of small sections of
 code. If you're looking for measuring startup or jank, see the guide for
-MACRObenchmarks [here](macrobenchmarking).
+MACRObenchmarks [here](/company/teams/androidx/macrobenchmarking.md).
 
 ### Writing the benchmark
 
diff --git a/docs/index.md b/docs/index.md
index a34d30b..6408bc9 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -14,10 +14,11 @@
 simplify and unify platform features, and other new features that target
 developer pain points.
 
-For frequently asked questions, see the [General FAQ](faq.md).
+For frequently asked questions, see the
+[General FAQ](/company/teams/androidx/faq.md).
 
-To get started developing in AndroidX, see the [Getting started](onboarding.md)
-guide.
+To get started developing in AndroidX, see the
+[Getting started](/company/teams/androidx/onboarding.md) guide.
 
 ## Quick links
 
@@ -27,4 +28,4 @@
 [public Issue Tracker component](http://issuetracker.google.com/issues/new?component=192731&template=842428)
 for duplicates first, then file against the appropriate sub-component according
 to the library package or infrastructure system. Learn more at
-[Issue tracking](onboarding.md#tracking-bugs).
+[Issue tracking](/company/teams/androidx/onboarding.md#tracking-bugs).
diff --git a/docs/issue_tracking.md b/docs/issue_tracking.md
index e8b9c84..f33022c 100644
--- a/docs/issue_tracking.md
+++ b/docs/issue_tracking.md
@@ -100,13 +100,13 @@
 ## Issue lifecycle
 
 1.  When an issue is reported, it is set to `Assigned` status for default
-    assignee (typically the [library owner](owners.md)) with a priority of
-    **P4**.
+    assignee (typically the [library owner](/company/teams/androidx/owners.md))
+    with a priority of **P4**.
 1.  Once an issue has been triaged by the assignee, its priority will be raised
     from **P4** according to severity.
 1.  The issue may still be reassigned at this point.
-    [Bug bounty](onboarding.md#bug-bounty) issues are likely to change
-    assignees.
+    [Bug bounty](/company/teams/androidx/onboarding.md#bug-bounty) issues are
+    likely to change assignees.
 1.  A status of **Accepted** means the assignee is actively working on the
     issue.
 1.  A status of **Fixed** means that the issue has been resolved in the
diff --git a/docs/kdoc_guidelines.md b/docs/kdoc_guidelines.md
index 0d7469c..fdea07c 100644
--- a/docs/kdoc_guidelines.md
+++ b/docs/kdoc_guidelines.md
@@ -93,7 +93,7 @@
 maintenance. You can use multiple samples per KDoc, with text in between
 explaining what the samples are showing. For more information on using
 `@sample`, see the
-[API guidelines](/company/teams/androidx/api_guidelines.md#sample-code-in-kotlin-modules).
+[API guidelines](/company/teams/androidx/api_guidelines/index.md#sample-code-in-kotlin-modules).
 
 ### Do not link to the same identifier inside documentation
 
diff --git a/docs/macrobenchmarking.md b/docs/macrobenchmarking.md
index a908ea9..86d0536 100644
--- a/docs/macrobenchmarking.md
+++ b/docs/macrobenchmarking.md
@@ -36,7 +36,7 @@
 for macrobenchmark explains how to use the library. This page focuses on
 specifics to writing library macrobenchmarks in the AndroidX repo. If you're
 looking for measuring CPU perf of individual functions, see the guide for
-MICRObenchmarks [here](benchmarking).
+MICRObenchmarks [here](/company/teams/androidx/benchmarking.md).
 
 ### Writing the benchmark
 
diff --git a/docs/manual_prebuilts_dance.md b/docs/manual_prebuilts_dance.md
index 0439d44..4dcbf63 100644
--- a/docs/manual_prebuilts_dance.md
+++ b/docs/manual_prebuilts_dance.md
@@ -1,7 +1,8 @@
 # The Manual Prebuilts Dance™
 
-NOTE There is also a [script](releasing_detailed.md#the-prebuilts-dance™) that
-automates this step.
+NOTE There is also a
+[script](/company/teams/androidx/releasing_detailed.md#the-prebuilts-dance™)
+that automates this step.
 
 Public-facing Jetpack library docs are built from prebuilts to reconcile our
 monolithic docs update process with our independently-versioned library release
diff --git a/docs/onboarding.md b/docs/onboarding.md
index 56fd9f3..358fdbd 100644
--- a/docs/onboarding.md
+++ b/docs/onboarding.md
@@ -6,9 +6,11 @@
 make simple changes in Android Studio, and upload commits to Gerrit for review.
 
 This page does **not** cover best practices for the content of changes. Please
-see [Life of a Jetpack Feature](loaf.md) for details on developing and releasing
-a library, [API Guidelines](api_guidelines/index.md) for best practices
-regarding public APIs and an overview of the constraints placed on changes.
+see [Life of a Jetpack Feature](/company/teams/androidx/loaf.md) for details on
+developing and releasing a library,
+[API Guidelines](/company/teams/androidx/api_guidelines/index.md) for best
+practices regarding public APIs and an overview of the constraints placed on
+changes.
 
 ## Workstation setup {#setup}
 
@@ -220,46 +222,52 @@
 normally would for application or library development -- right-click on a test
 or sample to run or debug it, search through classes, and so on.
 
-If you get a “Unregistered VCS root detected” message, click “Add root” to
-enable the Git/VCS integration for Android Studio.
-
-If you see any errors (red underlines), click Gradle's elephant button in the
-toolbar ("Sync Project with Gradle Files") and they should resolve once the
-build completes.
-
 > NOTE: You should choose "Use project SDK" when prompted by Studio. If you
 > picked "Android Studio SDK" by mistake, don't panic! You can fix this by
 > opening `File > Project Structure > Platform Settings > SDKs` and manually
 > setting the Android SDK home path to
 > `<project-root>/prebuilts/fullsdk-<platform>`.
 
-> NOTE: If Android Studio's UI looks scaled up, ex. twice the size it should be,
-> you may need to add the following line to your `studio64.vmoptions` file using
-> `Help -> Edit Custom VM Options`:
->
-> ```
-> -Dsun.java2d.uiScale.enabled=false
-> ```
+### Troubleshooting {#studio-troubleshooting}
 
-> NOTE: We are aware of a bug where running `./studiow` does not result in
-> Android Studio application being launched.
+*   If you've updated to macOS Ventura and receive a "App is damaged and cannot
+    be opened" message when running Studio, *do not* move the app to the Trash.
+    Cancel out of the dialog and open macOS `System Settings > Gatekeeper`, look
+    for `"Android Studio" was blocked`, and click `Open Anyway` to grant an
+    exception. Alternatively, you can navigate to the Studio `.app` file under
+    `frameworks/support/studio` and run it once using `Control+Click` and `Open`
+    to automatically grant an exception.
+*   If you've updated to macOS Ventura and receive a "xcrun: error: invalid
+    active developer path" message when running Studio, reinstall Xcode using
+    `xcode-select --install`. If that does not work, you will need to download
+    Xcode.
+*   If you get a “Unregistered VCS root detected” message, click “Add root” to
+    enable the Git/VCS integration for Android Studio.
+*   If you see any errors (red underlines), click Gradle's elephant button in
+    the toolbar (or `File > Sync Project with Gradle Files`) and they should
+    resolve once the build completes.
+*   If you run `./studiow` with a new project set but you're still seeing the
+    old project set in `Project`, use `File > Sync Project with Gradle Files` to
+    force a re-sync.
+*   If Android Studio's UI looks scaled up, ex. twice the size it should be, you
+    may need to add the following line to your `studio64.vmoptions` file using
+    `Help > Edit Custom VM Options`: `-Dsun.java2d.uiScale.enabled=false`
+*   If you don't see a specific Gradle task listed in Studio's Gradle pane,
+    check the following:
+    *   Studio might be running a different project subset than the one
+        intended. For example, `./studiow main` only loads the `main` set of
+        androidx projects; run `./studiow compose` to load the tasks specific to
+        Compose.
+    *   Gradle tasks aren't being loaded. Under Studio's settings =>
+        Experimental, make sure that "Do not build Gradle task list during
+        Gradle sync" is unchecked. Note that unchecking this can reduce Studio's
+        performance.
 
 If in the future you encounter unexpected errors in Studio and you want to check
 for the possibility it is due to some incorrect settings or other generated
 files, you can run `./studiow --clean main <project subset>` or `./studiow
 --reinstall <project subset>` to clean generated files or reinstall Studio.
 
-> Tip: If you don't see a specific Gradle task listed in Studio's Gradle pane,
-> check the following:
->
-> *   Studio might be running a different project subset than the one intended.
->     For example, `./studiow main` only loads the `main` set of androidx
->     projects; run `./studiow compose` to load the tasks specific to Compose.
->
-> *   Gradle tasks aren't being loaded. Under Studio's settings => Experimental,
->     make sure that "Do not build Gradle task list during Gradle sync" is
->     unchecked. (Note that unchecking this can reduce Studio's performance)
-
 ### Enabling Compose `@Preview` annotation previews
 
 Add the following dependencies to your project's `build.gradle`:
@@ -372,9 +380,8 @@
 ./gradlew core:core:assemble --strict
 ```
 
-To build every module, run the Lint verifier, verify the public API surface, and
-generate the local Maven repository artifact, use the `createArchive` Gradle
-task:
+To build every module and generate the local Maven repository artifact, use the
+`createArchive` Gradle task:
 
 ```shell
 ./gradlew createArchive
@@ -468,45 +475,20 @@
 NOTE `./gradlew tasks` always has the canonical task information! When in doubt,
 run `./gradlew tasks`
 
-#### Javadocs
-
-To build API reference docs for tip-of-tree Java source code, run the Gradle
-task:
-
-```
-./gradlew doclavaDocs
-```
-
-Places the documentation in
-`{androidx-main}/out/androidx/docs-tip-of-tree/build/javadoc`
-
-#### KotlinDocs
-
-To build API reference docs for tip-of-tree Kotlin source code, run the Gradle
-task:
-
-```
-./gradlew dokkaKotlinDocs
-```
-
-Places the documentation in
-`{androidx-main}/out/androidx/docs-tip-of-tree/build/dokkaKotlinDocs`
-
-#### Dackka docs
+#### Generate Docs
 
 To build API reference docs for both Java and Kotlin source code using Dackka,
 run the Gradle task:
 
 ```
-./gradlew dackkaDocs
+./gradlew docs
 ```
 
 Location of generated refdocs:
 
 *   docs-public (what is published to DAC):
-    `{androidx-main}/out/androidx/docs-public/build/dackkaDocs`
-*   docs-tip-of-tree:
-    `{androidx-main}/out/androidx/docs-tip-of-tree/build/dackkaDocs`
+    `{androidx-main}/out/androidx/docs-public/build/docs`
+*   docs-tip-of-tree: `{androidx-main}/out/androidx/docs-tip-of-tree/build/docs`
 
 The generated docs are plain HTML pages with links that do not work locally.
 These issues are fixed when the docs are published to DAC, but to preview a
@@ -532,40 +514,21 @@
 [d.android.com](http://d.android.com), run the Gradle command:
 
 ```
-./gradlew zipDoclavaDocs
+./gradlew zipDocs
 ```
 
-This will create the artifact
-`{androidx-main}/out/dist/doclava-public-docs-0.zip`. This command builds docs
-based on the version specified in
+This will create the artifact `{androidx-main}/out/dist/docs-public-0.zip`. This
+command builds docs based on the version specified in
 `{androidx-main-checkout}/frameworks/support/docs-public/build.gradle` and uses
 the prebuilt checked into
 `{androidx-main-checkout}/prebuilts/androidx/internal/androidx/`. We
 colloquially refer to this two step process of (1) updating `docs-public` and
 (2) checking in a prebuilt artifact into the prebuilts directory as
-[The Prebuilts Dance](releasing_detailed.md#the-prebuilts-dance™). So, to build
-javadocs that will be published to
+[The Prebuilts Dance](/company/teams/androidx/releasing_detailed.md#the-prebuilts-dance™).
+So, to build javadocs that will be published to
 https://developer.android.com/reference/androidx/packages, both of these steps
 need to be completed.
 
-Once you done the above steps, Kotlin docs will also be generated, with the only
-difference being that we use the Gradle command:
-
-```
-./gradlew zipDokkaDocs
-```
-
-This will create the artifact `{androidx-main}/out/dist/dokka-public-docs-0.zip`
-
-To generate a zip artifact for both Java and Kotlin source code using Dackka:
-
-```
-./gradlew zipDackkaDocs
-```
-
-This will create the artifact
-`{androidx-main}/out/dist/dackka-public-docs-0.zip`
-
 ### Updating public APIs {#updating-public-apis}
 
 Public API tasks -- including tracking, linting, and verifying compatibility --
@@ -595,6 +558,10 @@
 If you change the public APIs without updating the API file, your module will
 still build **but** your CL will fail Treehugger presubmit checks.
 
+NOTE The `updateApi` task does not generate versioned API files (e.g.
+`1.0.0-beta01.txt`) during a library's `alpha`, `rc` or stable cycles. The task
+will always generate `current.txt` API files.
+
 #### What are all these files in `api/`? {#updating-public-apis-glossary}
 
 Historical API surfaces are tracked for compatibility and docs generation
@@ -604,11 +571,11 @@
 *   `<version>.txt`: Public API surface, tracked for compatibility
 *   `restricted_<version>.txt`: `@RestrictTo` API surface, tracked for
     compatibility where necessary (see
-    [Restricted APIs](api_guidelines/index.md#restricted-api))
+    [Restricted APIs](/company/teams/androidx/api_guidelines/index.md#restricted-api))
 *   `public_plus_experimental_<version>.txt`: Public API surface plus
     `@RequiresOptIn` experimental API surfaces used for documentation (see
-    [Experimental APIs](api_guidelines/index.md#experimental-api)) and API
-    review
+    [Experimental APIs](/company/teams/androidx/api_guidelines/index.md#experimental-api))
+    and API review
 
 ### Release notes & the `Relnote:` tag {#relnote}
 
@@ -794,16 +761,20 @@
 are referencing new framework APIs, you will need to wait for the framework
 changes to land in an SDK build (or build it yourself) and then land in both
 prebuilts/fullsdk and prebuilts/sdk. See
-[Updating SDK prebuilts](playbook.md#prebuilts-fullsdk) for more information.
+[Updating SDK prebuilts](/company/teams/androidx/playbook.md#prebuilts-fullsdk)
+for more information.
 
 #### How do I handle refactoring a framework API referenced from a library?
 
 Because AndroidX must compile against both the current framework and the latest
 SDK prebuilt, and because compiling the SDK prebuilt depends on AndroidX, you
-will need to refactor in stages: Remove references to the target APIs from
-AndroidX Perform the refactoring in the framework Update the framework prebuilt
-SDK to incorporate changes in (2) Add references to the refactored APIs in
-AndroidX Update AndroidX prebuilts to incorporate changes in (4)
+will need to refactor in stages:
+
+1.  Remove references to the target APIs from AndroidX
+2.  Perform the refactoring in the framework
+3.  Update the framework prebuilt SDK to incorporate changes in (2)
+4.  Add references to the refactored APIs in AndroidX
+5.  Update AndroidX prebuilts to incorporate changes in (4)
 
 ## Testing {#testing}
 
@@ -836,8 +807,8 @@
 or `support-\*-demos` (e.g. `support-v4-demos` or `support-leanback-demos`). You
 can run them by clicking `Run > Run ...` and choosing the desired application.
 
-See the [Testing](testing.md) page for more resources on writing, running, and
-monitoring tests.
+See the [Testing](/company/teams/androidx/testing.md) page for more resources on
+writing, running, and monitoring tests.
 
 ### AVD Manager
 
@@ -937,7 +908,7 @@
 
 ### Using in a Gradle build
 
-To make these artifacts visible to Gradle, you need to add it as a respository:
+To make these artifacts visible to Gradle, you need to add it as a repository:
 
 ```groovy
 allprojects {
@@ -960,7 +931,7 @@
 to use a snapshot artifact, the version in your `build.gradle` will need to be
 updated to `androidx.<groupId>:<artifactId>:X.Y.Z-SNAPSHOT`
 
-For example, to use the `core:core:1.2.0-SHAPSHOT` snapshot, you would add the
+For example, to use the `core:core:1.2.0-SNAPSHOT` snapshot, you would add the
 following to your `build.gradle`:
 
 ```
@@ -1032,7 +1003,7 @@
 full m2repository to avoid version issues of other modules. You can either take
 the unzipped directory from
 `<path-to-checkout>/out/dist/top-of-tree-m2repository-##.zip`, or from
-`<path-to-checkout>/out/androidx/build/support_repo/` after buiding `androidx`.
+`<path-to-checkout>/out/androidx/build/support_repo/` after building `androidx`.
 Here is an example of replacing the RecyclerView module:
 
 ```shell
@@ -1047,7 +1018,8 @@
 ### How do I measure library size? {#library-size}
 
 Method count and bytecode size are tracked in CI
-[alongside benchmarks](benchmarking.md#monitoring) to detect regressions.
+[alongside benchmarks](/company/teams/androidx/benchmarking.md#monitoring) to
+detect regressions.
 
 For local measurements, use the `:reportLibraryMetrics` task. For example:
 
@@ -1072,3 +1044,21 @@
 [Overview page](https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary)
 includes content from
 [compose-runtime-documentation.md](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/compose-runtime-documentation.md).
+
+### How do I enable MultiDex for my library?
+
+Go to your project/app level build.gradle file, and add
+
+```
+android {
+    defaultConfig {
+        multiDexEnabled = true
+    }
+}
+```
+
+as well as `androidTestImplementation(libs.multidex)` to the dependenices block.
+
+If you want it enabled for the application and not test apk, add
+`implementation(libs.multidex)` to the dependencies block instead. Any prior
+failures may not re-occur now that the software is multi-dexed. Rerun the build.
diff --git a/docs/open_source.md b/docs/open_source.md
index 173d1e77..392dc34 100644
--- a/docs/open_source.md
+++ b/docs/open_source.md
@@ -29,7 +29,7 @@
 #### Closed-source dependencies
 
 In specific cases, libraries *may* include closed-source dependencies. See the
-[Open-source compatibility](api_guidelines/dependencies.md#dependencies-aosp)
+[Open-source compatibility](/company/teams/androidx/api_guidelines/index.md#dependencies-aosp)
 section of the API Guidelines for implementation details.
 
 ### Examples of products that are _not_ open-source
diff --git a/docs/principles.md b/docs/principles.md
index 523c87f..a2d2a42 100644
--- a/docs/principles.md
+++ b/docs/principles.md
@@ -60,8 +60,9 @@
     to choose between a variety of services as the backing implementation
 -   Comply with AndroidX checks and policies such as API tracking and style
     checks
--   See [Integrating proprietary components](open_source.md) for guidance on
-    using closed-source and proprietary libraries and services
+-   See
+    [Integrating proprietary components](/company/teams/androidx/open_source.md)
+    for guidance on using closed-source and proprietary libraries and services
 
 ### 6. Written using language-idiomatic APIs
 
diff --git a/docs/team.md b/docs/team.md
index 0c61aa4b..0149376 100644
--- a/docs/team.md
+++ b/docs/team.md
@@ -4,7 +4,7 @@
 
 <!--*
 # Document freshness: For more information, see go/fresh-source.
-freshness: { owner: 'alanv' reviewed: '2022-06-10' }
+freshness: { owner: 'alanv' reviewed: '2022-12-09' }
 *-->
 
 ## What we do
diff --git a/docs/testability.md b/docs/testability.md
index 45d4b6d..5a1a773 100644
--- a/docs/testability.md
+++ b/docs/testability.md
@@ -39,11 +39,11 @@
 improvement.
 
 To get started with sample code, see
-[Sample code in Kotlin modules](api_guidelines.md#sample-code-in-kotlin-modules)
+[Sample code in Kotlin modules](/company/teams/androidx/api_guidelines/index.md#sample-code-in-kotlin-modules)
 for information on writing samples that can be referenced from API reference
 documentation or
-[Project directory structure](api_guidelines.md#module-structure) for module
-naming guidelines if you'd like to create a basic test app.
+[Project directory structure](/company/teams/androidx/api_guidelines/index.md#module-structure)
+for module naming guidelines if you'd like to create a basic test app.
 
 ### Avoiding side-effects {#side-effects}
 
@@ -308,8 +308,9 @@
 state has been reached or the requested action has been completed. This might
 mean adding listeners, callbacks, `ListenableFuture`s, or `LiveData`.
 
-See [Asynchronous work](api_guidelines.md#async) in the API Guidelines for more
-information on exposing the state of asynchronous work to clients.
+See [Asynchronous work](/company/teams/androidx/api_guidelines/index.md#async)
+in the API Guidelines for more information on exposing the state of asynchronous
+work to clients.
 
 ### Calling `Thread.sleep()` as a synchronization barrier
 
diff --git a/docs/testing.md b/docs/testing.md
index b59a240..b6fde3a 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -30,7 +30,7 @@
 
 NOTE For best practices on writing libraries in a way that makes it easy for end
 users -- and library developers -- to write tests, see the
-[Testability](testability.md) guide.
+[Testability](/company/teams/androidx/testability.md) guide.
 
 ### What gets tested, and when {#affected-module-detector}
 
@@ -255,7 +255,7 @@
 *   Validation of API usability and developer experience, when paired with a use
     case or critical user journey
 *   Sample documentation, when embedded into API reference docs using the
-    [`@sample` and `@Sampled` annotations](api_guidelines.md#sample-usage)
+    [`@sample` and `@Sampled` annotations](/company/teams/androidx/api_guidelines/index.md#sample-usage)
 
 ### Legacy test apps {#testapps-legacy}
 
@@ -273,4 +273,4 @@
 
 AndroidX supports benchmarking - locally with Studio/Gradle, and continuously in
 post-submit. For more information on how to create and run benchmarks, see
-[Benchmarking](benchmarking.md).
+[Benchmarking](/company/teams/androidx/benchmarking.md).
diff --git a/docs/versioning.md b/docs/versioning.md
index b7b5a1a..12434a5 100644
--- a/docs/versioning.md
+++ b/docs/versioning.md
@@ -6,24 +6,31 @@
 including the expectations at each cycle and criteria for moving to the next
 cycle or SemVer revision.
 
-## Semantic versioning
+## Semantic versioning and binary compatibility {#semver}
 
 Artifacts follow strict [semantic versioning](http://semver.org) for binary
-compatibility with an added inter-version sequence of pre-release revisions. The
-version for a finalized release artifact will follow the format
+compatibility with an added inter-version sequence of pre-release revisions.
+Versions for finalized release artifacts, which are available on
+[Google Maven](https://maven.google.com) will follow the format
 `<major>.<minor>.<bugfix>` with an optional `-<alpha|beta|rc><nn>` suffix.
-Internal or nightly releases (via [androidx.dev](http://androidx.dev)) use the
-`-SNAPSHOT` suffix.
+Internal or nightly releases, which are available on
+[androidx.dev](http://androidx.dev), use the `-SNAPSHOT` suffix.
 
-### Source compatibility
+### Behavioral and source compatibility {#compat}
 
-Libraries are encouraged -- but not required -- to preserve source compatibility
-across minor versions. Strictly requiring source compatibility would require
-major version bumps when implementing quality-of-life improvements such as
-nullability annotations or generics, which would be
-[disruptive to the library ecosystem](#major-implications).
+Libraries are required to preserve *behavioral compatibility* -- APIs must
+behave as described in their documentation -- across minor versions. Special
+consideration must also be made for changes to undocumented behavior, as
+developers may have made their own assumptions about API contracts based on
+observed behavior.
 
-### Notation
+Libraries are strongly encouraged to preserve *source compatibility* across
+minor versions. Strictly requiring source compatibility would require major
+version bumps when implementing quality-of-life improvements such as nullability
+annotations or generics, which would be [disruptive](#major-implications) to the
+library ecosystem.
+
+### Notation {#notation}
 
 Major (`x.0.0`)
 :   An artifact's major version indicates a guaranteed forward-compatibility
@@ -33,7 +40,7 @@
 Minor (`1.x.0`)
 :   Minor indicates compatible public API changes. This number is incremented
     when APIs are added, including the addition of
-    [`@Deprecated` annotations](api_guidelines.md#deprecation-and-removal).
+    [`@Deprecated` annotations](/company/teams/androidx/api_guidelines/index.md#deprecation-and-removal).
     Binary compatibility must be preserved between minor version changes.
 
 Bugfix (`1.0.x`)
@@ -41,7 +48,7 @@
     taken to ensure that existing clients are not broken, including clients that
     may have been working around long-standing broken behavior.
 
-#### Pre-release cycles
+#### Pre-release cycles {#prerelease}
 
 Alpha (`1.0.0-alphaXX`)
 :   Feature development and API stabilization phase.
@@ -231,8 +238,9 @@
         `publish=true` or create an `api` directory) and remain enabled
     *   May add/remove APIs within `alpha` cycle, but deprecate/remove cycle is
         strongly recommended.
-    *   May use [experimental APIs](api_guidelines.md#experimental-api) across
-        same-version group boundaries
+    *   May use
+        [experimental APIs](/company/teams/androidx/api_guidelines/index.md#experimental-api)
+        across same-version group boundaries
 *   Testing
     *   All changes **should** be accompanied by a `Test:` stanza
     *   All pre-submit and post-submit tests are passing
@@ -264,13 +272,13 @@
     *   All APIs from alpha undergoing deprecate/remove cycle must be removed
         *   The final removal of a `@Deprecated` API should occur in alpha, not
             in beta
-    *   Must not use [experimental APIs](api_guidelines.md#experimental-api)
+    *   Must not use
+        [experimental APIs](/company/teams/androidx/api_guidelines#experimental-api)
         across same-version group boundaries
 *   Testing
     *   All public APIs are tested
     *   All pre-submit and post-submit tests are enabled (e.g. all suppressions
         are removed) and passing
-    *   Your library passes `./gradlew library:checkReleaseReady`
 *   Use of experimental Kotlin features (e.g. `@OptIn`) must be audited for
     stability
 *   All dependencies are `beta`, `rc`, or stable
@@ -336,22 +344,19 @@
 -   The version of your library listed in `androidx-main` should *always* be
     higher than the version publically available on Google Maven. This allows us
     to do proper version tracking and API tracking.
-
 -   Version increments must be done before the CL cutoff date (aka the build cut
     date).
-
 -   **Increments to the next stability suffix** (like `alpha` to `beta`) should
-    be handled by the library owner, with the Jetpack TPM (nickanthony@) CC'd
+    be handled by the library owner, with the Jetpack TPM (natnaelbelay@) CC'd
     for API+1.
-
 -   Version increments in release branches will need to follow the guide
-    [How to update your version on a release branch](release_branches.md#update-your-version)
-
+    [How to update your version on a release branch](/company/teams/androidx/release_branches.md#update-your-version)
 -   When you're ready for `rc01`, the increment to `rc01` should be done in
     `androidx-main` and then your release branch should be snapped to that
-    build. See the guide [Snap your release branch](release_branches.md#snap) on
-    how to do this. After the release branch is snapped to that build, you will
-    need to update your version in `androidx-main` to `alpha01` of the next
+    build. See the guide
+    [Snap your release branch](/company/teams/androidx/release_branches.md#snap)
+    on how to do this. After the release branch is snapped to that build, you
+    will need to update your version in `androidx-main` to `alpha01` of the next
     minor (or major) version.
 
 ### How to update your version
@@ -369,9 +374,10 @@
 
 ## `-ktx` Modules {#ktx}
 
-[Kotlin extension libraries](api_guidelines.md#module-ktx) (`-ktx`) follow the
-same versioning requirements as other libraries, but with one exception: they
-must match the version of the Java libraries that they extend.
+[Kotlin extension libraries](/company/teams/androidx/api_guidelines/index.md#module-ktx)
+(`-ktx`) follow the same versioning requirements as other libraries, but with
+one exception: they must match the version of the Java libraries that they
+extend.
 
 For example, let's say you are developing a Java library
 `androidx.foo:foo-bar:1.1.0-alpha01` and you want to add a Kotlin extension
@@ -389,10 +395,6 @@
 Generally, these occur during the batched bi-weekly (every 2 weeks) release
 because all tip-of-tree dependencies will need to be released too.
 
-### Are there restrictions on when or how often an alpha can ship?
-
-Nope.
-
 ### Can alpha work (ex. for the next Minor release) occur in the primary development branch during beta API lockdown?
 
 No. This is by design. Focus should be spent on improving the Beta version and
@@ -405,5 +407,5 @@
 
 ### How often can a beta release?
 
-As often as needed, however, releases outside of the bi-weekly (every 2 weeks)
+As often as needed; however, releases outside of the bi-weekly (every 2 weeks)
 release will need to get approval from the TPM (natnaelbelay@).
diff --git a/drawerlayout/OWNERS b/drawerlayout/OWNERS
deleted file mode 100644
index 0d15e98..0000000
--- a/drawerlayout/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-sjgilbert@google.com
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/dynamicanimation/dynamicanimation/src/androidTest/java/androidx/dynamicanimation/tests/SpringTests.java b/dynamicanimation/dynamicanimation/src/androidTest/java/androidx/dynamicanimation/tests/SpringTests.java
index ae48994..64702c0 100644
--- a/dynamicanimation/dynamicanimation/src/androidTest/java/androidx/dynamicanimation/tests/SpringTests.java
+++ b/dynamicanimation/dynamicanimation/src/androidTest/java/androidx/dynamicanimation/tests/SpringTests.java
@@ -33,6 +33,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 
+import android.os.Build;
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.SystemClock;
@@ -876,6 +877,10 @@
     @Test
     @SdkSuppress(minSdkVersion = 33, maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void testDurationScaleChangeListener() throws InterruptedException {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         final SpringAnimation anim = new SpringAnimation(mView1, DynamicAnimation.Y, 0f);
         final CountDownLatch registerUnregisterLatch = new CountDownLatch(2);
 
diff --git a/emoji2/emoji2-bundled/api/1.3.0-beta01.txt b/emoji2/emoji2-bundled/api/1.3.0-beta01.txt
new file mode 100644
index 0000000..8749c28
--- /dev/null
+++ b/emoji2/emoji2-bundled/api/1.3.0-beta01.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.emoji2.bundled {
+
+  public class BundledEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+    ctor public BundledEmojiCompatConfig(android.content.Context);
+  }
+
+}
+
diff --git a/emoji2/emoji2-bundled/api/1.3.0-beta02.txt b/emoji2/emoji2-bundled/api/1.3.0-beta02.txt
new file mode 100644
index 0000000..8749c28
--- /dev/null
+++ b/emoji2/emoji2-bundled/api/1.3.0-beta02.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.emoji2.bundled {
+
+  public class BundledEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+    ctor public BundledEmojiCompatConfig(android.content.Context);
+  }
+
+}
+
diff --git a/emoji2/emoji2-bundled/api/public_plus_experimental_1.3.0-beta01.txt b/emoji2/emoji2-bundled/api/public_plus_experimental_1.3.0-beta01.txt
new file mode 100644
index 0000000..8749c28
--- /dev/null
+++ b/emoji2/emoji2-bundled/api/public_plus_experimental_1.3.0-beta01.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.emoji2.bundled {
+
+  public class BundledEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+    ctor public BundledEmojiCompatConfig(android.content.Context);
+  }
+
+}
+
diff --git a/emoji2/emoji2-bundled/api/public_plus_experimental_1.3.0-beta02.txt b/emoji2/emoji2-bundled/api/public_plus_experimental_1.3.0-beta02.txt
new file mode 100644
index 0000000..8749c28
--- /dev/null
+++ b/emoji2/emoji2-bundled/api/public_plus_experimental_1.3.0-beta02.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.emoji2.bundled {
+
+  public class BundledEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+    ctor public BundledEmojiCompatConfig(android.content.Context);
+  }
+
+}
+
diff --git a/camera/camera-viewfinder/api/res-1.2.0-beta01.txt b/emoji2/emoji2-bundled/api/res-1.3.0-beta01.txt
similarity index 100%
copy from camera/camera-viewfinder/api/res-1.2.0-beta01.txt
copy to emoji2/emoji2-bundled/api/res-1.3.0-beta01.txt
diff --git a/camera/camera-viewfinder/api/res-1.2.0-beta02.txt b/emoji2/emoji2-bundled/api/res-1.3.0-beta02.txt
similarity index 100%
copy from camera/camera-viewfinder/api/res-1.2.0-beta02.txt
copy to emoji2/emoji2-bundled/api/res-1.3.0-beta02.txt
diff --git a/emoji2/emoji2-bundled/api/restricted_1.3.0-beta01.txt b/emoji2/emoji2-bundled/api/restricted_1.3.0-beta01.txt
new file mode 100644
index 0000000..8749c28
--- /dev/null
+++ b/emoji2/emoji2-bundled/api/restricted_1.3.0-beta01.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.emoji2.bundled {
+
+  public class BundledEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+    ctor public BundledEmojiCompatConfig(android.content.Context);
+  }
+
+}
+
diff --git a/emoji2/emoji2-bundled/api/restricted_1.3.0-beta02.txt b/emoji2/emoji2-bundled/api/restricted_1.3.0-beta02.txt
new file mode 100644
index 0000000..8749c28
--- /dev/null
+++ b/emoji2/emoji2-bundled/api/restricted_1.3.0-beta02.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.emoji2.bundled {
+
+  public class BundledEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+    ctor public BundledEmojiCompatConfig(android.content.Context);
+  }
+
+}
+
diff --git a/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/AllEmojisTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/AllEmojisTest.java
index 929dbc8..1cf822f 100644
--- a/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/AllEmojisTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/AllEmojisTest.java
@@ -22,6 +22,8 @@
 import android.content.Context;
 import android.graphics.Paint;
 import android.text.Spanned;
+import android.text.StaticLayout;
+import android.text.TextPaint;
 
 import androidx.core.graphics.PaintCompat;
 import androidx.emoji2.bundled.util.EmojiMatcher;
@@ -135,6 +137,23 @@
                 EmojiCompat.get().hasEmojiGlyph(mString, Integer.MAX_VALUE));
     }
 
+    @Test
+    public void emoji_hasDesiredWidth() {
+        TextPaint tp = new TextPaint();
+        // spanned, test fails, width == 0
+        CharSequence processedText = EmojiCompat.get()
+                .process(
+                        mString,
+                        /* start= */ 0,
+                        /* end= */ mString.length(),
+                        /* maxEmojiCount= */ mString.length(),
+                        EmojiCompat.REPLACE_STRATEGY_ALL);
+        float emojiCompatWidth = StaticLayout.getDesiredWidth(processedText, tp);
+        assertTrue("emoji " + mString + " with codepoints " + mCodepoints + "has "
+                + "desired width " + emojiCompatWidth,
+                emojiCompatWidth > 0);
+    }
+
     private void assertSpanCanRenderEmoji(final String str) {
         final Spanned spanned = (Spanned) EmojiCompat.get().process(new TestString(str).toString());
         final EmojiSpan[] spans = spanned.getSpans(0, spanned.length(), EmojiSpan.class);
diff --git a/emoji2/emoji2-emojipicker/build.gradle b/emoji2/emoji2-emojipicker/build.gradle
index 17422f1..3ea643f 100644
--- a/emoji2/emoji2-emojipicker/build.gradle
+++ b/emoji2/emoji2-emojipicker/build.gradle
@@ -24,6 +24,7 @@
 
 dependencies {
     api(libs.kotlinStdlib)
+    implementation(libs.constraintLayout)
     implementation(libs.kotlinCoroutinesCore)
     implementation("androidx.core:core-ktx:1.8.0")
     implementation project(path: ':emoji2:emoji2')
@@ -59,6 +60,7 @@
 androidx {
     name = "androidx.emoji2:emoji2-emojipicker"
     type = LibraryType.PUBLISHED_LIBRARY
+    mavenVersion = LibraryVersions.EMOJI2_QUARANTINE
     inceptionYear = "2022"
     description = "This library provides the latest emoji support and emoji picker UI to input " +
             "emoji in current and older devices"
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml b/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
index a155641..45e6dbd 100644
--- a/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
+++ b/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
@@ -17,7 +17,8 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
     <uses-sdk android:minSdkVersion="21"/>
-    <application>
+    <application
+        android:label="Emoji Picker Sample App" >
         <activity android:name=".MainActivity" android:exported="true"
             android:theme="@style/MyPinkTheme">
             <!-- Handle Google app icon launch. -->
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt b/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt
index dde4446..1447129 100644
--- a/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt
+++ b/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt
@@ -18,8 +18,8 @@
 
 import android.content.Context
 import android.os.Bundle
-import android.widget.Button
 import android.widget.EditText
+import android.widget.ToggleButton
 import androidx.appcompat.app.AppCompatActivity
 import androidx.emoji2.emojipicker.EmojiPickerView
 import androidx.emoji2.emojipicker.RecentEmojiProvider
@@ -35,10 +35,16 @@
         }
         view.setRecentEmojiProvider(CustomRecentEmojiProvider(applicationContext))
 
-        findViewById<Button>(R.id.button).setOnClickListener {
-            view.emojiGridColumns = 8
-            view.emojiGridRows = 8.3f
-        }
+        findViewById<ToggleButton>(R.id.button)
+            .setOnCheckedChangeListener { _, isChecked ->
+                if (isChecked) {
+                    view.emojiGridColumns = 8
+                    view.emojiGridRows = 8.3f
+                } else {
+                    view.emojiGridColumns = 9
+                    view.emojiGridRows = 15f
+                }
+            }
     }
 }
 
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/res/layout-land/main.xml b/emoji2/emoji2-emojipicker/samples/src/main/res/layout-land/main.xml
index e55d888..41230cb 100644
--- a/emoji2/emoji2-emojipicker/samples/src/main/res/layout-land/main.xml
+++ b/emoji2/emoji2-emojipicker/samples/src/main/res/layout-land/main.xml
@@ -25,11 +25,12 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content" />
 
-    <Button
+    <ToggleButton
         android:id="@+id/button"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:text="Change Layout" />
+        android:textOff="Display Larger Emojis"
+        android:textOn="Display Smaller Emojis" />
 
     <androidx.emoji2.emojipicker.EmojiPickerView
         android:id="@+id/emoji_picker"
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/res/layout/main.xml b/emoji2/emoji2-emojipicker/samples/src/main/res/layout/main.xml
index 2270f92..fc424b5 100644
--- a/emoji2/emoji2-emojipicker/samples/src/main/res/layout/main.xml
+++ b/emoji2/emoji2-emojipicker/samples/src/main/res/layout/main.xml
@@ -25,11 +25,12 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content" />
 
-    <Button
+    <ToggleButton
         android:id="@+id/button"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:text="Change Layout" />
+        android:textOff="Display Larger Emojis"
+        android:textOn="Display Smaller Emojis" />
 
     <androidx.emoji2.emojipicker.EmojiPickerView
         android:id="@+id/emoji_picker"
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/res/values/styles.xml b/emoji2/emoji2-emojipicker/samples/src/main/res/values/styles.xml
index 35b4a46..32ff475 100644
--- a/emoji2/emoji2-emojipicker/samples/src/main/res/values/styles.xml
+++ b/emoji2/emoji2-emojipicker/samples/src/main/res/values/styles.xml
@@ -18,12 +18,14 @@
 <resources>
 
     <style name="MyPinkTheme" parent="Theme.AppCompat.DayNight" >
-        <!-- The color for selected headers -->
-        <item name="colorAccent">#ffffff</item>
-        <!-- The color for unselected headers -->
-        <item name="colorControlNormal">#FF69B4</item>
+        <!-- The color for selected category icons -->
+        <item name="colorAccent">#696969</item>
+        <!-- The color for unselected category icons -->
+        <item name="colorControlNormal">#FFC0CB</item>
         <!-- The color for all text -->
-        <item name="android:textColorPrimary">#ffffff</item>
+        <item name="android:textColorPrimary">#696969</item>
+        <!-- The text size for category text -->
+        <item name="android:textSize">12dp</item>
         <!-- The color for variant popup background -->
         <item name="colorButtonNormal">#FFC0CB</item>
     </style>
diff --git a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt
index 4774237..6244759 100644
--- a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt
+++ b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt
@@ -24,7 +24,6 @@
 import android.view.View.GONE
 import android.view.View.VISIBLE
 import android.view.ViewGroup
-import android.widget.FrameLayout
 import android.widget.ImageView
 import androidx.core.view.children
 import androidx.core.view.isVisible
@@ -97,7 +96,7 @@
             )!!
             // No variant indicator
             assertEquals(
-                (targetView.parent as FrameLayout).findViewById<ImageView>(
+                (targetView.parent.parent as ViewGroup).findViewById<ImageView>(
                     EmojiPickerViewR.id.variant_availability_indicator
                 ).visibility,
                 GONE
@@ -122,7 +121,7 @@
         val targetView = findViewByEmoji(view, NOSE_EMOJI)!!
         // Variant indicator visible
         assertEquals(
-            (targetView.parent as FrameLayout).findViewById<ImageView>(
+            (targetView.parent.parent as ViewGroup).findViewById<ImageView>(
                 EmojiPickerViewR.id.variant_availability_indicator
             ).visibility, VISIBLE
         )
@@ -229,7 +228,7 @@
             BoundedMatcher<RecyclerView.ViewHolder, EmojiViewHolder>(EmojiViewHolder::class.java) {
             override fun describeTo(description: Description) {}
             override fun matchesSafely(item: EmojiViewHolder) =
-                (item.itemView as FrameLayout)
+                (item.itemView as ViewGroup)
                     .findViewById<EmojiView>(EmojiPickerViewR.id.emoji_view)
                     .emoji == emoji
         }
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-emojipicker/src/main/java/androidx/emoji2/emojipicker/PopupViewHelper.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/PopupViewHelper.kt
index f6e41bb..8038e48 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/PopupViewHelper.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/PopupViewHelper.kt
@@ -7,8 +7,8 @@
 import android.view.LayoutInflater
 import android.view.View
 import android.view.View.OnClickListener
+import android.view.ViewGroup
 import android.view.accessibility.AccessibilityEvent
-import android.widget.FrameLayout
 import android.widget.GridLayout
 import androidx.core.content.ContextCompat
 
@@ -99,8 +99,8 @@
                     R.layout.emoji_view_holder,
                     null,
                     false
-                ) as FrameLayout).apply {
-                    (this.getChildAt(0) as EmojiView).apply {
+                ) as ViewGroup).apply {
+                    findViewById<EmojiView>(R.id.emoji_view).apply {
                         emoji = variants[it - 1]
                         setOnClickListener(clickListener)
                         if (it == 1) {
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/category_text_view.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/category_text_view.xml
index c7bc5af..354bc25 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/layout/category_text_view.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/category_text_view.xml
@@ -31,6 +31,5 @@
         android:gravity="center_vertical|start"
         android:letterSpacing="0.1"
         android:importantForAccessibility="yes"
-        android:textColor="?android:attr/textColorPrimary"
-        android:textSize="12dp" />
+        android:textColor="?android:attr/textColorPrimary" />
 </FrameLayout>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
index 5c64c9f..e9529a9 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
@@ -14,24 +14,35 @@
   limitations under the License.
   -->
 
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="0dp"
     android:layout_height="0dp">
 
-    <androidx.emoji2.emojipicker.EmojiView
-        android:id="@+id/emoji_view"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:background="@drawable/ripple_emoji_view"
-        android:importantForAccessibility="yes"
-        android:textSize="30dp"
-        android:layout_margin="1dp" />
+    <FrameLayout
+        android:id="@+id/emoji_view_frame"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" >
+        <androidx.emoji2.emojipicker.EmojiView
+            android:id="@+id/emoji_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@drawable/ripple_emoji_view"
+            android:importantForAccessibility="yes"
+            android:layout_margin="1dp" />
+    </FrameLayout>
     <ImageView
         android:id="@+id/variant_availability_indicator"
         android:visibility="gone"
         android:src="@drawable/variant_availability_indicator"
         android:layout_width="5dp"
         android:layout_height="5dp"
-        android:layout_gravity="bottom|end"
-        android:textColor="?android:attr/textColorPrimary" />
-</FrameLayout>
\ No newline at end of file
+        android:textColor="?android:attr/textColorPrimary"
+        app:layout_constraintEnd_toEndOf="@id/emoji_view_frame"
+        app:layout_constraintBottom_toBottomOf="@id/emoji_view_frame"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/emoji2/emoji2-views-helper/api/1.3.0-beta01.txt b/emoji2/emoji2-views-helper/api/1.3.0-beta01.txt
new file mode 100644
index 0000000..30a6feb
--- /dev/null
+++ b/emoji2/emoji2-views-helper/api/1.3.0-beta01.txt
@@ -0,0 +1,27 @@
+// Signature format: 4.0
+package androidx.emoji2.viewsintegration {
+
+  public final class EmojiEditTextHelper {
+    ctor public EmojiEditTextHelper(android.widget.EditText);
+    ctor public EmojiEditTextHelper(android.widget.EditText, boolean);
+    method public android.text.method.KeyListener? getKeyListener(android.text.method.KeyListener?);
+    method public int getMaxEmojiCount();
+    method public boolean isEnabled();
+    method public android.view.inputmethod.InputConnection? onCreateInputConnection(android.view.inputmethod.InputConnection?, android.view.inputmethod.EditorInfo);
+    method public void setEnabled(boolean);
+    method public void setMaxEmojiCount(@IntRange(from=0) int);
+  }
+
+  public final class EmojiTextViewHelper {
+    ctor public EmojiTextViewHelper(android.widget.TextView);
+    ctor public EmojiTextViewHelper(android.widget.TextView, boolean);
+    method public android.text.InputFilter![] getFilters(android.text.InputFilter![]);
+    method public boolean isEnabled();
+    method public void setAllCaps(boolean);
+    method public void setEnabled(boolean);
+    method public void updateTransformationMethod();
+    method public android.text.method.TransformationMethod? wrapTransformationMethod(android.text.method.TransformationMethod?);
+  }
+
+}
+
diff --git a/emoji2/emoji2-views-helper/api/1.3.0-beta02.txt b/emoji2/emoji2-views-helper/api/1.3.0-beta02.txt
new file mode 100644
index 0000000..30a6feb
--- /dev/null
+++ b/emoji2/emoji2-views-helper/api/1.3.0-beta02.txt
@@ -0,0 +1,27 @@
+// Signature format: 4.0
+package androidx.emoji2.viewsintegration {
+
+  public final class EmojiEditTextHelper {
+    ctor public EmojiEditTextHelper(android.widget.EditText);
+    ctor public EmojiEditTextHelper(android.widget.EditText, boolean);
+    method public android.text.method.KeyListener? getKeyListener(android.text.method.KeyListener?);
+    method public int getMaxEmojiCount();
+    method public boolean isEnabled();
+    method public android.view.inputmethod.InputConnection? onCreateInputConnection(android.view.inputmethod.InputConnection?, android.view.inputmethod.EditorInfo);
+    method public void setEnabled(boolean);
+    method public void setMaxEmojiCount(@IntRange(from=0) int);
+  }
+
+  public final class EmojiTextViewHelper {
+    ctor public EmojiTextViewHelper(android.widget.TextView);
+    ctor public EmojiTextViewHelper(android.widget.TextView, boolean);
+    method public android.text.InputFilter![] getFilters(android.text.InputFilter![]);
+    method public boolean isEnabled();
+    method public void setAllCaps(boolean);
+    method public void setEnabled(boolean);
+    method public void updateTransformationMethod();
+    method public android.text.method.TransformationMethod? wrapTransformationMethod(android.text.method.TransformationMethod?);
+  }
+
+}
+
diff --git a/emoji2/emoji2-views-helper/api/public_plus_experimental_1.3.0-beta01.txt b/emoji2/emoji2-views-helper/api/public_plus_experimental_1.3.0-beta01.txt
new file mode 100644
index 0000000..30a6feb
--- /dev/null
+++ b/emoji2/emoji2-views-helper/api/public_plus_experimental_1.3.0-beta01.txt
@@ -0,0 +1,27 @@
+// Signature format: 4.0
+package androidx.emoji2.viewsintegration {
+
+  public final class EmojiEditTextHelper {
+    ctor public EmojiEditTextHelper(android.widget.EditText);
+    ctor public EmojiEditTextHelper(android.widget.EditText, boolean);
+    method public android.text.method.KeyListener? getKeyListener(android.text.method.KeyListener?);
+    method public int getMaxEmojiCount();
+    method public boolean isEnabled();
+    method public android.view.inputmethod.InputConnection? onCreateInputConnection(android.view.inputmethod.InputConnection?, android.view.inputmethod.EditorInfo);
+    method public void setEnabled(boolean);
+    method public void setMaxEmojiCount(@IntRange(from=0) int);
+  }
+
+  public final class EmojiTextViewHelper {
+    ctor public EmojiTextViewHelper(android.widget.TextView);
+    ctor public EmojiTextViewHelper(android.widget.TextView, boolean);
+    method public android.text.InputFilter![] getFilters(android.text.InputFilter![]);
+    method public boolean isEnabled();
+    method public void setAllCaps(boolean);
+    method public void setEnabled(boolean);
+    method public void updateTransformationMethod();
+    method public android.text.method.TransformationMethod? wrapTransformationMethod(android.text.method.TransformationMethod?);
+  }
+
+}
+
diff --git a/emoji2/emoji2-views-helper/api/public_plus_experimental_1.3.0-beta02.txt b/emoji2/emoji2-views-helper/api/public_plus_experimental_1.3.0-beta02.txt
new file mode 100644
index 0000000..30a6feb
--- /dev/null
+++ b/emoji2/emoji2-views-helper/api/public_plus_experimental_1.3.0-beta02.txt
@@ -0,0 +1,27 @@
+// Signature format: 4.0
+package androidx.emoji2.viewsintegration {
+
+  public final class EmojiEditTextHelper {
+    ctor public EmojiEditTextHelper(android.widget.EditText);
+    ctor public EmojiEditTextHelper(android.widget.EditText, boolean);
+    method public android.text.method.KeyListener? getKeyListener(android.text.method.KeyListener?);
+    method public int getMaxEmojiCount();
+    method public boolean isEnabled();
+    method public android.view.inputmethod.InputConnection? onCreateInputConnection(android.view.inputmethod.InputConnection?, android.view.inputmethod.EditorInfo);
+    method public void setEnabled(boolean);
+    method public void setMaxEmojiCount(@IntRange(from=0) int);
+  }
+
+  public final class EmojiTextViewHelper {
+    ctor public EmojiTextViewHelper(android.widget.TextView);
+    ctor public EmojiTextViewHelper(android.widget.TextView, boolean);
+    method public android.text.InputFilter![] getFilters(android.text.InputFilter![]);
+    method public boolean isEnabled();
+    method public void setAllCaps(boolean);
+    method public void setEnabled(boolean);
+    method public void updateTransformationMethod();
+    method public android.text.method.TransformationMethod? wrapTransformationMethod(android.text.method.TransformationMethod?);
+  }
+
+}
+
diff --git a/camera/camera-viewfinder/api/res-1.2.0-beta01.txt b/emoji2/emoji2-views-helper/api/res-1.3.0-beta01.txt
similarity index 100%
copy from camera/camera-viewfinder/api/res-1.2.0-beta01.txt
copy to emoji2/emoji2-views-helper/api/res-1.3.0-beta01.txt
diff --git a/camera/camera-viewfinder/api/res-1.2.0-beta02.txt b/emoji2/emoji2-views-helper/api/res-1.3.0-beta02.txt
similarity index 100%
rename from camera/camera-viewfinder/api/res-1.2.0-beta02.txt
rename to emoji2/emoji2-views-helper/api/res-1.3.0-beta02.txt
diff --git a/emoji2/emoji2-views-helper/api/restricted_1.3.0-beta01.txt b/emoji2/emoji2-views-helper/api/restricted_1.3.0-beta01.txt
new file mode 100644
index 0000000..30a6feb
--- /dev/null
+++ b/emoji2/emoji2-views-helper/api/restricted_1.3.0-beta01.txt
@@ -0,0 +1,27 @@
+// Signature format: 4.0
+package androidx.emoji2.viewsintegration {
+
+  public final class EmojiEditTextHelper {
+    ctor public EmojiEditTextHelper(android.widget.EditText);
+    ctor public EmojiEditTextHelper(android.widget.EditText, boolean);
+    method public android.text.method.KeyListener? getKeyListener(android.text.method.KeyListener?);
+    method public int getMaxEmojiCount();
+    method public boolean isEnabled();
+    method public android.view.inputmethod.InputConnection? onCreateInputConnection(android.view.inputmethod.InputConnection?, android.view.inputmethod.EditorInfo);
+    method public void setEnabled(boolean);
+    method public void setMaxEmojiCount(@IntRange(from=0) int);
+  }
+
+  public final class EmojiTextViewHelper {
+    ctor public EmojiTextViewHelper(android.widget.TextView);
+    ctor public EmojiTextViewHelper(android.widget.TextView, boolean);
+    method public android.text.InputFilter![] getFilters(android.text.InputFilter![]);
+    method public boolean isEnabled();
+    method public void setAllCaps(boolean);
+    method public void setEnabled(boolean);
+    method public void updateTransformationMethod();
+    method public android.text.method.TransformationMethod? wrapTransformationMethod(android.text.method.TransformationMethod?);
+  }
+
+}
+
diff --git a/emoji2/emoji2-views-helper/api/restricted_1.3.0-beta02.txt b/emoji2/emoji2-views-helper/api/restricted_1.3.0-beta02.txt
new file mode 100644
index 0000000..30a6feb
--- /dev/null
+++ b/emoji2/emoji2-views-helper/api/restricted_1.3.0-beta02.txt
@@ -0,0 +1,27 @@
+// Signature format: 4.0
+package androidx.emoji2.viewsintegration {
+
+  public final class EmojiEditTextHelper {
+    ctor public EmojiEditTextHelper(android.widget.EditText);
+    ctor public EmojiEditTextHelper(android.widget.EditText, boolean);
+    method public android.text.method.KeyListener? getKeyListener(android.text.method.KeyListener?);
+    method public int getMaxEmojiCount();
+    method public boolean isEnabled();
+    method public android.view.inputmethod.InputConnection? onCreateInputConnection(android.view.inputmethod.InputConnection?, android.view.inputmethod.EditorInfo);
+    method public void setEnabled(boolean);
+    method public void setMaxEmojiCount(@IntRange(from=0) int);
+  }
+
+  public final class EmojiTextViewHelper {
+    ctor public EmojiTextViewHelper(android.widget.TextView);
+    ctor public EmojiTextViewHelper(android.widget.TextView, boolean);
+    method public android.text.InputFilter![] getFilters(android.text.InputFilter![]);
+    method public boolean isEnabled();
+    method public void setAllCaps(boolean);
+    method public void setEnabled(boolean);
+    method public void updateTransformationMethod();
+    method public android.text.method.TransformationMethod? wrapTransformationMethod(android.text.method.TransformationMethod?);
+  }
+
+}
+
diff --git a/emoji2/emoji2-views/api/1.3.0-beta01.txt b/emoji2/emoji2-views/api/1.3.0-beta01.txt
new file mode 100644
index 0000000..879b30e
--- /dev/null
+++ b/emoji2/emoji2-views/api/1.3.0-beta01.txt
@@ -0,0 +1,34 @@
+// Signature format: 4.0
+package androidx.emoji2.widget {
+
+  public class EmojiButton extends android.widget.Button {
+    ctor public EmojiButton(android.content.Context);
+    ctor public EmojiButton(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiButton(android.content.Context, android.util.AttributeSet?, int);
+  }
+
+  public class EmojiEditText extends android.widget.EditText {
+    ctor public EmojiEditText(android.content.Context);
+    ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?, int);
+    method public int getMaxEmojiCount();
+    method public void setMaxEmojiCount(@IntRange(from=0) int);
+  }
+
+  public class EmojiExtractTextLayout extends android.widget.LinearLayout {
+    ctor public EmojiExtractTextLayout(android.content.Context);
+    ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?, int);
+    method public int getEmojiReplaceStrategy();
+    method public void onUpdateExtractingViews(android.inputmethodservice.InputMethodService, android.view.inputmethod.EditorInfo);
+    method public void setEmojiReplaceStrategy(int);
+  }
+
+  public class EmojiTextView extends android.widget.TextView {
+    ctor public EmojiTextView(android.content.Context);
+    ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?, int);
+  }
+
+}
+
diff --git a/emoji2/emoji2-views/api/1.3.0-beta02.txt b/emoji2/emoji2-views/api/1.3.0-beta02.txt
new file mode 100644
index 0000000..879b30e
--- /dev/null
+++ b/emoji2/emoji2-views/api/1.3.0-beta02.txt
@@ -0,0 +1,34 @@
+// Signature format: 4.0
+package androidx.emoji2.widget {
+
+  public class EmojiButton extends android.widget.Button {
+    ctor public EmojiButton(android.content.Context);
+    ctor public EmojiButton(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiButton(android.content.Context, android.util.AttributeSet?, int);
+  }
+
+  public class EmojiEditText extends android.widget.EditText {
+    ctor public EmojiEditText(android.content.Context);
+    ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?, int);
+    method public int getMaxEmojiCount();
+    method public void setMaxEmojiCount(@IntRange(from=0) int);
+  }
+
+  public class EmojiExtractTextLayout extends android.widget.LinearLayout {
+    ctor public EmojiExtractTextLayout(android.content.Context);
+    ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?, int);
+    method public int getEmojiReplaceStrategy();
+    method public void onUpdateExtractingViews(android.inputmethodservice.InputMethodService, android.view.inputmethod.EditorInfo);
+    method public void setEmojiReplaceStrategy(int);
+  }
+
+  public class EmojiTextView extends android.widget.TextView {
+    ctor public EmojiTextView(android.content.Context);
+    ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?, int);
+  }
+
+}
+
diff --git a/emoji2/emoji2-views/api/public_plus_experimental_1.3.0-beta01.txt b/emoji2/emoji2-views/api/public_plus_experimental_1.3.0-beta01.txt
new file mode 100644
index 0000000..879b30e
--- /dev/null
+++ b/emoji2/emoji2-views/api/public_plus_experimental_1.3.0-beta01.txt
@@ -0,0 +1,34 @@
+// Signature format: 4.0
+package androidx.emoji2.widget {
+
+  public class EmojiButton extends android.widget.Button {
+    ctor public EmojiButton(android.content.Context);
+    ctor public EmojiButton(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiButton(android.content.Context, android.util.AttributeSet?, int);
+  }
+
+  public class EmojiEditText extends android.widget.EditText {
+    ctor public EmojiEditText(android.content.Context);
+    ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?, int);
+    method public int getMaxEmojiCount();
+    method public void setMaxEmojiCount(@IntRange(from=0) int);
+  }
+
+  public class EmojiExtractTextLayout extends android.widget.LinearLayout {
+    ctor public EmojiExtractTextLayout(android.content.Context);
+    ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?, int);
+    method public int getEmojiReplaceStrategy();
+    method public void onUpdateExtractingViews(android.inputmethodservice.InputMethodService, android.view.inputmethod.EditorInfo);
+    method public void setEmojiReplaceStrategy(int);
+  }
+
+  public class EmojiTextView extends android.widget.TextView {
+    ctor public EmojiTextView(android.content.Context);
+    ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?, int);
+  }
+
+}
+
diff --git a/emoji2/emoji2-views/api/public_plus_experimental_1.3.0-beta02.txt b/emoji2/emoji2-views/api/public_plus_experimental_1.3.0-beta02.txt
new file mode 100644
index 0000000..879b30e
--- /dev/null
+++ b/emoji2/emoji2-views/api/public_plus_experimental_1.3.0-beta02.txt
@@ -0,0 +1,34 @@
+// Signature format: 4.0
+package androidx.emoji2.widget {
+
+  public class EmojiButton extends android.widget.Button {
+    ctor public EmojiButton(android.content.Context);
+    ctor public EmojiButton(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiButton(android.content.Context, android.util.AttributeSet?, int);
+  }
+
+  public class EmojiEditText extends android.widget.EditText {
+    ctor public EmojiEditText(android.content.Context);
+    ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?, int);
+    method public int getMaxEmojiCount();
+    method public void setMaxEmojiCount(@IntRange(from=0) int);
+  }
+
+  public class EmojiExtractTextLayout extends android.widget.LinearLayout {
+    ctor public EmojiExtractTextLayout(android.content.Context);
+    ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?, int);
+    method public int getEmojiReplaceStrategy();
+    method public void onUpdateExtractingViews(android.inputmethodservice.InputMethodService, android.view.inputmethod.EditorInfo);
+    method public void setEmojiReplaceStrategy(int);
+  }
+
+  public class EmojiTextView extends android.widget.TextView {
+    ctor public EmojiTextView(android.content.Context);
+    ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?, int);
+  }
+
+}
+
diff --git a/emoji2/emoji2-views/api/res-1.3.0-beta01.txt b/emoji2/emoji2-views/api/res-1.3.0-beta01.txt
new file mode 100644
index 0000000..8bc8423
--- /dev/null
+++ b/emoji2/emoji2-views/api/res-1.3.0-beta01.txt
@@ -0,0 +1,2 @@
+attr emojiReplaceStrategy
+attr maxEmojiCount
diff --git a/emoji2/emoji2-views/api/res-1.3.0-beta02.txt b/emoji2/emoji2-views/api/res-1.3.0-beta02.txt
new file mode 100644
index 0000000..8bc8423
--- /dev/null
+++ b/emoji2/emoji2-views/api/res-1.3.0-beta02.txt
@@ -0,0 +1,2 @@
+attr emojiReplaceStrategy
+attr maxEmojiCount
diff --git a/emoji2/emoji2-views/api/restricted_1.3.0-beta01.txt b/emoji2/emoji2-views/api/restricted_1.3.0-beta01.txt
new file mode 100644
index 0000000..879b30e
--- /dev/null
+++ b/emoji2/emoji2-views/api/restricted_1.3.0-beta01.txt
@@ -0,0 +1,34 @@
+// Signature format: 4.0
+package androidx.emoji2.widget {
+
+  public class EmojiButton extends android.widget.Button {
+    ctor public EmojiButton(android.content.Context);
+    ctor public EmojiButton(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiButton(android.content.Context, android.util.AttributeSet?, int);
+  }
+
+  public class EmojiEditText extends android.widget.EditText {
+    ctor public EmojiEditText(android.content.Context);
+    ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?, int);
+    method public int getMaxEmojiCount();
+    method public void setMaxEmojiCount(@IntRange(from=0) int);
+  }
+
+  public class EmojiExtractTextLayout extends android.widget.LinearLayout {
+    ctor public EmojiExtractTextLayout(android.content.Context);
+    ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?, int);
+    method public int getEmojiReplaceStrategy();
+    method public void onUpdateExtractingViews(android.inputmethodservice.InputMethodService, android.view.inputmethod.EditorInfo);
+    method public void setEmojiReplaceStrategy(int);
+  }
+
+  public class EmojiTextView extends android.widget.TextView {
+    ctor public EmojiTextView(android.content.Context);
+    ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?, int);
+  }
+
+}
+
diff --git a/emoji2/emoji2-views/api/restricted_1.3.0-beta02.txt b/emoji2/emoji2-views/api/restricted_1.3.0-beta02.txt
new file mode 100644
index 0000000..879b30e
--- /dev/null
+++ b/emoji2/emoji2-views/api/restricted_1.3.0-beta02.txt
@@ -0,0 +1,34 @@
+// Signature format: 4.0
+package androidx.emoji2.widget {
+
+  public class EmojiButton extends android.widget.Button {
+    ctor public EmojiButton(android.content.Context);
+    ctor public EmojiButton(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiButton(android.content.Context, android.util.AttributeSet?, int);
+  }
+
+  public class EmojiEditText extends android.widget.EditText {
+    ctor public EmojiEditText(android.content.Context);
+    ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?, int);
+    method public int getMaxEmojiCount();
+    method public void setMaxEmojiCount(@IntRange(from=0) int);
+  }
+
+  public class EmojiExtractTextLayout extends android.widget.LinearLayout {
+    ctor public EmojiExtractTextLayout(android.content.Context);
+    ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?, int);
+    method public int getEmojiReplaceStrategy();
+    method public void onUpdateExtractingViews(android.inputmethodservice.InputMethodService, android.view.inputmethod.EditorInfo);
+    method public void setEmojiReplaceStrategy(int);
+  }
+
+  public class EmojiTextView extends android.widget.TextView {
+    ctor public EmojiTextView(android.content.Context);
+    ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?, int);
+  }
+
+}
+
diff --git a/emoji2/emoji2/api/1.3.0-beta01.txt b/emoji2/emoji2/api/1.3.0-beta01.txt
new file mode 100644
index 0000000..11d9335
--- /dev/null
+++ b/emoji2/emoji2/api/1.3.0-beta01.txt
@@ -0,0 +1,131 @@
+// Signature format: 4.0
+package androidx.emoji2.text {
+
+  public final class DefaultEmojiCompatConfig {
+    method public static androidx.emoji2.text.FontRequestEmojiCompatConfig? create(android.content.Context);
+  }
+
+  @AnyThread public class EmojiCompat {
+    method public static androidx.emoji2.text.EmojiCompat get();
+    method public String getAssetSignature();
+    method public int getEmojiEnd(CharSequence, @IntRange(from=0) int);
+    method public int getEmojiMatch(CharSequence, @IntRange(from=0) int);
+    method public int getEmojiStart(CharSequence, @IntRange(from=0) int);
+    method public int getLoadState();
+    method public static boolean handleDeleteSurroundingText(android.view.inputmethod.InputConnection, android.text.Editable, @IntRange(from=0) int, @IntRange(from=0) int, boolean);
+    method public static boolean handleOnKeyDown(android.text.Editable, int, android.view.KeyEvent);
+    method @Deprecated public boolean hasEmojiGlyph(CharSequence);
+    method @Deprecated public boolean hasEmojiGlyph(CharSequence, @IntRange(from=0) int);
+    method public static androidx.emoji2.text.EmojiCompat? init(android.content.Context);
+    method public static androidx.emoji2.text.EmojiCompat init(androidx.emoji2.text.EmojiCompat.Config);
+    method public static boolean isConfigured();
+    method public void load();
+    method @CheckResult public CharSequence? process(CharSequence?);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int, int);
+    method public void registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public void unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public void updateEditorInfo(android.view.inputmethod.EditorInfo);
+    field public static final String EDITOR_INFO_METAVERSION_KEY = "android.support.text.emoji.emojiCompat_metadataVersion";
+    field public static final String EDITOR_INFO_REPLACE_ALL_KEY = "android.support.text.emoji.emojiCompat_replaceAll";
+    field public static final int EMOJI_FALLBACK = 2; // 0x2
+    field public static final int EMOJI_SUPPORTED = 1; // 0x1
+    field public static final int EMOJI_UNSUPPORTED = 0; // 0x0
+    field public static final int LOAD_STATE_DEFAULT = 3; // 0x3
+    field public static final int LOAD_STATE_FAILED = 2; // 0x2
+    field public static final int LOAD_STATE_LOADING = 0; // 0x0
+    field public static final int LOAD_STATE_SUCCEEDED = 1; // 0x1
+    field public static final int LOAD_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int LOAD_STRATEGY_MANUAL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_ALL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int REPLACE_STRATEGY_NON_EXISTENT = 2; // 0x2
+  }
+
+  public abstract static class EmojiCompat.Config {
+    ctor protected EmojiCompat.Config(androidx.emoji2.text.EmojiCompat.MetadataRepoLoader);
+    method protected final androidx.emoji2.text.EmojiCompat.MetadataRepoLoader getMetadataRepoLoader();
+    method public androidx.emoji2.text.EmojiCompat.Config registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorColor(@ColorInt int);
+    method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorEnabled(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setGlyphChecker(androidx.emoji2.text.EmojiCompat.GlyphChecker);
+    method public androidx.emoji2.text.EmojiCompat.Config setMetadataLoadStrategy(int);
+    method public androidx.emoji2.text.EmojiCompat.Config setReplaceAll(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setSpanFactory(androidx.emoji2.text.EmojiCompat.SpanFactory);
+    method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean, java.util.List<java.lang.Integer!>?);
+    method public androidx.emoji2.text.EmojiCompat.Config unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+  }
+
+  public static interface EmojiCompat.GlyphChecker {
+    method public boolean hasGlyph(CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+  }
+
+  public abstract static class EmojiCompat.InitCallback {
+    ctor public EmojiCompat.InitCallback();
+    method public void onFailed(Throwable?);
+    method public void onInitialized();
+  }
+
+  public static interface EmojiCompat.MetadataRepoLoader {
+    method public void load(androidx.emoji2.text.EmojiCompat.MetadataRepoLoaderCallback);
+  }
+
+  public abstract static class EmojiCompat.MetadataRepoLoaderCallback {
+    ctor public EmojiCompat.MetadataRepoLoaderCallback();
+    method public abstract void onFailed(Throwable?);
+    method public abstract void onLoaded(androidx.emoji2.text.MetadataRepo);
+  }
+
+  public static interface EmojiCompat.SpanFactory {
+    method @RequiresApi(19) public androidx.emoji2.text.EmojiSpan createSpan(androidx.emoji2.text.TypefaceEmojiRasterizer);
+  }
+
+  public class EmojiCompatInitializer implements androidx.startup.Initializer<java.lang.Boolean> {
+    ctor public EmojiCompatInitializer();
+    method public Boolean create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
+  @RequiresApi(19) public abstract class EmojiSpan extends android.text.style.ReplacementSpan {
+    method public int getSize(android.graphics.Paint, CharSequence!, int, int, android.graphics.Paint.FontMetricsInt?);
+    method public final androidx.emoji2.text.TypefaceEmojiRasterizer getTypefaceRasterizer();
+  }
+
+  public class FontRequestEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+    ctor public FontRequestEmojiCompatConfig(android.content.Context, androidx.core.provider.FontRequest);
+    method @Deprecated public androidx.emoji2.text.FontRequestEmojiCompatConfig setHandler(android.os.Handler?);
+    method public androidx.emoji2.text.FontRequestEmojiCompatConfig setLoadingExecutor(java.util.concurrent.Executor);
+    method public androidx.emoji2.text.FontRequestEmojiCompatConfig setRetryPolicy(androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy?);
+  }
+
+  public static class FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy extends androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy {
+    ctor public FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy(long);
+    method public long getRetryDelay();
+  }
+
+  public abstract static class FontRequestEmojiCompatConfig.RetryPolicy {
+    ctor public FontRequestEmojiCompatConfig.RetryPolicy();
+    method public abstract long getRetryDelay();
+  }
+
+  @AnyThread @RequiresApi(19) public final class MetadataRepo {
+    method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.io.InputStream) throws java.io.IOException;
+    method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.nio.ByteBuffer) throws java.io.IOException;
+    method public static androidx.emoji2.text.MetadataRepo create(android.content.res.AssetManager, String) throws java.io.IOException;
+  }
+
+  @AnyThread @RequiresApi(19) public class TypefaceEmojiRasterizer {
+    method public void draw(android.graphics.Canvas, float, float, android.graphics.Paint);
+    method public int getCodepointAt(int);
+    method public int getCodepointsLength();
+    method public int getHeight();
+    method public android.graphics.Typeface getTypeface();
+    method public int getWidth();
+    method public boolean isDefaultEmoji();
+    method public boolean isPreferredSystemRender();
+  }
+
+}
+
diff --git a/emoji2/emoji2/api/1.3.0-beta02.txt b/emoji2/emoji2/api/1.3.0-beta02.txt
new file mode 100644
index 0000000..11d9335
--- /dev/null
+++ b/emoji2/emoji2/api/1.3.0-beta02.txt
@@ -0,0 +1,131 @@
+// Signature format: 4.0
+package androidx.emoji2.text {
+
+  public final class DefaultEmojiCompatConfig {
+    method public static androidx.emoji2.text.FontRequestEmojiCompatConfig? create(android.content.Context);
+  }
+
+  @AnyThread public class EmojiCompat {
+    method public static androidx.emoji2.text.EmojiCompat get();
+    method public String getAssetSignature();
+    method public int getEmojiEnd(CharSequence, @IntRange(from=0) int);
+    method public int getEmojiMatch(CharSequence, @IntRange(from=0) int);
+    method public int getEmojiStart(CharSequence, @IntRange(from=0) int);
+    method public int getLoadState();
+    method public static boolean handleDeleteSurroundingText(android.view.inputmethod.InputConnection, android.text.Editable, @IntRange(from=0) int, @IntRange(from=0) int, boolean);
+    method public static boolean handleOnKeyDown(android.text.Editable, int, android.view.KeyEvent);
+    method @Deprecated public boolean hasEmojiGlyph(CharSequence);
+    method @Deprecated public boolean hasEmojiGlyph(CharSequence, @IntRange(from=0) int);
+    method public static androidx.emoji2.text.EmojiCompat? init(android.content.Context);
+    method public static androidx.emoji2.text.EmojiCompat init(androidx.emoji2.text.EmojiCompat.Config);
+    method public static boolean isConfigured();
+    method public void load();
+    method @CheckResult public CharSequence? process(CharSequence?);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int, int);
+    method public void registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public void unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public void updateEditorInfo(android.view.inputmethod.EditorInfo);
+    field public static final String EDITOR_INFO_METAVERSION_KEY = "android.support.text.emoji.emojiCompat_metadataVersion";
+    field public static final String EDITOR_INFO_REPLACE_ALL_KEY = "android.support.text.emoji.emojiCompat_replaceAll";
+    field public static final int EMOJI_FALLBACK = 2; // 0x2
+    field public static final int EMOJI_SUPPORTED = 1; // 0x1
+    field public static final int EMOJI_UNSUPPORTED = 0; // 0x0
+    field public static final int LOAD_STATE_DEFAULT = 3; // 0x3
+    field public static final int LOAD_STATE_FAILED = 2; // 0x2
+    field public static final int LOAD_STATE_LOADING = 0; // 0x0
+    field public static final int LOAD_STATE_SUCCEEDED = 1; // 0x1
+    field public static final int LOAD_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int LOAD_STRATEGY_MANUAL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_ALL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int REPLACE_STRATEGY_NON_EXISTENT = 2; // 0x2
+  }
+
+  public abstract static class EmojiCompat.Config {
+    ctor protected EmojiCompat.Config(androidx.emoji2.text.EmojiCompat.MetadataRepoLoader);
+    method protected final androidx.emoji2.text.EmojiCompat.MetadataRepoLoader getMetadataRepoLoader();
+    method public androidx.emoji2.text.EmojiCompat.Config registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorColor(@ColorInt int);
+    method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorEnabled(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setGlyphChecker(androidx.emoji2.text.EmojiCompat.GlyphChecker);
+    method public androidx.emoji2.text.EmojiCompat.Config setMetadataLoadStrategy(int);
+    method public androidx.emoji2.text.EmojiCompat.Config setReplaceAll(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setSpanFactory(androidx.emoji2.text.EmojiCompat.SpanFactory);
+    method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean, java.util.List<java.lang.Integer!>?);
+    method public androidx.emoji2.text.EmojiCompat.Config unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+  }
+
+  public static interface EmojiCompat.GlyphChecker {
+    method public boolean hasGlyph(CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+  }
+
+  public abstract static class EmojiCompat.InitCallback {
+    ctor public EmojiCompat.InitCallback();
+    method public void onFailed(Throwable?);
+    method public void onInitialized();
+  }
+
+  public static interface EmojiCompat.MetadataRepoLoader {
+    method public void load(androidx.emoji2.text.EmojiCompat.MetadataRepoLoaderCallback);
+  }
+
+  public abstract static class EmojiCompat.MetadataRepoLoaderCallback {
+    ctor public EmojiCompat.MetadataRepoLoaderCallback();
+    method public abstract void onFailed(Throwable?);
+    method public abstract void onLoaded(androidx.emoji2.text.MetadataRepo);
+  }
+
+  public static interface EmojiCompat.SpanFactory {
+    method @RequiresApi(19) public androidx.emoji2.text.EmojiSpan createSpan(androidx.emoji2.text.TypefaceEmojiRasterizer);
+  }
+
+  public class EmojiCompatInitializer implements androidx.startup.Initializer<java.lang.Boolean> {
+    ctor public EmojiCompatInitializer();
+    method public Boolean create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
+  @RequiresApi(19) public abstract class EmojiSpan extends android.text.style.ReplacementSpan {
+    method public int getSize(android.graphics.Paint, CharSequence!, int, int, android.graphics.Paint.FontMetricsInt?);
+    method public final androidx.emoji2.text.TypefaceEmojiRasterizer getTypefaceRasterizer();
+  }
+
+  public class FontRequestEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+    ctor public FontRequestEmojiCompatConfig(android.content.Context, androidx.core.provider.FontRequest);
+    method @Deprecated public androidx.emoji2.text.FontRequestEmojiCompatConfig setHandler(android.os.Handler?);
+    method public androidx.emoji2.text.FontRequestEmojiCompatConfig setLoadingExecutor(java.util.concurrent.Executor);
+    method public androidx.emoji2.text.FontRequestEmojiCompatConfig setRetryPolicy(androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy?);
+  }
+
+  public static class FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy extends androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy {
+    ctor public FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy(long);
+    method public long getRetryDelay();
+  }
+
+  public abstract static class FontRequestEmojiCompatConfig.RetryPolicy {
+    ctor public FontRequestEmojiCompatConfig.RetryPolicy();
+    method public abstract long getRetryDelay();
+  }
+
+  @AnyThread @RequiresApi(19) public final class MetadataRepo {
+    method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.io.InputStream) throws java.io.IOException;
+    method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.nio.ByteBuffer) throws java.io.IOException;
+    method public static androidx.emoji2.text.MetadataRepo create(android.content.res.AssetManager, String) throws java.io.IOException;
+  }
+
+  @AnyThread @RequiresApi(19) public class TypefaceEmojiRasterizer {
+    method public void draw(android.graphics.Canvas, float, float, android.graphics.Paint);
+    method public int getCodepointAt(int);
+    method public int getCodepointsLength();
+    method public int getHeight();
+    method public android.graphics.Typeface getTypeface();
+    method public int getWidth();
+    method public boolean isDefaultEmoji();
+    method public boolean isPreferredSystemRender();
+  }
+
+}
+
diff --git a/emoji2/emoji2/api/public_plus_experimental_1.3.0-beta01.txt b/emoji2/emoji2/api/public_plus_experimental_1.3.0-beta01.txt
new file mode 100644
index 0000000..11d9335
--- /dev/null
+++ b/emoji2/emoji2/api/public_plus_experimental_1.3.0-beta01.txt
@@ -0,0 +1,131 @@
+// Signature format: 4.0
+package androidx.emoji2.text {
+
+  public final class DefaultEmojiCompatConfig {
+    method public static androidx.emoji2.text.FontRequestEmojiCompatConfig? create(android.content.Context);
+  }
+
+  @AnyThread public class EmojiCompat {
+    method public static androidx.emoji2.text.EmojiCompat get();
+    method public String getAssetSignature();
+    method public int getEmojiEnd(CharSequence, @IntRange(from=0) int);
+    method public int getEmojiMatch(CharSequence, @IntRange(from=0) int);
+    method public int getEmojiStart(CharSequence, @IntRange(from=0) int);
+    method public int getLoadState();
+    method public static boolean handleDeleteSurroundingText(android.view.inputmethod.InputConnection, android.text.Editable, @IntRange(from=0) int, @IntRange(from=0) int, boolean);
+    method public static boolean handleOnKeyDown(android.text.Editable, int, android.view.KeyEvent);
+    method @Deprecated public boolean hasEmojiGlyph(CharSequence);
+    method @Deprecated public boolean hasEmojiGlyph(CharSequence, @IntRange(from=0) int);
+    method public static androidx.emoji2.text.EmojiCompat? init(android.content.Context);
+    method public static androidx.emoji2.text.EmojiCompat init(androidx.emoji2.text.EmojiCompat.Config);
+    method public static boolean isConfigured();
+    method public void load();
+    method @CheckResult public CharSequence? process(CharSequence?);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int, int);
+    method public void registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public void unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public void updateEditorInfo(android.view.inputmethod.EditorInfo);
+    field public static final String EDITOR_INFO_METAVERSION_KEY = "android.support.text.emoji.emojiCompat_metadataVersion";
+    field public static final String EDITOR_INFO_REPLACE_ALL_KEY = "android.support.text.emoji.emojiCompat_replaceAll";
+    field public static final int EMOJI_FALLBACK = 2; // 0x2
+    field public static final int EMOJI_SUPPORTED = 1; // 0x1
+    field public static final int EMOJI_UNSUPPORTED = 0; // 0x0
+    field public static final int LOAD_STATE_DEFAULT = 3; // 0x3
+    field public static final int LOAD_STATE_FAILED = 2; // 0x2
+    field public static final int LOAD_STATE_LOADING = 0; // 0x0
+    field public static final int LOAD_STATE_SUCCEEDED = 1; // 0x1
+    field public static final int LOAD_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int LOAD_STRATEGY_MANUAL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_ALL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int REPLACE_STRATEGY_NON_EXISTENT = 2; // 0x2
+  }
+
+  public abstract static class EmojiCompat.Config {
+    ctor protected EmojiCompat.Config(androidx.emoji2.text.EmojiCompat.MetadataRepoLoader);
+    method protected final androidx.emoji2.text.EmojiCompat.MetadataRepoLoader getMetadataRepoLoader();
+    method public androidx.emoji2.text.EmojiCompat.Config registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorColor(@ColorInt int);
+    method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorEnabled(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setGlyphChecker(androidx.emoji2.text.EmojiCompat.GlyphChecker);
+    method public androidx.emoji2.text.EmojiCompat.Config setMetadataLoadStrategy(int);
+    method public androidx.emoji2.text.EmojiCompat.Config setReplaceAll(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setSpanFactory(androidx.emoji2.text.EmojiCompat.SpanFactory);
+    method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean, java.util.List<java.lang.Integer!>?);
+    method public androidx.emoji2.text.EmojiCompat.Config unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+  }
+
+  public static interface EmojiCompat.GlyphChecker {
+    method public boolean hasGlyph(CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+  }
+
+  public abstract static class EmojiCompat.InitCallback {
+    ctor public EmojiCompat.InitCallback();
+    method public void onFailed(Throwable?);
+    method public void onInitialized();
+  }
+
+  public static interface EmojiCompat.MetadataRepoLoader {
+    method public void load(androidx.emoji2.text.EmojiCompat.MetadataRepoLoaderCallback);
+  }
+
+  public abstract static class EmojiCompat.MetadataRepoLoaderCallback {
+    ctor public EmojiCompat.MetadataRepoLoaderCallback();
+    method public abstract void onFailed(Throwable?);
+    method public abstract void onLoaded(androidx.emoji2.text.MetadataRepo);
+  }
+
+  public static interface EmojiCompat.SpanFactory {
+    method @RequiresApi(19) public androidx.emoji2.text.EmojiSpan createSpan(androidx.emoji2.text.TypefaceEmojiRasterizer);
+  }
+
+  public class EmojiCompatInitializer implements androidx.startup.Initializer<java.lang.Boolean> {
+    ctor public EmojiCompatInitializer();
+    method public Boolean create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
+  @RequiresApi(19) public abstract class EmojiSpan extends android.text.style.ReplacementSpan {
+    method public int getSize(android.graphics.Paint, CharSequence!, int, int, android.graphics.Paint.FontMetricsInt?);
+    method public final androidx.emoji2.text.TypefaceEmojiRasterizer getTypefaceRasterizer();
+  }
+
+  public class FontRequestEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+    ctor public FontRequestEmojiCompatConfig(android.content.Context, androidx.core.provider.FontRequest);
+    method @Deprecated public androidx.emoji2.text.FontRequestEmojiCompatConfig setHandler(android.os.Handler?);
+    method public androidx.emoji2.text.FontRequestEmojiCompatConfig setLoadingExecutor(java.util.concurrent.Executor);
+    method public androidx.emoji2.text.FontRequestEmojiCompatConfig setRetryPolicy(androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy?);
+  }
+
+  public static class FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy extends androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy {
+    ctor public FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy(long);
+    method public long getRetryDelay();
+  }
+
+  public abstract static class FontRequestEmojiCompatConfig.RetryPolicy {
+    ctor public FontRequestEmojiCompatConfig.RetryPolicy();
+    method public abstract long getRetryDelay();
+  }
+
+  @AnyThread @RequiresApi(19) public final class MetadataRepo {
+    method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.io.InputStream) throws java.io.IOException;
+    method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.nio.ByteBuffer) throws java.io.IOException;
+    method public static androidx.emoji2.text.MetadataRepo create(android.content.res.AssetManager, String) throws java.io.IOException;
+  }
+
+  @AnyThread @RequiresApi(19) public class TypefaceEmojiRasterizer {
+    method public void draw(android.graphics.Canvas, float, float, android.graphics.Paint);
+    method public int getCodepointAt(int);
+    method public int getCodepointsLength();
+    method public int getHeight();
+    method public android.graphics.Typeface getTypeface();
+    method public int getWidth();
+    method public boolean isDefaultEmoji();
+    method public boolean isPreferredSystemRender();
+  }
+
+}
+
diff --git a/emoji2/emoji2/api/public_plus_experimental_1.3.0-beta02.txt b/emoji2/emoji2/api/public_plus_experimental_1.3.0-beta02.txt
new file mode 100644
index 0000000..11d9335
--- /dev/null
+++ b/emoji2/emoji2/api/public_plus_experimental_1.3.0-beta02.txt
@@ -0,0 +1,131 @@
+// Signature format: 4.0
+package androidx.emoji2.text {
+
+  public final class DefaultEmojiCompatConfig {
+    method public static androidx.emoji2.text.FontRequestEmojiCompatConfig? create(android.content.Context);
+  }
+
+  @AnyThread public class EmojiCompat {
+    method public static androidx.emoji2.text.EmojiCompat get();
+    method public String getAssetSignature();
+    method public int getEmojiEnd(CharSequence, @IntRange(from=0) int);
+    method public int getEmojiMatch(CharSequence, @IntRange(from=0) int);
+    method public int getEmojiStart(CharSequence, @IntRange(from=0) int);
+    method public int getLoadState();
+    method public static boolean handleDeleteSurroundingText(android.view.inputmethod.InputConnection, android.text.Editable, @IntRange(from=0) int, @IntRange(from=0) int, boolean);
+    method public static boolean handleOnKeyDown(android.text.Editable, int, android.view.KeyEvent);
+    method @Deprecated public boolean hasEmojiGlyph(CharSequence);
+    method @Deprecated public boolean hasEmojiGlyph(CharSequence, @IntRange(from=0) int);
+    method public static androidx.emoji2.text.EmojiCompat? init(android.content.Context);
+    method public static androidx.emoji2.text.EmojiCompat init(androidx.emoji2.text.EmojiCompat.Config);
+    method public static boolean isConfigured();
+    method public void load();
+    method @CheckResult public CharSequence? process(CharSequence?);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int, int);
+    method public void registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public void unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public void updateEditorInfo(android.view.inputmethod.EditorInfo);
+    field public static final String EDITOR_INFO_METAVERSION_KEY = "android.support.text.emoji.emojiCompat_metadataVersion";
+    field public static final String EDITOR_INFO_REPLACE_ALL_KEY = "android.support.text.emoji.emojiCompat_replaceAll";
+    field public static final int EMOJI_FALLBACK = 2; // 0x2
+    field public static final int EMOJI_SUPPORTED = 1; // 0x1
+    field public static final int EMOJI_UNSUPPORTED = 0; // 0x0
+    field public static final int LOAD_STATE_DEFAULT = 3; // 0x3
+    field public static final int LOAD_STATE_FAILED = 2; // 0x2
+    field public static final int LOAD_STATE_LOADING = 0; // 0x0
+    field public static final int LOAD_STATE_SUCCEEDED = 1; // 0x1
+    field public static final int LOAD_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int LOAD_STRATEGY_MANUAL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_ALL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int REPLACE_STRATEGY_NON_EXISTENT = 2; // 0x2
+  }
+
+  public abstract static class EmojiCompat.Config {
+    ctor protected EmojiCompat.Config(androidx.emoji2.text.EmojiCompat.MetadataRepoLoader);
+    method protected final androidx.emoji2.text.EmojiCompat.MetadataRepoLoader getMetadataRepoLoader();
+    method public androidx.emoji2.text.EmojiCompat.Config registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorColor(@ColorInt int);
+    method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorEnabled(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setGlyphChecker(androidx.emoji2.text.EmojiCompat.GlyphChecker);
+    method public androidx.emoji2.text.EmojiCompat.Config setMetadataLoadStrategy(int);
+    method public androidx.emoji2.text.EmojiCompat.Config setReplaceAll(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setSpanFactory(androidx.emoji2.text.EmojiCompat.SpanFactory);
+    method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean, java.util.List<java.lang.Integer!>?);
+    method public androidx.emoji2.text.EmojiCompat.Config unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+  }
+
+  public static interface EmojiCompat.GlyphChecker {
+    method public boolean hasGlyph(CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+  }
+
+  public abstract static class EmojiCompat.InitCallback {
+    ctor public EmojiCompat.InitCallback();
+    method public void onFailed(Throwable?);
+    method public void onInitialized();
+  }
+
+  public static interface EmojiCompat.MetadataRepoLoader {
+    method public void load(androidx.emoji2.text.EmojiCompat.MetadataRepoLoaderCallback);
+  }
+
+  public abstract static class EmojiCompat.MetadataRepoLoaderCallback {
+    ctor public EmojiCompat.MetadataRepoLoaderCallback();
+    method public abstract void onFailed(Throwable?);
+    method public abstract void onLoaded(androidx.emoji2.text.MetadataRepo);
+  }
+
+  public static interface EmojiCompat.SpanFactory {
+    method @RequiresApi(19) public androidx.emoji2.text.EmojiSpan createSpan(androidx.emoji2.text.TypefaceEmojiRasterizer);
+  }
+
+  public class EmojiCompatInitializer implements androidx.startup.Initializer<java.lang.Boolean> {
+    ctor public EmojiCompatInitializer();
+    method public Boolean create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
+  @RequiresApi(19) public abstract class EmojiSpan extends android.text.style.ReplacementSpan {
+    method public int getSize(android.graphics.Paint, CharSequence!, int, int, android.graphics.Paint.FontMetricsInt?);
+    method public final androidx.emoji2.text.TypefaceEmojiRasterizer getTypefaceRasterizer();
+  }
+
+  public class FontRequestEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+    ctor public FontRequestEmojiCompatConfig(android.content.Context, androidx.core.provider.FontRequest);
+    method @Deprecated public androidx.emoji2.text.FontRequestEmojiCompatConfig setHandler(android.os.Handler?);
+    method public androidx.emoji2.text.FontRequestEmojiCompatConfig setLoadingExecutor(java.util.concurrent.Executor);
+    method public androidx.emoji2.text.FontRequestEmojiCompatConfig setRetryPolicy(androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy?);
+  }
+
+  public static class FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy extends androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy {
+    ctor public FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy(long);
+    method public long getRetryDelay();
+  }
+
+  public abstract static class FontRequestEmojiCompatConfig.RetryPolicy {
+    ctor public FontRequestEmojiCompatConfig.RetryPolicy();
+    method public abstract long getRetryDelay();
+  }
+
+  @AnyThread @RequiresApi(19) public final class MetadataRepo {
+    method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.io.InputStream) throws java.io.IOException;
+    method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.nio.ByteBuffer) throws java.io.IOException;
+    method public static androidx.emoji2.text.MetadataRepo create(android.content.res.AssetManager, String) throws java.io.IOException;
+  }
+
+  @AnyThread @RequiresApi(19) public class TypefaceEmojiRasterizer {
+    method public void draw(android.graphics.Canvas, float, float, android.graphics.Paint);
+    method public int getCodepointAt(int);
+    method public int getCodepointsLength();
+    method public int getHeight();
+    method public android.graphics.Typeface getTypeface();
+    method public int getWidth();
+    method public boolean isDefaultEmoji();
+    method public boolean isPreferredSystemRender();
+  }
+
+}
+
diff --git a/camera/camera-viewfinder/api/res-1.2.0-beta01.txt b/emoji2/emoji2/api/res-1.3.0-beta01.txt
similarity index 100%
copy from camera/camera-viewfinder/api/res-1.2.0-beta01.txt
copy to emoji2/emoji2/api/res-1.3.0-beta01.txt
diff --git a/camera/camera-viewfinder/api/res-1.2.0-beta02.txt b/emoji2/emoji2/api/res-1.3.0-beta02.txt
similarity index 100%
copy from camera/camera-viewfinder/api/res-1.2.0-beta02.txt
copy to emoji2/emoji2/api/res-1.3.0-beta02.txt
diff --git a/emoji2/emoji2/api/restricted_1.3.0-beta01.txt b/emoji2/emoji2/api/restricted_1.3.0-beta01.txt
new file mode 100644
index 0000000..11d9335
--- /dev/null
+++ b/emoji2/emoji2/api/restricted_1.3.0-beta01.txt
@@ -0,0 +1,131 @@
+// Signature format: 4.0
+package androidx.emoji2.text {
+
+  public final class DefaultEmojiCompatConfig {
+    method public static androidx.emoji2.text.FontRequestEmojiCompatConfig? create(android.content.Context);
+  }
+
+  @AnyThread public class EmojiCompat {
+    method public static androidx.emoji2.text.EmojiCompat get();
+    method public String getAssetSignature();
+    method public int getEmojiEnd(CharSequence, @IntRange(from=0) int);
+    method public int getEmojiMatch(CharSequence, @IntRange(from=0) int);
+    method public int getEmojiStart(CharSequence, @IntRange(from=0) int);
+    method public int getLoadState();
+    method public static boolean handleDeleteSurroundingText(android.view.inputmethod.InputConnection, android.text.Editable, @IntRange(from=0) int, @IntRange(from=0) int, boolean);
+    method public static boolean handleOnKeyDown(android.text.Editable, int, android.view.KeyEvent);
+    method @Deprecated public boolean hasEmojiGlyph(CharSequence);
+    method @Deprecated public boolean hasEmojiGlyph(CharSequence, @IntRange(from=0) int);
+    method public static androidx.emoji2.text.EmojiCompat? init(android.content.Context);
+    method public static androidx.emoji2.text.EmojiCompat init(androidx.emoji2.text.EmojiCompat.Config);
+    method public static boolean isConfigured();
+    method public void load();
+    method @CheckResult public CharSequence? process(CharSequence?);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int, int);
+    method public void registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public void unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public void updateEditorInfo(android.view.inputmethod.EditorInfo);
+    field public static final String EDITOR_INFO_METAVERSION_KEY = "android.support.text.emoji.emojiCompat_metadataVersion";
+    field public static final String EDITOR_INFO_REPLACE_ALL_KEY = "android.support.text.emoji.emojiCompat_replaceAll";
+    field public static final int EMOJI_FALLBACK = 2; // 0x2
+    field public static final int EMOJI_SUPPORTED = 1; // 0x1
+    field public static final int EMOJI_UNSUPPORTED = 0; // 0x0
+    field public static final int LOAD_STATE_DEFAULT = 3; // 0x3
+    field public static final int LOAD_STATE_FAILED = 2; // 0x2
+    field public static final int LOAD_STATE_LOADING = 0; // 0x0
+    field public static final int LOAD_STATE_SUCCEEDED = 1; // 0x1
+    field public static final int LOAD_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int LOAD_STRATEGY_MANUAL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_ALL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int REPLACE_STRATEGY_NON_EXISTENT = 2; // 0x2
+  }
+
+  public abstract static class EmojiCompat.Config {
+    ctor protected EmojiCompat.Config(androidx.emoji2.text.EmojiCompat.MetadataRepoLoader);
+    method protected final androidx.emoji2.text.EmojiCompat.MetadataRepoLoader getMetadataRepoLoader();
+    method public androidx.emoji2.text.EmojiCompat.Config registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorColor(@ColorInt int);
+    method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorEnabled(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setGlyphChecker(androidx.emoji2.text.EmojiCompat.GlyphChecker);
+    method public androidx.emoji2.text.EmojiCompat.Config setMetadataLoadStrategy(int);
+    method public androidx.emoji2.text.EmojiCompat.Config setReplaceAll(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setSpanFactory(androidx.emoji2.text.EmojiCompat.SpanFactory);
+    method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean, java.util.List<java.lang.Integer!>?);
+    method public androidx.emoji2.text.EmojiCompat.Config unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+  }
+
+  public static interface EmojiCompat.GlyphChecker {
+    method public boolean hasGlyph(CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+  }
+
+  public abstract static class EmojiCompat.InitCallback {
+    ctor public EmojiCompat.InitCallback();
+    method public void onFailed(Throwable?);
+    method public void onInitialized();
+  }
+
+  public static interface EmojiCompat.MetadataRepoLoader {
+    method public void load(androidx.emoji2.text.EmojiCompat.MetadataRepoLoaderCallback);
+  }
+
+  public abstract static class EmojiCompat.MetadataRepoLoaderCallback {
+    ctor public EmojiCompat.MetadataRepoLoaderCallback();
+    method public abstract void onFailed(Throwable?);
+    method public abstract void onLoaded(androidx.emoji2.text.MetadataRepo);
+  }
+
+  public static interface EmojiCompat.SpanFactory {
+    method @RequiresApi(19) public androidx.emoji2.text.EmojiSpan createSpan(androidx.emoji2.text.TypefaceEmojiRasterizer);
+  }
+
+  public class EmojiCompatInitializer implements androidx.startup.Initializer<java.lang.Boolean> {
+    ctor public EmojiCompatInitializer();
+    method public Boolean create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
+  @RequiresApi(19) public abstract class EmojiSpan extends android.text.style.ReplacementSpan {
+    method public int getSize(android.graphics.Paint, CharSequence!, int, int, android.graphics.Paint.FontMetricsInt?);
+    method public final androidx.emoji2.text.TypefaceEmojiRasterizer getTypefaceRasterizer();
+  }
+
+  public class FontRequestEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+    ctor public FontRequestEmojiCompatConfig(android.content.Context, androidx.core.provider.FontRequest);
+    method @Deprecated public androidx.emoji2.text.FontRequestEmojiCompatConfig setHandler(android.os.Handler?);
+    method public androidx.emoji2.text.FontRequestEmojiCompatConfig setLoadingExecutor(java.util.concurrent.Executor);
+    method public androidx.emoji2.text.FontRequestEmojiCompatConfig setRetryPolicy(androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy?);
+  }
+
+  public static class FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy extends androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy {
+    ctor public FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy(long);
+    method public long getRetryDelay();
+  }
+
+  public abstract static class FontRequestEmojiCompatConfig.RetryPolicy {
+    ctor public FontRequestEmojiCompatConfig.RetryPolicy();
+    method public abstract long getRetryDelay();
+  }
+
+  @AnyThread @RequiresApi(19) public final class MetadataRepo {
+    method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.io.InputStream) throws java.io.IOException;
+    method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.nio.ByteBuffer) throws java.io.IOException;
+    method public static androidx.emoji2.text.MetadataRepo create(android.content.res.AssetManager, String) throws java.io.IOException;
+  }
+
+  @AnyThread @RequiresApi(19) public class TypefaceEmojiRasterizer {
+    method public void draw(android.graphics.Canvas, float, float, android.graphics.Paint);
+    method public int getCodepointAt(int);
+    method public int getCodepointsLength();
+    method public int getHeight();
+    method public android.graphics.Typeface getTypeface();
+    method public int getWidth();
+    method public boolean isDefaultEmoji();
+    method public boolean isPreferredSystemRender();
+  }
+
+}
+
diff --git a/emoji2/emoji2/api/restricted_1.3.0-beta02.txt b/emoji2/emoji2/api/restricted_1.3.0-beta02.txt
new file mode 100644
index 0000000..11d9335
--- /dev/null
+++ b/emoji2/emoji2/api/restricted_1.3.0-beta02.txt
@@ -0,0 +1,131 @@
+// Signature format: 4.0
+package androidx.emoji2.text {
+
+  public final class DefaultEmojiCompatConfig {
+    method public static androidx.emoji2.text.FontRequestEmojiCompatConfig? create(android.content.Context);
+  }
+
+  @AnyThread public class EmojiCompat {
+    method public static androidx.emoji2.text.EmojiCompat get();
+    method public String getAssetSignature();
+    method public int getEmojiEnd(CharSequence, @IntRange(from=0) int);
+    method public int getEmojiMatch(CharSequence, @IntRange(from=0) int);
+    method public int getEmojiStart(CharSequence, @IntRange(from=0) int);
+    method public int getLoadState();
+    method public static boolean handleDeleteSurroundingText(android.view.inputmethod.InputConnection, android.text.Editable, @IntRange(from=0) int, @IntRange(from=0) int, boolean);
+    method public static boolean handleOnKeyDown(android.text.Editable, int, android.view.KeyEvent);
+    method @Deprecated public boolean hasEmojiGlyph(CharSequence);
+    method @Deprecated public boolean hasEmojiGlyph(CharSequence, @IntRange(from=0) int);
+    method public static androidx.emoji2.text.EmojiCompat? init(android.content.Context);
+    method public static androidx.emoji2.text.EmojiCompat init(androidx.emoji2.text.EmojiCompat.Config);
+    method public static boolean isConfigured();
+    method public void load();
+    method @CheckResult public CharSequence? process(CharSequence?);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int, int);
+    method public void registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public void unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public void updateEditorInfo(android.view.inputmethod.EditorInfo);
+    field public static final String EDITOR_INFO_METAVERSION_KEY = "android.support.text.emoji.emojiCompat_metadataVersion";
+    field public static final String EDITOR_INFO_REPLACE_ALL_KEY = "android.support.text.emoji.emojiCompat_replaceAll";
+    field public static final int EMOJI_FALLBACK = 2; // 0x2
+    field public static final int EMOJI_SUPPORTED = 1; // 0x1
+    field public static final int EMOJI_UNSUPPORTED = 0; // 0x0
+    field public static final int LOAD_STATE_DEFAULT = 3; // 0x3
+    field public static final int LOAD_STATE_FAILED = 2; // 0x2
+    field public static final int LOAD_STATE_LOADING = 0; // 0x0
+    field public static final int LOAD_STATE_SUCCEEDED = 1; // 0x1
+    field public static final int LOAD_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int LOAD_STRATEGY_MANUAL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_ALL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int REPLACE_STRATEGY_NON_EXISTENT = 2; // 0x2
+  }
+
+  public abstract static class EmojiCompat.Config {
+    ctor protected EmojiCompat.Config(androidx.emoji2.text.EmojiCompat.MetadataRepoLoader);
+    method protected final androidx.emoji2.text.EmojiCompat.MetadataRepoLoader getMetadataRepoLoader();
+    method public androidx.emoji2.text.EmojiCompat.Config registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorColor(@ColorInt int);
+    method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorEnabled(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setGlyphChecker(androidx.emoji2.text.EmojiCompat.GlyphChecker);
+    method public androidx.emoji2.text.EmojiCompat.Config setMetadataLoadStrategy(int);
+    method public androidx.emoji2.text.EmojiCompat.Config setReplaceAll(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setSpanFactory(androidx.emoji2.text.EmojiCompat.SpanFactory);
+    method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean, java.util.List<java.lang.Integer!>?);
+    method public androidx.emoji2.text.EmojiCompat.Config unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+  }
+
+  public static interface EmojiCompat.GlyphChecker {
+    method public boolean hasGlyph(CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+  }
+
+  public abstract static class EmojiCompat.InitCallback {
+    ctor public EmojiCompat.InitCallback();
+    method public void onFailed(Throwable?);
+    method public void onInitialized();
+  }
+
+  public static interface EmojiCompat.MetadataRepoLoader {
+    method public void load(androidx.emoji2.text.EmojiCompat.MetadataRepoLoaderCallback);
+  }
+
+  public abstract static class EmojiCompat.MetadataRepoLoaderCallback {
+    ctor public EmojiCompat.MetadataRepoLoaderCallback();
+    method public abstract void onFailed(Throwable?);
+    method public abstract void onLoaded(androidx.emoji2.text.MetadataRepo);
+  }
+
+  public static interface EmojiCompat.SpanFactory {
+    method @RequiresApi(19) public androidx.emoji2.text.EmojiSpan createSpan(androidx.emoji2.text.TypefaceEmojiRasterizer);
+  }
+
+  public class EmojiCompatInitializer implements androidx.startup.Initializer<java.lang.Boolean> {
+    ctor public EmojiCompatInitializer();
+    method public Boolean create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
+  @RequiresApi(19) public abstract class EmojiSpan extends android.text.style.ReplacementSpan {
+    method public int getSize(android.graphics.Paint, CharSequence!, int, int, android.graphics.Paint.FontMetricsInt?);
+    method public final androidx.emoji2.text.TypefaceEmojiRasterizer getTypefaceRasterizer();
+  }
+
+  public class FontRequestEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+    ctor public FontRequestEmojiCompatConfig(android.content.Context, androidx.core.provider.FontRequest);
+    method @Deprecated public androidx.emoji2.text.FontRequestEmojiCompatConfig setHandler(android.os.Handler?);
+    method public androidx.emoji2.text.FontRequestEmojiCompatConfig setLoadingExecutor(java.util.concurrent.Executor);
+    method public androidx.emoji2.text.FontRequestEmojiCompatConfig setRetryPolicy(androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy?);
+  }
+
+  public static class FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy extends androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy {
+    ctor public FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy(long);
+    method public long getRetryDelay();
+  }
+
+  public abstract static class FontRequestEmojiCompatConfig.RetryPolicy {
+    ctor public FontRequestEmojiCompatConfig.RetryPolicy();
+    method public abstract long getRetryDelay();
+  }
+
+  @AnyThread @RequiresApi(19) public final class MetadataRepo {
+    method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.io.InputStream) throws java.io.IOException;
+    method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.nio.ByteBuffer) throws java.io.IOException;
+    method public static androidx.emoji2.text.MetadataRepo create(android.content.res.AssetManager, String) throws java.io.IOException;
+  }
+
+  @AnyThread @RequiresApi(19) public class TypefaceEmojiRasterizer {
+    method public void draw(android.graphics.Canvas, float, float, android.graphics.Paint);
+    method public int getCodepointAt(int);
+    method public int getCodepointsLength();
+    method public int getHeight();
+    method public android.graphics.Typeface getTypeface();
+    method public int getWidth();
+    method public boolean isDefaultEmoji();
+    method public boolean isPreferredSystemRender();
+  }
+
+}
+
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/external/paparazzi/paparazzi/build.gradle b/external/paparazzi/paparazzi/build.gradle
index 3fbe6a3..8e87582 100644
--- a/external/paparazzi/paparazzi/build.gradle
+++ b/external/paparazzi/paparazzi/build.gradle
@@ -39,7 +39,7 @@
 
     compileOnlyAarAsJar("androidx.compose.runtime:runtime:1.2.1")
     compileOnlyAarAsJar("androidx.compose.ui:ui:1.2.1")
-    compileOnly("androidx.lifecycle:lifecycle-common:2.5.0")
+    compileOnly(project(":lifecycle:lifecycle-common"))
     compileOnlyAarAsJar(project(":lifecycle:lifecycle-runtime"))
     compileOnlyAarAsJar("androidx.savedstate:savedstate:1.2.0")
 
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt
index da62b21..0a96bcf 100644
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt
+++ b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt
@@ -602,7 +602,8 @@
     private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
     private val savedStateRegistryController = SavedStateRegistryController.create(this)
 
-    override fun getLifecycle(): Lifecycle = lifecycleRegistry
+    override val lifecycle: Lifecycle
+      get() = lifecycleRegistry
     override val savedStateRegistry: SavedStateRegistry =
       savedStateRegistryController.savedStateRegistry
 
diff --git a/fragment/OWNERS b/fragment/OWNERS
index 94b111e..b867087 100644
--- a/fragment/OWNERS
+++ b/fragment/OWNERS
@@ -1,4 +1,4 @@
-# Bug component: 460964
+# Bug component: 461227
 ilake@google.com
 jbwoods@google.com
 mount@google.com
diff --git a/fragment/fragment-ktx/api/current.txt b/fragment/fragment-ktx/api/current.txt
index b582114..052abdb 100644
--- a/fragment/fragment-ktx/api/current.txt
+++ b/fragment/fragment-ktx/api/current.txt
@@ -15,9 +15,9 @@
   }
 
   public final class FragmentTransactionKt {
-    method public static inline <reified F extends androidx.fragment.app.Fragment> androidx.fragment.app.FragmentTransaction add(androidx.fragment.app.FragmentTransaction, @IdRes int containerViewId, optional String tag, optional android.os.Bundle? args);
+    method public static inline <reified F extends androidx.fragment.app.Fragment> androidx.fragment.app.FragmentTransaction add(androidx.fragment.app.FragmentTransaction, @IdRes int containerViewId, optional String? tag, optional android.os.Bundle? args);
     method public static inline <reified F extends androidx.fragment.app.Fragment> androidx.fragment.app.FragmentTransaction add(androidx.fragment.app.FragmentTransaction, String tag, optional android.os.Bundle? args);
-    method public static inline <reified F extends androidx.fragment.app.Fragment> androidx.fragment.app.FragmentTransaction replace(androidx.fragment.app.FragmentTransaction, @IdRes int containerViewId, optional String tag, optional android.os.Bundle? args);
+    method public static inline <reified F extends androidx.fragment.app.Fragment> androidx.fragment.app.FragmentTransaction replace(androidx.fragment.app.FragmentTransaction, @IdRes int containerViewId, optional String? tag, optional android.os.Bundle? args);
   }
 
   public final class FragmentViewModelLazyKt {
diff --git a/fragment/fragment-ktx/api/public_plus_experimental_current.txt b/fragment/fragment-ktx/api/public_plus_experimental_current.txt
index b582114..052abdb 100644
--- a/fragment/fragment-ktx/api/public_plus_experimental_current.txt
+++ b/fragment/fragment-ktx/api/public_plus_experimental_current.txt
@@ -15,9 +15,9 @@
   }
 
   public final class FragmentTransactionKt {
-    method public static inline <reified F extends androidx.fragment.app.Fragment> androidx.fragment.app.FragmentTransaction add(androidx.fragment.app.FragmentTransaction, @IdRes int containerViewId, optional String tag, optional android.os.Bundle? args);
+    method public static inline <reified F extends androidx.fragment.app.Fragment> androidx.fragment.app.FragmentTransaction add(androidx.fragment.app.FragmentTransaction, @IdRes int containerViewId, optional String? tag, optional android.os.Bundle? args);
     method public static inline <reified F extends androidx.fragment.app.Fragment> androidx.fragment.app.FragmentTransaction add(androidx.fragment.app.FragmentTransaction, String tag, optional android.os.Bundle? args);
-    method public static inline <reified F extends androidx.fragment.app.Fragment> androidx.fragment.app.FragmentTransaction replace(androidx.fragment.app.FragmentTransaction, @IdRes int containerViewId, optional String tag, optional android.os.Bundle? args);
+    method public static inline <reified F extends androidx.fragment.app.Fragment> androidx.fragment.app.FragmentTransaction replace(androidx.fragment.app.FragmentTransaction, @IdRes int containerViewId, optional String? tag, optional android.os.Bundle? args);
   }
 
   public final class FragmentViewModelLazyKt {
diff --git a/fragment/fragment-ktx/api/restricted_current.txt b/fragment/fragment-ktx/api/restricted_current.txt
index b582114..052abdb 100644
--- a/fragment/fragment-ktx/api/restricted_current.txt
+++ b/fragment/fragment-ktx/api/restricted_current.txt
@@ -15,9 +15,9 @@
   }
 
   public final class FragmentTransactionKt {
-    method public static inline <reified F extends androidx.fragment.app.Fragment> androidx.fragment.app.FragmentTransaction add(androidx.fragment.app.FragmentTransaction, @IdRes int containerViewId, optional String tag, optional android.os.Bundle? args);
+    method public static inline <reified F extends androidx.fragment.app.Fragment> androidx.fragment.app.FragmentTransaction add(androidx.fragment.app.FragmentTransaction, @IdRes int containerViewId, optional String? tag, optional android.os.Bundle? args);
     method public static inline <reified F extends androidx.fragment.app.Fragment> androidx.fragment.app.FragmentTransaction add(androidx.fragment.app.FragmentTransaction, String tag, optional android.os.Bundle? args);
-    method public static inline <reified F extends androidx.fragment.app.Fragment> androidx.fragment.app.FragmentTransaction replace(androidx.fragment.app.FragmentTransaction, @IdRes int containerViewId, optional String tag, optional android.os.Bundle? args);
+    method public static inline <reified F extends androidx.fragment.app.Fragment> androidx.fragment.app.FragmentTransaction replace(androidx.fragment.app.FragmentTransaction, @IdRes int containerViewId, optional String? tag, optional android.os.Bundle? args);
   }
 
   public final class FragmentViewModelLazyKt {
diff --git a/fragment/fragment-ktx/src/androidTest/java/androidx/fragment/app/FragmentViewModelLazyTest.kt b/fragment/fragment-ktx/src/androidTest/java/androidx/fragment/app/FragmentViewModelLazyTest.kt
index 5ab1dda..504a5af 100644
--- a/fragment/fragment-ktx/src/androidTest/java/androidx/fragment/app/FragmentViewModelLazyTest.kt
+++ b/fragment/fragment-ktx/src/androidTest/java/androidx/fragment/app/FragmentViewModelLazyTest.kt
@@ -99,18 +99,17 @@
             enableSavedStateHandles()
         }
 
-        override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
-            return SavedStateViewModelFactory()
-        }
+        override val defaultViewModelProviderFactory = SavedStateViewModelFactory()
 
-        override fun getDefaultViewModelCreationExtras(): CreationExtras {
-            val extras = MutableCreationExtras()
-            extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] = application
-            extras[SAVED_STATE_REGISTRY_OWNER_KEY] = this
-            extras[VIEW_MODEL_STORE_OWNER_KEY] = this
-            extras[DEFAULT_ARGS_KEY] = bundleOf("test" to "value")
-            return extras
-        }
+        override val defaultViewModelCreationExtras: CreationExtras
+            get() {
+                val extras = MutableCreationExtras()
+                extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] = application
+                extras[SAVED_STATE_REGISTRY_OWNER_KEY] = this
+                extras[VIEW_MODEL_STORE_OWNER_KEY] = this
+                extras[DEFAULT_ARGS_KEY] = bundleOf("test" to "value")
+                return extras
+            }
     }
 
     class TestViewModel : ViewModel()
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..2a5c8e1 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/266247269
             .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/266247269
             .run()
             .expect(
                 """
@@ -257,6 +260,7 @@
             DIALOG_FRAGMENT,
             ALERT_DIALOG
         ).allowCompilationErrors(false)
+            .skipTestModes(TestMode.IMPORT_ALIAS, TestMode.TYPE_ALIAS) // b/266247269
             .run()
             .expect(
                 """
diff --git a/fragment/fragment-testing-lint/src/main/java/androidx/fragment/testing/lint/GradleConfigurationDetector.kt b/fragment/fragment-testing-lint/src/main/java/androidx/fragment/testing/lint/GradleConfigurationDetector.kt
index f2b2317..1a50e05 100644
--- a/fragment/fragment-testing-lint/src/main/java/androidx/fragment/testing/lint/GradleConfigurationDetector.kt
+++ b/fragment/fragment-testing-lint/src/main/java/androidx/fragment/testing/lint/GradleConfigurationDetector.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
@@ -65,14 +66,17 @@
         if (library.startsWith("androidx.fragment:fragment-testing") &&
             property != "debugImplementation"
         ) {
-            context.report(
-                ISSUE, statementCookie, context.getLocation(statementCookie),
-                "Replace with debugImplementation.",
-                fix().replace()
-                    .text(property)
-                    .with("debugImplementation")
-                    .build()
-            )
+            val incident = Incident(context)
+                .issue(ISSUE)
+                .location(context.getLocation(statementCookie))
+                .message("Replace with debugImplementation.")
+                .fix(
+                    fix().replace()
+                        .text(property)
+                        .with("debugImplementation")
+                        .build()
+                )
+            context.report(incident)
         }
     }
 
diff --git a/fragment/fragment-testing-manifest-lint/src/main/java/androidx/fragment/testing/manifest/lint/GradleConfigurationDetector.kt b/fragment/fragment-testing-manifest-lint/src/main/java/androidx/fragment/testing/manifest/lint/GradleConfigurationDetector.kt
index c493e4f..05e3d08 100644
--- a/fragment/fragment-testing-manifest-lint/src/main/java/androidx/fragment/testing/manifest/lint/GradleConfigurationDetector.kt
+++ b/fragment/fragment-testing-manifest-lint/src/main/java/androidx/fragment/testing/manifest/lint/GradleConfigurationDetector.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
@@ -65,14 +66,17 @@
         if (library.startsWith("androidx.fragment:fragment-testing-manifest") &&
             property != "debugImplementation"
         ) {
-            context.report(
-                ISSUE, statementCookie, context.getLocation(statementCookie),
-                "Replace with debugImplementation.",
-                fix().replace()
-                    .text(property)
-                    .with("debugImplementation")
-                    .build()
-            )
+            val incident = Incident(context)
+                .issue(ISSUE)
+                .location(context.getLocation(statementCookie))
+                .message("Replace with debugImplementation.")
+                .fix(
+                    fix().replace()
+                        .text(property)
+                        .with("debugImplementation")
+                        .build()
+                )
+            context.report(incident)
         }
     }
 
diff --git a/fragment/fragment/api/current.txt b/fragment/fragment/api/current.txt
index 76aeea3..8417281 100644
--- a/fragment/fragment/api/current.txt
+++ b/fragment/fragment/api/current.txt
@@ -145,11 +145,11 @@
     method @Deprecated public void setTargetFragment(androidx.fragment.app.Fragment?, int);
     method @Deprecated public void setUserVisibleHint(boolean);
     method public boolean shouldShowRequestPermissionRationale(String);
-    method public void startActivity(android.content.Intent!);
-    method public void startActivity(android.content.Intent!, android.os.Bundle?);
-    method @Deprecated public void startActivityForResult(android.content.Intent!, int);
-    method @Deprecated public void startActivityForResult(android.content.Intent!, int, android.os.Bundle?);
-    method @Deprecated public void startIntentSenderForResult(android.content.IntentSender!, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
+    method public void startActivity(android.content.Intent);
+    method public void startActivity(android.content.Intent, android.os.Bundle?);
+    method @Deprecated public void startActivityForResult(android.content.Intent, int);
+    method @Deprecated public void startActivityForResult(android.content.Intent, int, android.os.Bundle?);
+    method @Deprecated public void startIntentSenderForResult(android.content.IntentSender, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
     method public void startPostponedEnterTransition();
     method public void unregisterForContextMenu(android.view.View);
   }
@@ -174,9 +174,9 @@
     method public void onStateNotSaved();
     method public void setEnterSharedElementCallback(androidx.core.app.SharedElementCallback?);
     method public void setExitSharedElementCallback(androidx.core.app.SharedElementCallback?);
-    method public void startActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent!, int);
-    method public void startActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent!, int, android.os.Bundle?);
-    method @Deprecated public void startIntentSenderFromFragment(androidx.fragment.app.Fragment, android.content.IntentSender!, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
+    method public void startActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent, int);
+    method public void startActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent, int, android.os.Bundle?);
+    method @Deprecated public void startIntentSenderFromFragment(androidx.fragment.app.Fragment, android.content.IntentSender, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
     method public void supportFinishAfterTransition();
     method @Deprecated public void supportInvalidateOptionsMenu();
     method public void supportPostponeEnterTransition();
@@ -261,9 +261,9 @@
     method @Deprecated public void onRequestPermissionsFromFragment(androidx.fragment.app.Fragment, String![], int);
     method public boolean onShouldSaveFragmentState(androidx.fragment.app.Fragment);
     method public boolean onShouldShowRequestPermissionRationale(String);
-    method public void onStartActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent!, int);
-    method public void onStartActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent!, int, android.os.Bundle?);
-    method @Deprecated public void onStartIntentSenderFromFragment(androidx.fragment.app.Fragment, android.content.IntentSender!, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
+    method public void onStartActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent, int);
+    method public void onStartActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent, int, android.os.Bundle?);
+    method @Deprecated public void onStartIntentSenderFromFragment(androidx.fragment.app.Fragment, android.content.IntentSender, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
     method public void onSupportInvalidateOptionsMenu();
   }
 
diff --git a/fragment/fragment/api/public_plus_experimental_current.txt b/fragment/fragment/api/public_plus_experimental_current.txt
index 76aeea3..8417281 100644
--- a/fragment/fragment/api/public_plus_experimental_current.txt
+++ b/fragment/fragment/api/public_plus_experimental_current.txt
@@ -145,11 +145,11 @@
     method @Deprecated public void setTargetFragment(androidx.fragment.app.Fragment?, int);
     method @Deprecated public void setUserVisibleHint(boolean);
     method public boolean shouldShowRequestPermissionRationale(String);
-    method public void startActivity(android.content.Intent!);
-    method public void startActivity(android.content.Intent!, android.os.Bundle?);
-    method @Deprecated public void startActivityForResult(android.content.Intent!, int);
-    method @Deprecated public void startActivityForResult(android.content.Intent!, int, android.os.Bundle?);
-    method @Deprecated public void startIntentSenderForResult(android.content.IntentSender!, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
+    method public void startActivity(android.content.Intent);
+    method public void startActivity(android.content.Intent, android.os.Bundle?);
+    method @Deprecated public void startActivityForResult(android.content.Intent, int);
+    method @Deprecated public void startActivityForResult(android.content.Intent, int, android.os.Bundle?);
+    method @Deprecated public void startIntentSenderForResult(android.content.IntentSender, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
     method public void startPostponedEnterTransition();
     method public void unregisterForContextMenu(android.view.View);
   }
@@ -174,9 +174,9 @@
     method public void onStateNotSaved();
     method public void setEnterSharedElementCallback(androidx.core.app.SharedElementCallback?);
     method public void setExitSharedElementCallback(androidx.core.app.SharedElementCallback?);
-    method public void startActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent!, int);
-    method public void startActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent!, int, android.os.Bundle?);
-    method @Deprecated public void startIntentSenderFromFragment(androidx.fragment.app.Fragment, android.content.IntentSender!, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
+    method public void startActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent, int);
+    method public void startActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent, int, android.os.Bundle?);
+    method @Deprecated public void startIntentSenderFromFragment(androidx.fragment.app.Fragment, android.content.IntentSender, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
     method public void supportFinishAfterTransition();
     method @Deprecated public void supportInvalidateOptionsMenu();
     method public void supportPostponeEnterTransition();
@@ -261,9 +261,9 @@
     method @Deprecated public void onRequestPermissionsFromFragment(androidx.fragment.app.Fragment, String![], int);
     method public boolean onShouldSaveFragmentState(androidx.fragment.app.Fragment);
     method public boolean onShouldShowRequestPermissionRationale(String);
-    method public void onStartActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent!, int);
-    method public void onStartActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent!, int, android.os.Bundle?);
-    method @Deprecated public void onStartIntentSenderFromFragment(androidx.fragment.app.Fragment, android.content.IntentSender!, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
+    method public void onStartActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent, int);
+    method public void onStartActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent, int, android.os.Bundle?);
+    method @Deprecated public void onStartIntentSenderFromFragment(androidx.fragment.app.Fragment, android.content.IntentSender, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
     method public void onSupportInvalidateOptionsMenu();
   }
 
diff --git a/fragment/fragment/api/restricted_current.txt b/fragment/fragment/api/restricted_current.txt
index bdcefeb..3671696 100644
--- a/fragment/fragment/api/restricted_current.txt
+++ b/fragment/fragment/api/restricted_current.txt
@@ -149,11 +149,11 @@
     method @Deprecated public void setTargetFragment(androidx.fragment.app.Fragment?, int);
     method @Deprecated public void setUserVisibleHint(boolean);
     method public boolean shouldShowRequestPermissionRationale(String);
-    method public void startActivity(android.content.Intent!);
-    method public void startActivity(android.content.Intent!, android.os.Bundle?);
-    method @Deprecated public void startActivityForResult(android.content.Intent!, int);
-    method @Deprecated public void startActivityForResult(android.content.Intent!, int, android.os.Bundle?);
-    method @Deprecated public void startIntentSenderForResult(android.content.IntentSender!, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
+    method public void startActivity(android.content.Intent);
+    method public void startActivity(android.content.Intent, android.os.Bundle?);
+    method @Deprecated public void startActivityForResult(android.content.Intent, int);
+    method @Deprecated public void startActivityForResult(android.content.Intent, int, android.os.Bundle?);
+    method @Deprecated public void startIntentSenderForResult(android.content.IntentSender, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
     method public void startPostponedEnterTransition();
     method public void unregisterForContextMenu(android.view.View);
   }
@@ -178,9 +178,9 @@
     method public void onStateNotSaved();
     method public void setEnterSharedElementCallback(androidx.core.app.SharedElementCallback?);
     method public void setExitSharedElementCallback(androidx.core.app.SharedElementCallback?);
-    method public void startActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent!, int);
-    method public void startActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent!, int, android.os.Bundle?);
-    method @Deprecated public void startIntentSenderFromFragment(androidx.fragment.app.Fragment, android.content.IntentSender!, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
+    method public void startActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent, int);
+    method public void startActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent, int, android.os.Bundle?);
+    method @Deprecated public void startIntentSenderFromFragment(androidx.fragment.app.Fragment, android.content.IntentSender, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
     method public void supportFinishAfterTransition();
     method @Deprecated public void supportInvalidateOptionsMenu();
     method public void supportPostponeEnterTransition();
@@ -265,9 +265,9 @@
     method @Deprecated public void onRequestPermissionsFromFragment(androidx.fragment.app.Fragment, String![], int);
     method public boolean onShouldSaveFragmentState(androidx.fragment.app.Fragment);
     method public boolean onShouldShowRequestPermissionRationale(String);
-    method public void onStartActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent!, int);
-    method public void onStartActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent!, int, android.os.Bundle?);
-    method @Deprecated public void onStartIntentSenderFromFragment(androidx.fragment.app.Fragment, android.content.IntentSender!, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
+    method public void onStartActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent, int);
+    method public void onStartActivityFromFragment(androidx.fragment.app.Fragment, android.content.Intent, int, android.os.Bundle?);
+    method @Deprecated public void onStartIntentSenderFromFragment(androidx.fragment.app.Fragment, android.content.IntentSender, int, android.content.Intent?, int, int, int, android.os.Bundle?) throws android.content.IntentSender.SendIntentException;
     method public void onSupportInvalidateOptionsMenu();
   }
 
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 168db10..b751358 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -32,7 +32,7 @@
     api("androidx.activity:activity:1.5.1")
     api(projectOrArtifact(":lifecycle:lifecycle-runtime"))
     api("androidx.lifecycle:lifecycle-livedata-core:2.5.1")
-    api("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
     api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
     api("androidx.savedstate:savedstate:1.2.0")
     api("androidx.annotation:annotation-experimental:1.0.0")
@@ -57,8 +57,9 @@
     androidTestImplementation(project(":internal-testutils-runtime"), {
         exclude group: "androidx.fragment", module: "fragment"
     })
+    androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
 
-    testImplementation(project(":fragment:fragment"))
+    testImplementation(projectOrArtifact(":fragment:fragment"))
     testImplementation(libs.kotlinStdlib)
     testImplementation(libs.testExtJunit)
     testImplementation(libs.testCore)
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ControllerHostCallbacks.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ControllerHostCallbacks.kt
index 095f013..f4ba719b 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ControllerHostCallbacks.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ControllerHostCallbacks.kt
@@ -81,12 +81,10 @@
 
 class ControllerHostCallbacks(
     private val activity: FragmentActivity,
-    private val viewModelStore: ViewModelStore
+    private val vmStore: ViewModelStore
 ) : FragmentHostCallback<FragmentActivity>(activity), ViewModelStoreOwner {
 
-    override fun getViewModelStore(): ViewModelStore {
-        return viewModelStore
-    }
+    override val viewModelStore: ViewModelStore = vmStore
 
     override fun onDump(
         prefix: String,
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/FragmentArchLifecycleTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentArchLifecycleTest.kt
index 18af166..319226a 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentArchLifecycleTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentArchLifecycleTest.kt
@@ -294,9 +294,8 @@
 
     private val lifecycleRegistry = LifecycleRegistry(this)
 
-    override fun getLifecycle(): Lifecycle {
-        return lifecycleRegistry
-    }
+    override val lifecycle: Lifecycle
+        get() = lifecycleRegistry
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleOwnerTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleOwnerTest.kt
index c68e779..6a83cf8 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleOwnerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleOwnerTest.kt
@@ -183,7 +183,6 @@
     }
 
     public class FragmentWithFactoryOverride : StrictViewFragment() {
-        public override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory =
-            FakeViewModelProviderFactory()
+        public override val defaultViewModelProviderFactory = FakeViewModelProviderFactory()
     }
 }
\ No newline at end of file
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/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
index cbd53a2..31a1632 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -1431,7 +1431,7 @@
      * Call {@link Activity#startActivity(Intent)} from the fragment's
      * containing Activity.
      */
-    public void startActivity(@SuppressLint("UnknownNullness") Intent intent) {
+    public void startActivity(@NonNull Intent intent) {
         startActivity(intent, null);
     }
 
@@ -1439,7 +1439,7 @@
      * Call {@link Activity#startActivity(Intent, Bundle)} from the fragment's
      * containing Activity.
      */
-    public void startActivity(@SuppressLint("UnknownNullness") Intent intent,
+    public void startActivity(@NonNull Intent intent,
             @Nullable Bundle options) {
         if (mHost == null) {
             throw new IllegalStateException("Fragment " + this + " not attached to Activity");
@@ -1468,7 +1468,7 @@
      */
     @SuppressWarnings("deprecation")
     @Deprecated
-    public void startActivityForResult(@SuppressLint("UnknownNullness") Intent intent,
+    public void startActivityForResult(@NonNull Intent intent,
             int requestCode) {
         startActivityForResult(intent, requestCode, null);
     }
@@ -1496,7 +1496,7 @@
      */
     @SuppressWarnings("DeprecatedIsStillUsed")
     @Deprecated
-    public void startActivityForResult(@SuppressLint("UnknownNullness") Intent intent,
+    public void startActivityForResult(@NonNull Intent intent,
             int requestCode, @Nullable Bundle options) {
         if (mHost == null) {
             throw new IllegalStateException("Fragment " + this + " not attached to Activity");
@@ -1534,7 +1534,7 @@
      * {@link ActivityResultContract}.
      */
     @Deprecated
-    public void startIntentSenderForResult(@SuppressLint("UnknownNullness") IntentSender intent,
+    public void startIntentSenderForResult(@NonNull IntentSender intent,
             int requestCode, @Nullable Intent fillInIntent, int flagsMask, int flagsValues,
             int extraFlags, @Nullable Bundle options) throws IntentSender.SendIntentException {
         if (mHost == null) {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java
index b48c19c..c373049 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java
@@ -18,7 +18,6 @@
 
 import static androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult;
 
-import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
@@ -501,7 +500,7 @@
      *                    greater than 65535, an IllegalArgumentException would be thrown.
      */
     public void startActivityFromFragment(@NonNull Fragment fragment,
-            @SuppressLint("UnknownNullness") Intent intent, int requestCode) {
+            @NonNull Intent intent, int requestCode) {
         startActivityFromFragment(fragment, intent, requestCode, null);
     }
 
@@ -519,7 +518,7 @@
      */
     @SuppressWarnings("deprecation")
     public void startActivityFromFragment(@NonNull Fragment fragment,
-            @SuppressLint("UnknownNullness") Intent intent, int requestCode,
+            @NonNull Intent intent, int requestCode,
             @Nullable Bundle options) {
         // request code will be -1 if called from fragment.startActivity
         if (requestCode == -1) {
@@ -558,7 +557,7 @@
     @SuppressWarnings({"deprecation"})
     @Deprecated
     public void startIntentSenderFromFragment(@NonNull Fragment fragment,
-            @SuppressLint("UnknownNullness") IntentSender intent, int requestCode,
+            @NonNull IntentSender intent, int requestCode,
             @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags,
             @Nullable Bundle options) throws IntentSender.SendIntentException {
         if (requestCode == -1) {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentHostCallback.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentHostCallback.java
index 21079f0..b9ebe38 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentHostCallback.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentHostCallback.java
@@ -19,7 +19,6 @@
 import static androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions;
 import static androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult;
 
-import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
@@ -149,7 +148,7 @@
      * See {@link FragmentActivity#startActivityForResult(Intent, int)}.
      */
     public void onStartActivityFromFragment(@NonNull Fragment fragment,
-            @SuppressLint("UnknownNullness") Intent intent, int requestCode) {
+            @NonNull Intent intent, int requestCode) {
         onStartActivityFromFragment(fragment, intent, requestCode, null);
     }
 
@@ -158,7 +157,7 @@
      * See {@link FragmentActivity#startActivityForResult(Intent, int, Bundle)}.
      */
     public void onStartActivityFromFragment(
-            @NonNull Fragment fragment, @SuppressLint("UnknownNullness") Intent intent,
+            @NonNull Fragment fragment, @NonNull Intent intent,
             int requestCode, @Nullable Bundle options) {
         if (requestCode != -1) {
             throw new IllegalStateException(
@@ -179,7 +178,7 @@
      */
     @Deprecated
     public void onStartIntentSenderFromFragment(@NonNull Fragment fragment,
-            @SuppressLint("UnknownNullness") IntentSender intent, int requestCode,
+            @NonNull IntentSender intent, int requestCode,
             @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags,
             @Nullable Bundle options) throws IntentSender.SendIntentException {
         if (requestCode != -1) {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index 1e0f070..af6591c 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -2815,12 +2815,12 @@
     }
 
     void launchStartActivityForResult(@NonNull Fragment f,
-            @SuppressLint("UnknownNullness") Intent intent,
+            @NonNull Intent intent,
             int requestCode, @Nullable Bundle options) {
         if (mStartActivityForResult != null) {
             LaunchedFragmentInfo info = new LaunchedFragmentInfo(f.mWho, requestCode);
             mLaunchedFragments.addLast(info);
-            if (intent != null && options != null) {
+            if (options != null) {
                 intent.putExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE, options);
             }
             mStartActivityForResult.launch(intent);
@@ -2831,7 +2831,7 @@
 
     @SuppressWarnings("deprecation")
     void launchStartIntentSenderForResult(@NonNull Fragment f,
-            @SuppressLint("UnknownNullness") IntentSender intent,
+            @NonNull IntentSender intent,
             int requestCode, @Nullable Intent fillInIntent, int flagsMask, int flagsValues,
             int extraFlags, @Nullable Bundle options) throws IntentSender.SendIntentException {
         if (mStartIntentSenderForResult != null) {
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ImageAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ImageAppWidget.kt
index f407767..1d16188 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ImageAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ImageAppWidget.kt
@@ -24,21 +24,27 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
 import androidx.glance.Button
+import androidx.glance.ColorFilter
 import androidx.glance.GlanceModifier
 import androidx.glance.Image
 import androidx.glance.ImageProvider
+import androidx.glance.LocalContext
 import androidx.glance.appwidget.GlanceAppWidget
 import androidx.glance.appwidget.GlanceAppWidgetReceiver
 import androidx.glance.appwidget.SizeMode
 import androidx.glance.background
+import androidx.glance.color.ColorProvider
+import androidx.glance.layout.Alignment
 import androidx.glance.layout.Column
 import androidx.glance.layout.ContentScale
+import androidx.glance.layout.Row
 import androidx.glance.layout.Spacer
 import androidx.glance.layout.fillMaxSize
 import androidx.glance.layout.fillMaxWidth
 import androidx.glance.layout.padding
 import androidx.glance.layout.size
 import androidx.glance.session.GlanceSessionManager
+import androidx.glance.text.Text
 
 /**
  * Sample AppWidget that showcase the [ContentScale] options for [Image]
@@ -52,6 +58,8 @@
     override fun Content() {
         var type by remember { mutableStateOf(ContentScale.Fit) }
         Column(modifier = GlanceModifier.fillMaxSize().padding(8.dp)) {
+            Header()
+            Spacer(GlanceModifier.size(4.dp))
             Button(
                 text = "Content Scale: ${type.asString()}",
                 modifier = GlanceModifier.fillMaxWidth(),
@@ -73,6 +81,28 @@
         }
     }
 
+    @Composable
+    private fun Header() {
+        val context = LocalContext.current
+        Row(
+            horizontalAlignment = Alignment.CenterHorizontally,
+            verticalAlignment = Alignment.CenterVertically,
+            modifier = GlanceModifier.fillMaxWidth().background(Color.White)
+        ) {
+            Image(
+                provider = ImageProvider(R.drawable.ic_android),
+                contentDescription = null,
+                colorFilter = ColorFilter.tint(
+                    ColorProvider(day = Color.Green, night = Color.Blue)
+                ),
+            )
+            Text(
+                text = context.getString(R.string.image_widget_name),
+                modifier = GlanceModifier.padding(8.dp),
+            )
+        }
+    }
+
     private fun ContentScale.asString(): String =
         when (this) {
             ContentScale.Fit -> "Fit"
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/ic_android.xml b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/ic_android.xml
new file mode 100644
index 0000000..5508fb5
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/ic_android.xml
@@ -0,0 +1,5 @@
+<vector android:height="30dp"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="30dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M17.6,9.48l1.84,-3.18c0.16,-0.31 0.04,-0.69 -0.26,-0.85c-0.29,-0.15 -0.65,-0.06 -0.83,0.22l-1.88,3.24c-2.86,-1.21 -6.08,-1.21 -8.94,0L5.65,5.67c-0.19,-0.29 -0.58,-0.38 -0.87,-0.2C4.5,5.65 4.41,6.01 4.56,6.3L6.4,9.48C3.3,11.25 1.28,14.44 1,18h22C22.72,14.44 20.7,11.25 17.6,9.48zM7,15.25c-0.69,0 -1.25,-0.56 -1.25,-1.25c0,-0.69 0.56,-1.25 1.25,-1.25S8.25,13.31 8.25,14C8.25,14.69 7.69,15.25 7,15.25zM17,15.25c-0.69,0 -1.25,-0.56 -1.25,-1.25c0,-0.69 0.56,-1.25 1.25,-1.25s1.25,0.56 1.25,1.25C18.25,14.69 17.69,15.25 17,15.25z"/>
+</vector>
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/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
index 809209f..ac37082 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
@@ -228,6 +228,7 @@
         }
     }
 
+    @Ignore("b/266588723")
     @Test
     fun createTextWithFillMaxDimensions() {
         TestGlanceAppWidget.uiDefinition = {
@@ -346,6 +347,7 @@
         }
     }
 
+    @Ignore("b/265078768")
     @Test
     fun createRowWithTwoTexts() {
         TestGlanceAppWidget.uiDefinition = {
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetSession.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetSession.kt
index fa0e117..46cfa21 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetSession.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetSession.kt
@@ -31,6 +31,7 @@
 import androidx.compose.runtime.neverEqualPolicy
 import androidx.compose.runtime.produceState
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.unit.DpSize
 import androidx.datastore.preferences.core.emptyPreferences
 import androidx.glance.EmittableWithChildren
@@ -124,9 +125,9 @@
     override suspend fun processEmittableTree(
         context: Context,
         root: EmittableWithChildren
-    ) {
+    ): Boolean {
+        if (root.shouldIgnoreResult()) return false
         root as RemoteViewsRoot
-        if (root.shouldIgnoreResult()) return
         val layoutConfig = LayoutConfiguration.load(context, id.appWidgetId)
         val appWidgetManager = context.appWidgetManager
         try {
@@ -160,14 +161,18 @@
             layoutConfig.save()
             Tracing.endGlanceAppWidgetUpdate()
         }
+        return true
     }
 
     override suspend fun processEvent(context: Context, event: Any) {
         when (event) {
             is UpdateGlanceState -> {
                 if (DEBUG) Log.i(TAG, "Received UpdateGlanceState event for session($key)")
-                glanceState.value =
+                val newGlanceState =
                     configManager.getValue(context, PreferencesGlanceStateDefinition, key)
+                Snapshot.withMutableSnapshot {
+                    glanceState.value = newGlanceState
+                }
             }
             is UpdateAppWidgetOptions -> {
                 if (DEBUG) {
@@ -177,15 +182,15 @@
                             "for session($key)"
                     )
                 }
-                options.value = event.newOptions
+                Snapshot.withMutableSnapshot {
+                    options.value = event.newOptions
+                }
             }
             is RunLambda -> {
-                Log.i(TAG, "Received RunLambda(${event.key}) action for session($key)")
-                lambdas[event.key]?.map { it.block() }
-                    ?: Log.w(
-                        TAG,
-                        "Triggering Action(${event.key}) for session($key) failed"
-                    )
+                if (DEBUG) Log.i(TAG, "Received RunLambda(${event.key}) action for session($key)")
+                Snapshot.withMutableSnapshot {
+                    lambdas[event.key]?.forEach { it.block() }
+                } ?: Log.w(TAG, "Triggering Action(${event.key}) for session($key) failed")
             }
             else -> {
                 throw IllegalArgumentException(
@@ -207,7 +212,7 @@
         sendEvent(RunLambda(key))
     }
 
-    // Action types that this session supports.
+    // Event types that this session supports.
     @VisibleForTesting
     internal object UpdateGlanceState
     @VisibleForTesting
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/LayoutSelection.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/LayoutSelection.kt
index f87bb8e..933c70a 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/LayoutSelection.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/LayoutSelection.kt
@@ -17,6 +17,7 @@
 
 import android.content.Context
 import android.os.Build
+import android.util.Log
 import android.view.View
 import android.view.ViewGroup
 import android.widget.RemoteViews
@@ -370,10 +371,18 @@
     horizontalAlignment: Alignment.Horizontal?,
     verticalAlignment: Alignment.Vertical?,
 ): InsertedViewInfo {
+    if (numChildren > 10) {
+        Log.e(
+            GlanceAppWidgetTag,
+            "Truncated $type container from $numChildren to 10 elements",
+            IllegalArgumentException("$type container cannot have more than 10 elements")
+        )
+    }
+    val children = numChildren.coerceAtMost(10)
     val childLayout = selectLayout33(type, modifier)
         ?: generatedContainers[ContainerSelector(
             type,
-            numChildren,
+            children,
             horizontalAlignment,
             verticalAlignment
         )]?.layoutId
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
index de2c0e4..405ae563 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
@@ -434,9 +434,9 @@
 internal fun RemoteViews.setChildren(
     translationContext: TranslationContext,
     parentDef: InsertedViewInfo,
-    children: Iterable<Emittable>
+    children: List<Emittable>
 ) {
-    children.forEachIndexed { index, child ->
+    children.take(10).forEachIndexed { index, child ->
         translateChild(
             translationContext.forChild(parent = parentDef, pos = index),
             child,
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/ImageTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/ImageTranslator.kt
index c3ddf74..f012ed0 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/ImageTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/ImageTranslator.kt
@@ -22,22 +22,32 @@
 import android.widget.RemoteViews
 import androidx.annotation.DoNotInline
 import androidx.annotation.RequiresApi
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
 import androidx.core.widget.RemoteViewsCompat.setImageViewAdjustViewBounds
+import androidx.core.widget.RemoteViewsCompat.setImageViewColorFilter
+import androidx.core.widget.RemoteViewsCompat.setImageViewColorFilterResource
 import androidx.glance.AndroidResourceImageProvider
 import androidx.glance.BitmapImageProvider
-import androidx.glance.layout.ContentScale
+import androidx.glance.ColorFilterParams
 import androidx.glance.EmittableImage
 import androidx.glance.IconImageProvider
+import androidx.glance.TintColorFilterParams
 import androidx.glance.appwidget.GlanceAppWidgetTag
+import androidx.glance.appwidget.InsertedViewInfo
 import androidx.glance.appwidget.LayoutType
 import androidx.glance.appwidget.TranslationContext
 import androidx.glance.appwidget.UriImageProvider
 import androidx.glance.appwidget.applyModifiers
 import androidx.glance.appwidget.insertView
+import androidx.glance.color.DayNightColorProvider
 import androidx.glance.findModifier
+import androidx.glance.layout.ContentScale
 import androidx.glance.layout.HeightModifier
 import androidx.glance.layout.WidthModifier
+import androidx.glance.unit.ColorProvider
 import androidx.glance.unit.Dimension
+import androidx.glance.unit.ResourceColorProvider
 
 internal fun RemoteViews.translateEmittableImage(
     translationContext: TranslationContext,
@@ -64,6 +74,7 @@
         else ->
             throw IllegalArgumentException("An unsupported ImageProvider type was used.")
     }
+    element.colorFilterParams?.let { applyColorFilter(translationContext, this, it, viewDef) }
     applyModifiers(translationContext, this, element.modifier, viewDef)
 
     // If the content scale is Fit, the developer has expressed that they want the image to
@@ -76,6 +87,33 @@
     setImageViewAdjustViewBounds(viewDef.mainViewId, shouldAdjustViewBounds)
 }
 
+private fun applyColorFilter(
+    translationContext: TranslationContext,
+    rv: RemoteViews,
+    colorFilterParams: ColorFilterParams,
+    viewDef: InsertedViewInfo
+) {
+    when (colorFilterParams) {
+        is TintColorFilterParams -> {
+            val colorProvider = colorFilterParams.colorProvider
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                ImageTranslatorApi31Impl.applyTintColorFilter(
+                    translationContext,
+                    rv,
+                    colorProvider,
+                    viewDef.mainViewId
+                )
+            } else {
+                rv.setImageViewColorFilter(
+                    viewDef.mainViewId, colorProvider.getColor(translationContext.context).toArgb()
+                )
+            }
+        }
+
+        else -> throw IllegalArgumentException("An unsupported ColorFilter was used.")
+    }
+}
+
 private fun setImageViewIcon(rv: RemoteViews, viewId: Int, provider: IconImageProvider) {
     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
         throw IllegalStateException("Cannot use Icon ImageProvider before API 23.")
@@ -90,3 +128,37 @@
         rv.setImageViewIcon(viewId, icon)
     }
 }
+
+@RequiresApi(Build.VERSION_CODES.S)
+private object ImageTranslatorApi31Impl {
+    @DoNotInline
+    fun applyTintColorFilter(
+        translationContext: TranslationContext,
+        rv: RemoteViews,
+        colorProvider: ColorProvider,
+        viewId: Int
+    ) {
+        when (colorProvider) {
+            is DayNightColorProvider -> rv.setImageViewColorFilter(
+                viewId,
+                colorProvider.day,
+                colorProvider.night
+            )
+
+            is ResourceColorProvider -> rv.setImageViewColorFilterResource(
+                viewId,
+                colorProvider.resId
+            )
+
+            else -> rv.setImageViewColorFilter(
+                viewId,
+                colorProvider.getColor(translationContext.context).toArgb()
+            )
+        }
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.S)
+internal fun RemoteViews.setImageViewColorFilter(viewId: Int, notNight: Color, night: Color) {
+    setImageViewColorFilter(viewId = viewId, notNight = notNight.toArgb(), night = night.toArgb())
+}
diff --git a/glance/glance-appwidget/src/androidMain/res/drawable-v24/glance_switch_thumb_off.xml b/glance/glance-appwidget/src/androidMain/res/drawable-v24/glance_switch_thumb_off.xml
new file mode 100644
index 0000000..06c358f
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/res/drawable-v24/glance_switch_thumb_off.xml
@@ -0,0 +1,25 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="40dp"
+    android:height="26dp"
+    android:viewportWidth="40"
+    android:viewportHeight="26"
+>
+  <group android:name="thumb" android:translateX="0">
+    <path
+        android:pathData="M13,22.5C18.7989,22.5 23.5,17.7989 23.5,12 23.5,6.201 18.7989,1.5 13,1.5 7.201,1.5 2.5,6.201 2.5,12 2.5,17.7989 7.201,22.5 13,22.5Z"
+        android:strokeWidth="1.05"
+        android:fillColor="@color/glance_switch_off_ambient_shadow"
+        android:fillType="evenOdd"/>
+    <path
+        android:pathData="m12.9681,24.2511c5.7734,0 10.4537,-5.084 10.4537,-11.3552 0,-6.2713 -4.6803,-11.3552 -10.4537,-11.3552 -5.7734,0 -10.4537,5.0839 -10.4537,11.3552 0,6.2713 4.6803,11.3552 10.4537,11.3552z"
+        android:strokeWidth="1.08951"
+        android:fillColor="@color/glance_switch_off_key_shadow"
+        android:fillType="evenOdd"/>
+    <path
+        android:name="thumb_icon"
+        android:pathData="M13,22C18.5228,22 23,17.5228 23,12 23,6.4771 18.5228,2 13,2 7.4771,2 3,6.4771 3,12c0,5.5228 4.4771,10 10,10z"
+        android:strokeWidth="1"
+        android:fillColor="#fff"
+        android:fillType="evenOdd"/>
+  </group>
+</vector>
diff --git a/glance/glance-appwidget/src/androidMain/res/drawable-v24/glance_switch_thumb_on.xml b/glance/glance-appwidget/src/androidMain/res/drawable-v24/glance_switch_thumb_on.xml
new file mode 100644
index 0000000..0e0a21b
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/res/drawable-v24/glance_switch_thumb_on.xml
@@ -0,0 +1,26 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="40dp"
+    android:height="26dp"
+    android:viewportWidth="40"
+    android:viewportHeight="26"
+    >
+    <group
+        android:name="thumb"
+        android:translateX="3">
+        <path
+            android:fillColor="@color/glance_switch_on_key_shadow"
+            android:fillType="evenOdd"
+            android:pathData="m23.9373,24.5329c5.7734,0 10.4537,-5.084 10.4537,-11.3552 0,-6.2713 -4.6803,-11.3552 -10.4537,-11.3552 -5.7734,0 -10.4537,5.0839 -10.4537,11.3552 0,6.2713 4.6803,11.3552 10.4537,11.3552z"
+            android:strokeWidth="1.08951" />
+        <path
+            android:fillColor="@color/glance_switch_on_ambient_shadow"
+            android:fillType="evenOdd"
+            android:pathData="M24,22.5C29.7989,22.5 34.5,17.7989 34.5,12 34.5,6.201 29.7989,1.5 24,1.5 18.201,1.5 13.5,6.201 13.5,12c0,5.7989 4.701,10.5 10.5,10.5z"
+            android:strokeWidth="1.05" />
+        <path
+            android:name="thumb_icon"
+            android:fillColor="#fff"
+            android:fillType="evenOdd"
+            android:pathData="M24,22C29.5228,22 34,17.5228 34,12C34,6.4771 29.5228,2 24,2C18.4772,2 14,6.4771 14,12C14,17.5228 18.4772,22 24,22Z" />
+    </group>
+</vector>
diff --git a/glance/glance-appwidget/src/androidMain/res/drawable/glance_switch_thumb_off.xml b/glance/glance-appwidget/src/androidMain/res/drawable/glance_switch_thumb_off.xml
index 06c358f..4400089 100644
--- a/glance/glance-appwidget/src/androidMain/res/drawable/glance_switch_thumb_off.xml
+++ b/glance/glance-appwidget/src/androidMain/res/drawable/glance_switch_thumb_off.xml
@@ -6,20 +6,9 @@
 >
   <group android:name="thumb" android:translateX="0">
     <path
-        android:pathData="M13,22.5C18.7989,22.5 23.5,17.7989 23.5,12 23.5,6.201 18.7989,1.5 13,1.5 7.201,1.5 2.5,6.201 2.5,12 2.5,17.7989 7.201,22.5 13,22.5Z"
-        android:strokeWidth="1.05"
-        android:fillColor="@color/glance_switch_off_ambient_shadow"
-        android:fillType="evenOdd"/>
-    <path
-        android:pathData="m12.9681,24.2511c5.7734,0 10.4537,-5.084 10.4537,-11.3552 0,-6.2713 -4.6803,-11.3552 -10.4537,-11.3552 -5.7734,0 -10.4537,5.0839 -10.4537,11.3552 0,6.2713 4.6803,11.3552 10.4537,11.3552z"
-        android:strokeWidth="1.08951"
-        android:fillColor="@color/glance_switch_off_key_shadow"
-        android:fillType="evenOdd"/>
-    <path
         android:name="thumb_icon"
         android:pathData="M13,22C18.5228,22 23,17.5228 23,12 23,6.4771 18.5228,2 13,2 7.4771,2 3,6.4771 3,12c0,5.5228 4.4771,10 10,10z"
         android:strokeWidth="1"
-        android:fillColor="#fff"
-        android:fillType="evenOdd"/>
+        android:fillColor="#fff"/>
   </group>
 </vector>
diff --git a/glance/glance-appwidget/src/androidMain/res/drawable/glance_switch_thumb_on.xml b/glance/glance-appwidget/src/androidMain/res/drawable/glance_switch_thumb_on.xml
index 0e0a21b..dd64c38 100644
--- a/glance/glance-appwidget/src/androidMain/res/drawable/glance_switch_thumb_on.xml
+++ b/glance/glance-appwidget/src/androidMain/res/drawable/glance_switch_thumb_on.xml
@@ -8,19 +8,8 @@
         android:name="thumb"
         android:translateX="3">
         <path
-            android:fillColor="@color/glance_switch_on_key_shadow"
-            android:fillType="evenOdd"
-            android:pathData="m23.9373,24.5329c5.7734,0 10.4537,-5.084 10.4537,-11.3552 0,-6.2713 -4.6803,-11.3552 -10.4537,-11.3552 -5.7734,0 -10.4537,5.0839 -10.4537,11.3552 0,6.2713 4.6803,11.3552 10.4537,11.3552z"
-            android:strokeWidth="1.08951" />
-        <path
-            android:fillColor="@color/glance_switch_on_ambient_shadow"
-            android:fillType="evenOdd"
-            android:pathData="M24,22.5C29.7989,22.5 34.5,17.7989 34.5,12 34.5,6.201 29.7989,1.5 24,1.5 18.201,1.5 13.5,6.201 13.5,12c0,5.7989 4.701,10.5 10.5,10.5z"
-            android:strokeWidth="1.05" />
-        <path
             android:name="thumb_icon"
             android:fillColor="#fff"
-            android:fillType="evenOdd"
             android:pathData="M24,22C29.5228,22 34,17.5228 34,12C34,6.4771 29.5228,2 24,2C18.4772,2 14,6.4771 14,12C14,17.5228 18.4772,22 24,22Z" />
     </group>
 </vector>
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt
index dd6e6fc..5104c54 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt
@@ -46,6 +46,7 @@
 import org.junit.Assert.assertThrows
 import org.junit.Before
 import org.junit.Test
+import org.junit.Ignore
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
 import org.robolectric.Shadows
@@ -81,6 +82,7 @@
         assertThat(widget.provideGlanceCalled.get()).isTrue()
     }
 
+    @Ignore("b/266518169")
     @Test
     fun provideGlanceEmitsIgnoreResultForNullContent() = runTest {
         // The session starts out with null content, so we can check that here.
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
index 66cce3c..a8cec419 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
@@ -1000,6 +1000,31 @@
         }
     }
 
+    @Test
+    fun containersHaveAtMostTenChildren() = runTest {
+        val rv = context.runAndTranslate {
+            Box {
+                Box {
+                    repeat(15) { Text("") }
+                }
+                Column {
+                    repeat(15) { Text("") }
+                }
+                Row {
+                    repeat(15) { Text("") }
+                }
+            }
+        }
+
+        val children = assertIs<FrameLayout>(context.applyRemoteViews(rv)).nonGoneChildren.toList()
+        val box = assertIs<FrameLayout>(children[0])
+        assertThat(box.nonGoneChildCount).isEqualTo(10)
+        val column = assertIs<LinearLayout>(children[1])
+        assertThat(column.nonGoneChildCount).isEqualTo(10)
+        val row = assertIs<LinearLayout>(children[2])
+        assertThat(row.nonGoneChildCount).isEqualTo(10)
+    }
+
     private fun expectGlanceLog(type: Int, message: String) {
         ShadowLog.getLogsForTag(GlanceAppWidgetTag).forEach { logItem ->
             if (logItem.type == type && logItem.msg == message)
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxBackportTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxBackportTranslatorTest.kt
new file mode 100644
index 0000000..1923a54
--- /dev/null
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxBackportTranslatorTest.kt
@@ -0,0 +1,237 @@
+/*
+ * 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.glance.appwidget.translators
+
+import android.content.Context
+import android.content.res.Configuration
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.compose.ui.graphics.Color
+import androidx.glance.GlanceModifier
+import androidx.glance.appwidget.CheckBox
+import androidx.glance.appwidget.checkBoxColors
+import androidx.glance.appwidget.ImageViewSubject.Companion.assertThat
+import androidx.glance.appwidget.action.ActionCallback
+import androidx.glance.appwidget.action.actionRunCallback
+import androidx.glance.appwidget.applyRemoteViews
+import androidx.glance.appwidget.configurationContext
+import androidx.glance.appwidget.findViewByType
+import androidx.glance.appwidget.runAndTranslate
+import androidx.glance.color.ColorProvider
+import androidx.glance.semantics.contentDescription
+import androidx.glance.semantics.semantics
+import androidx.glance.unit.FixedColorProvider
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertIs
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@Config(minSdk = 23, maxSdk = 30)
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(RobolectricTestRunner::class)
+class CheckBoxBackportTranslatorTest {
+
+    private lateinit var fakeCoroutineScope: TestScope
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private val lightContext = configurationContext { uiMode = Configuration.UI_MODE_NIGHT_NO }
+    private val darkContext = configurationContext { uiMode = Configuration.UI_MODE_NIGHT_YES }
+
+    @Before
+    fun setUp() {
+        fakeCoroutineScope = TestScope()
+    }
+
+    @Test
+    fun canTranslateCheckBox_resolved_unchecked() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            CheckBox(
+                checked = false,
+                onCheckedChange = null,
+                text = "Check",
+                colors = checkBoxColors(checkedColor = Color.Red, uncheckedColor = Color.Blue)
+            )
+        }
+
+        val checkBoxRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+        val icon = checkBoxRoot.findViewByType<ImageView>()
+        assertThat(icon).hasColorFilter(Color.Blue)
+    }
+
+    @Test
+    fun canTranslateCheckBox_resolved_checked() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            CheckBox(
+                checked = true,
+                onCheckedChange = null,
+                text = "Check",
+                colors = checkBoxColors(checkedColor = Color.Red, uncheckedColor = Color.Blue)
+            )
+        }
+
+        val checkBoxRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+        val icon = checkBoxRoot.findViewByType<ImageView>()
+        assertThat(icon).hasColorFilter(Color.Red)
+    }
+
+    @Test
+    fun canTranslateCheckBox_dayNight_unchecked_day() = fakeCoroutineScope.runTest {
+        val rv = lightContext.runAndTranslate {
+            CheckBox(
+                checked = false,
+                onCheckedChange = null,
+                text = "Check",
+                colors = checkBoxColors(
+                    checkedColor = ColorProvider(day = Color.Red, night = Color.Blue),
+                    uncheckedColor = ColorProvider(day = Color.Yellow, night = Color.Green)
+                )
+            )
+        }
+
+        val checkBoxRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
+        val icon = checkBoxRoot.findViewByType<ImageView>()
+        assertThat(icon).hasColorFilter(Color.Yellow)
+    }
+
+    @Test
+    fun canTranslateCheckBox_dayNight_unchecked_night() = fakeCoroutineScope.runTest {
+        val rv = darkContext.runAndTranslate {
+            CheckBox(
+                checked = false,
+                onCheckedChange = null,
+                text = "Check",
+                colors = checkBoxColors(
+                    checkedColor = ColorProvider(day = Color.Red, night = Color.Blue),
+                    uncheckedColor = ColorProvider(day = Color.Yellow, night = Color.Green)
+                )
+            )
+        }
+
+        val checkBoxRoot = assertIs<ViewGroup>(darkContext.applyRemoteViews(rv))
+        val icon = checkBoxRoot.findViewByType<ImageView>()
+        assertThat(icon).hasColorFilter(Color.Green)
+    }
+
+    @Test
+    fun canTranslateCheckBox_dayNight_checked_day() = fakeCoroutineScope.runTest {
+        val rv = lightContext.runAndTranslate {
+            CheckBox(
+                checked = true,
+                onCheckedChange = null,
+                text = "Check",
+                colors = checkBoxColors(
+                    checkedColor = ColorProvider(day = Color.Red, night = Color.Blue),
+                    uncheckedColor = ColorProvider(day = Color.Yellow, night = Color.Green)
+                )
+            )
+        }
+
+        val checkBoxRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
+        val icon = checkBoxRoot.findViewByType<ImageView>()
+        assertThat(icon).hasColorFilter(Color.Red)
+    }
+
+    @Test
+    fun canTranslateCheckBox_dayNight_checked_night() = fakeCoroutineScope.runTest {
+        val rv = darkContext.runAndTranslate {
+            CheckBox(
+                checked = true,
+                onCheckedChange = null,
+                text = "Check",
+                colors = checkBoxColors(
+                    checkedColor = ColorProvider(day = Color.Red, night = Color.Blue),
+                    uncheckedColor = ColorProvider(day = Color.Yellow, night = Color.Green)
+                )
+            )
+        }
+
+        val checkBoxRoot = assertIs<ViewGroup>(darkContext.applyRemoteViews(rv))
+        val icon = checkBoxRoot.findViewByType<ImageView>()
+        assertThat(icon).hasColorFilter(Color.Blue)
+    }
+
+    @Test
+    fun canTranslateCheckBox_resource_checked() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            CheckBox(
+                checked = true,
+                onCheckedChange = null,
+                text = "Check",
+                colors = checkBoxColors(
+                    checkedColor = FixedColorProvider(Color.Red),
+                    uncheckedColor = FixedColorProvider(Color.Blue)
+                )
+            )
+        }
+
+        val checkBoxRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+        val icon = checkBoxRoot.findViewByType<ImageView>()
+        assertThat(icon).hasColorFilter(Color.Red)
+    }
+
+    @Test
+    fun canTranslateCheckBox_onCheckedChange_null() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            CheckBox(
+                checked = true,
+                onCheckedChange = null,
+                text = "CheckBox",
+            )
+        }
+
+        val checkboxRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+        assertThat(checkboxRoot.hasOnClickListeners()).isFalse()
+    }
+
+    @Test
+    fun canTranslateCheckBox_onCheckedChange_withAction() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            CheckBox(
+                checked = true,
+                onCheckedChange = actionRunCallback<ActionCallback>(),
+                text = "CheckBox",
+            )
+        }
+
+        val checkboxRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+        assertThat(checkboxRoot.hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun canTranslateCheckBoxWithSemanticsModifier_contentDescription() =
+        fakeCoroutineScope.runTest {
+            val rv = context.runAndTranslate {
+                CheckBox(
+                    checked = true,
+                    onCheckedChange = actionRunCallback<ActionCallback>(),
+                    text = "CheckBox",
+                    modifier = GlanceModifier.semantics {
+                        contentDescription = "Custom checkbox description"
+                    },
+                )
+            }
+
+            val checkboxRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+            assertThat(checkboxRoot.contentDescription).isEqualTo("Custom checkbox description")
+        }
+}
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslatorTest.kt
deleted file mode 100644
index e84a322..0000000
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslatorTest.kt
+++ /dev/null
@@ -1,245 +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.glance.appwidget.translators
-
-import android.content.Context
-import android.content.res.Configuration
-import android.view.ViewGroup
-import android.widget.ImageView
-import androidx.compose.ui.graphics.Color
-import androidx.glance.GlanceModifier
-import androidx.glance.appwidget.CheckBox
-import androidx.glance.appwidget.checkBoxColors
-import androidx.glance.appwidget.ImageViewSubject.Companion.assertThat
-import androidx.glance.appwidget.action.ActionCallback
-import androidx.glance.appwidget.action.actionRunCallback
-import androidx.glance.appwidget.applyRemoteViews
-import androidx.glance.appwidget.configurationContext
-import androidx.glance.appwidget.findViewByType
-import androidx.glance.appwidget.runAndTranslate
-import androidx.glance.color.ColorProvider
-import androidx.glance.semantics.contentDescription
-import androidx.glance.semantics.semantics
-import androidx.glance.unit.FixedColorProvider
-import androidx.test.core.app.ApplicationProvider
-import com.google.common.truth.Truth.assertThat
-import kotlin.test.assertIs
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.robolectric.RobolectricTestRunner
-import org.robolectric.annotation.Config
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@RunWith(RobolectricTestRunner::class)
-class CheckBoxTranslatorTest {
-
-    private lateinit var fakeCoroutineScope: TestScope
-    private val context = ApplicationProvider.getApplicationContext<Context>()
-    private val lightContext = configurationContext { uiMode = Configuration.UI_MODE_NIGHT_NO }
-    private val darkContext = configurationContext { uiMode = Configuration.UI_MODE_NIGHT_YES }
-
-    @Before
-    fun setUp() {
-        fakeCoroutineScope = TestScope()
-    }
-
-    @Config(sdk = [21, 23])
-    @Test
-    fun canTranslateCheckBox_resolved_unchecked() = fakeCoroutineScope.runTest {
-        val rv = context.runAndTranslate {
-            CheckBox(
-                checked = false,
-                onCheckedChange = null,
-                text = "Check",
-                colors = checkBoxColors(checkedColor = Color.Red, uncheckedColor = Color.Blue)
-            )
-        }
-
-        val checkBoxRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
-        val icon = checkBoxRoot.findViewByType<ImageView>()
-        assertThat(icon).hasColorFilter(Color.Blue)
-    }
-
-    @Config(sdk = [21, 23])
-    @Test
-    fun canTranslateCheckBox_resolved_checked() = fakeCoroutineScope.runTest {
-        val rv = context.runAndTranslate {
-            CheckBox(
-                checked = true,
-                onCheckedChange = null,
-                text = "Check",
-                colors = checkBoxColors(checkedColor = Color.Red, uncheckedColor = Color.Blue)
-            )
-        }
-
-        val checkBoxRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
-        val icon = checkBoxRoot.findViewByType<ImageView>()
-        assertThat(icon).hasColorFilter(Color.Red)
-    }
-
-    @Config(sdk = [29])
-    @Test
-    fun canTranslateCheckBox_dayNight_unchecked_day() = fakeCoroutineScope.runTest {
-        val rv = lightContext.runAndTranslate {
-            CheckBox(
-                checked = false,
-                onCheckedChange = null,
-                text = "Check",
-                colors = checkBoxColors(
-                    checkedColor = ColorProvider(day = Color.Red, night = Color.Blue),
-                    uncheckedColor = ColorProvider(day = Color.Yellow, night = Color.Green)
-                )
-            )
-        }
-
-        val checkBoxRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
-        val icon = checkBoxRoot.findViewByType<ImageView>()
-        assertThat(icon).hasColorFilter(Color.Yellow)
-    }
-
-    @Config(sdk = [29])
-    @Test
-    fun canTranslateCheckBox_dayNight_unchecked_night() = fakeCoroutineScope.runTest {
-        val rv = darkContext.runAndTranslate {
-            CheckBox(
-                checked = false,
-                onCheckedChange = null,
-                text = "Check",
-                colors = checkBoxColors(
-                    checkedColor = ColorProvider(day = Color.Red, night = Color.Blue),
-                    uncheckedColor = ColorProvider(day = Color.Yellow, night = Color.Green)
-                )
-            )
-        }
-
-        val checkBoxRoot = assertIs<ViewGroup>(darkContext.applyRemoteViews(rv))
-        val icon = checkBoxRoot.findViewByType<ImageView>()
-        assertThat(icon).hasColorFilter(Color.Green)
-    }
-
-    @Config(sdk = [29])
-    @Test
-    fun canTranslateCheckBox_dayNight_checked_day() = fakeCoroutineScope.runTest {
-        val rv = lightContext.runAndTranslate {
-            CheckBox(
-                checked = true,
-                onCheckedChange = null,
-                text = "Check",
-                colors = checkBoxColors(
-                    checkedColor = ColorProvider(day = Color.Red, night = Color.Blue),
-                    uncheckedColor = ColorProvider(day = Color.Yellow, night = Color.Green)
-                )
-            )
-        }
-
-        val checkBoxRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
-        val icon = checkBoxRoot.findViewByType<ImageView>()
-        assertThat(icon).hasColorFilter(Color.Red)
-    }
-
-    @Config(sdk = [29])
-    @Test
-    fun canTranslateCheckBox_dayNight_checked_night() = fakeCoroutineScope.runTest {
-        val rv = darkContext.runAndTranslate {
-            CheckBox(
-                checked = true,
-                onCheckedChange = null,
-                text = "Check",
-                colors = checkBoxColors(
-                    checkedColor = ColorProvider(day = Color.Red, night = Color.Blue),
-                    uncheckedColor = ColorProvider(day = Color.Yellow, night = Color.Green)
-                )
-            )
-        }
-
-        val checkBoxRoot = assertIs<ViewGroup>(darkContext.applyRemoteViews(rv))
-        val icon = checkBoxRoot.findViewByType<ImageView>()
-        assertThat(icon).hasColorFilter(Color.Blue)
-    }
-
-    @Config(sdk = [21, 23])
-    @Test
-    fun canTranslateCheckBox_resource_checked() = fakeCoroutineScope.runTest {
-        val rv = context.runAndTranslate {
-            CheckBox(
-                checked = true,
-                onCheckedChange = null,
-                text = "Check",
-                colors = checkBoxColors(
-                    checkedColor = FixedColorProvider(Color.Red),
-                    uncheckedColor = FixedColorProvider(Color.Blue)
-                )
-            )
-        }
-
-        val checkBoxRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
-        val icon = checkBoxRoot.findViewByType<ImageView>()
-        assertThat(icon).hasColorFilter(Color.Red)
-    }
-
-    @Config(sdk = [29])
-    @Test
-    fun canTranslateCheckBox_onCheckedChange_null() = fakeCoroutineScope.runTest {
-        val rv = context.runAndTranslate {
-            CheckBox(
-                checked = true,
-                onCheckedChange = null,
-                text = "CheckBox",
-            )
-        }
-
-        val checkboxRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
-        assertThat(checkboxRoot.hasOnClickListeners()).isFalse()
-    }
-
-    @Config(sdk = [29])
-    @Test
-    fun canTranslateCheckBox_onCheckedChange_withAction() = fakeCoroutineScope.runTest {
-        val rv = context.runAndTranslate {
-            CheckBox(
-                checked = true,
-                onCheckedChange = actionRunCallback<ActionCallback>(),
-                text = "CheckBox",
-            )
-        }
-
-        val checkboxRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
-        assertThat(checkboxRoot.hasOnClickListeners()).isTrue()
-    }
-
-    @Test
-    fun canTranslateCheckBoxWithSemanticsModifier_contentDescription() =
-        fakeCoroutineScope.runTest {
-            val rv = context.runAndTranslate {
-                CheckBox(
-                    checked = true,
-                    onCheckedChange = actionRunCallback<ActionCallback>(),
-                    text = "CheckBox",
-                    modifier = GlanceModifier.semantics {
-                        contentDescription = "Custom checkbox description"
-                    },
-                )
-            }
-
-            val checkboxRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
-            assertThat(checkboxRoot.contentDescription).isEqualTo("Custom checkbox description")
-        }
-}
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/ImageTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/ImageTranslatorTest.kt
index 52bdc36..f459ca9 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/ImageTranslatorTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/ImageTranslatorTest.kt
@@ -24,17 +24,22 @@
 import android.graphics.drawable.Icon
 import android.net.Uri
 import android.widget.ImageView
+import androidx.compose.ui.graphics.Color
 import androidx.core.graphics.drawable.toBitmap
+import androidx.glance.ColorFilter
 import androidx.glance.GlanceModifier
-import androidx.glance.appwidget.applyRemoteViews
+import androidx.glance.Image
+import androidx.glance.ImageProvider
 import androidx.glance.appwidget.ImageProvider
+import androidx.glance.appwidget.ImageViewSubject.Companion.assertThat
+import androidx.glance.appwidget.applyRemoteViews
 import androidx.glance.appwidget.runAndTranslate
 import androidx.glance.appwidget.test.R
 import androidx.glance.layout.ContentScale
-import androidx.glance.Image
-import androidx.glance.ImageProvider
 import androidx.glance.semantics.contentDescription
 import androidx.glance.semantics.semantics
+import androidx.glance.unit.ColorProvider
+import androidx.glance.unit.ResourceColorProvider
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
@@ -220,4 +225,52 @@
             val imageView = assertIs<ImageView>(context.applyRemoteViews(rv))
             assertThat(imageView.getContentDescription()).isNull()
         }
+
+    @Test
+    @Config(sdk = [23, 31])
+    fun translateImage_colorFilter() =
+        fakeCoroutineScope.runTest {
+            val rv = context.runAndTranslate {
+                Image(
+                    provider = ImageProvider(R.drawable.oval),
+                    contentDescription = null,
+                    colorFilter = ColorFilter.tint(ColorProvider(Color.Gray))
+                )
+            }
+
+            val imageView = assertIs<ImageView>(context.applyRemoteViews(rv))
+            assertThat(imageView).hasColorFilter(Color.Gray)
+        }
+
+    @Test
+    @Config(sdk = [23, 31])
+    fun translateImage_colorFilterWithResource() =
+        fakeCoroutineScope.runTest {
+            val colorProvider = ColorProvider(R.color.my_color) as ResourceColorProvider
+            val rv = context.runAndTranslate {
+                Image(
+                    provider = ImageProvider(R.drawable.oval),
+                    contentDescription = null,
+                    colorFilter = ColorFilter.tint(colorProvider)
+                )
+            }
+
+            val imageView = assertIs<ImageView>(context.applyRemoteViews(rv))
+            assertThat(imageView).hasColorFilter(colorProvider.getColor(context))
+        }
+
+    @Test
+    @Config(sdk = [23, 31])
+    fun translateImage_noColorFilter() =
+        fakeCoroutineScope.runTest {
+            val rv = context.runAndTranslate {
+                Image(
+                    provider = ImageProvider(R.drawable.oval),
+                    contentDescription = null,
+                )
+            }
+
+            val imageView = assertIs<ImageView>(context.applyRemoteViews(rv))
+            assertThat(imageView.colorFilter).isNull()
+        }
 }
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonBackportTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonBackportTranslatorTest.kt
new file mode 100644
index 0000000..a0a6683
--- /dev/null
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonBackportTranslatorTest.kt
@@ -0,0 +1,295 @@
+/*
+ * 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.glance.appwidget.translators
+
+import android.content.Context
+import android.content.res.Configuration
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.compose.ui.graphics.Color
+import androidx.glance.GlanceModifier
+import androidx.glance.appwidget.ImageViewSubject.Companion.assertThat
+import androidx.glance.appwidget.RadioButton
+import androidx.glance.appwidget.radioButtonColors
+import androidx.glance.appwidget.action.ActionCallback
+import androidx.glance.appwidget.action.actionRunCallback
+import androidx.glance.appwidget.applyRemoteViews
+import androidx.glance.appwidget.configurationContext
+import androidx.glance.appwidget.findView
+import androidx.glance.appwidget.findViewByType
+import androidx.glance.appwidget.runAndTranslate
+import androidx.glance.color.ColorProvider
+import androidx.glance.semantics.contentDescription
+import androidx.glance.semantics.semantics
+import androidx.glance.unit.ColorProvider
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.annotation.Config
+import kotlin.test.assertIs
+
+@Config(minSdk = 23, maxSdk = 30)
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(RobolectricTestRunner::class)
+class RadioButtonBackportTranslatorTest {
+
+    private lateinit var fakeCoroutineScope: TestScope
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private val lightContext = configurationContext { uiMode = Configuration.UI_MODE_NIGHT_NO }
+    private val darkContext = configurationContext { uiMode = Configuration.UI_MODE_NIGHT_YES }
+
+    @Before
+    fun setUp() {
+        fakeCoroutineScope = TestScope()
+    }
+
+    @Test
+    fun canTranslateRadioButton_fixed_unchecked() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            RadioButton(
+                checked = false,
+                onClick = null,
+                text = "RadioButton",
+                colors = radioButtonColors(
+                    checkedColor = ColorProvider(Color.Blue),
+                    uncheckedColor = ColorProvider(Color.Red),
+                )
+            )
+        }
+
+        val radioButtonRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+        assertThat(radioButtonRoot.radioImageView).hasColorFilter(Color.Red)
+    }
+
+    @Test
+    fun canTranslateRadioButton_fixed_checked() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            RadioButton(
+                checked = true,
+                onClick = null,
+                text = "RadioButton",
+                colors = radioButtonColors(
+                    checkedColor = ColorProvider(Color.Blue),
+                    uncheckedColor = ColorProvider(Color.Red),
+                )
+            )
+        }
+
+        val radioButtonRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+        assertThat(radioButtonRoot.radioImageView).hasColorFilter(Color.Blue)
+    }
+
+    @Test
+    fun canTranslateRadioButton_dayNight_unchecked_day() = fakeCoroutineScope.runTest {
+        val rv = lightContext.runAndTranslate {
+            RadioButton(
+                checked = false,
+                onClick = null,
+                text = "RadioButton",
+                colors = radioButtonColors(
+                    checkedColor = ColorProvider(day = Color.Blue, night = Color.Red),
+                    uncheckedColor = ColorProvider(day = Color.Green, night = Color.Yellow),
+                )
+            )
+        }
+
+        val radioButtonRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
+        assertThat(radioButtonRoot.radioImageView).hasColorFilter(Color.Green)
+    }
+
+    @Test
+    fun canTranslateRadioButton_dayNight_unchecked_night() = fakeCoroutineScope.runTest {
+        val rv = darkContext.runAndTranslate {
+            RadioButton(
+                checked = false,
+                onClick = null,
+                text = "RadioButton",
+                colors = radioButtonColors(
+                    checkedColor = ColorProvider(day = Color.Blue, night = Color.Red),
+                    uncheckedColor = ColorProvider(day = Color.Green, night = Color.Yellow),
+                )
+            )
+        }
+
+        val radioButtonRoot = assertIs<ViewGroup>(darkContext.applyRemoteViews(rv))
+        assertThat(radioButtonRoot.radioImageView).hasColorFilter(Color.Yellow)
+    }
+
+    @Test
+    fun canTranslateRadioButton_dayNight_checked_day() = fakeCoroutineScope.runTest {
+        val rv = lightContext.runAndTranslate {
+            RadioButton(
+                checked = true,
+                onClick = null,
+                text = "RadioButton",
+                colors = radioButtonColors(
+                    checkedColor = ColorProvider(day = Color.Blue, night = Color.Red),
+                    uncheckedColor = ColorProvider(day = Color.Green, night = Color.Yellow),
+                )
+            )
+        }
+
+        val radioButtonRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
+        assertThat(radioButtonRoot.radioImageView).hasColorFilter(Color.Blue)
+    }
+
+    @Test
+    fun canTranslateRadioButton_dayNight_checked_night() = fakeCoroutineScope.runTest {
+        val rv = darkContext.runAndTranslate {
+            RadioButton(
+                checked = true,
+                onClick = null,
+                text = "RadioButton",
+                colors = radioButtonColors(
+                    checkedColor = ColorProvider(day = Color.Blue, night = Color.Red),
+                    uncheckedColor = ColorProvider(day = Color.Green, night = Color.Yellow),
+                )
+            )
+        }
+
+        val radioButtonRoot = assertIs<ViewGroup>(darkContext.applyRemoteViews(rv))
+        assertThat(radioButtonRoot.radioImageView).hasColorFilter(Color.Red)
+    }
+
+    @Test
+    fun canTranslateRadioButton_resource_unchecked() = fakeCoroutineScope.runTest {
+        val rv = lightContext.runAndTranslate {
+            RadioButton(
+                checked = false,
+                onClick = null,
+                text = "RadioButton",
+                colors = radioButtonColors(
+                    checkedColor = ColorProvider(day = Color.Blue, night = Color.Red),
+                    uncheckedColor = ColorProvider(day = Color.Green, night = Color.Yellow),
+                )
+            )
+        }
+
+        val radioButtonRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
+        assertThat(radioButtonRoot.radioImageView).hasColorFilter(Color.Green)
+    }
+
+    @Test
+    fun canTranslateRadioButton_resource_checked() = fakeCoroutineScope.runTest {
+        val rv = lightContext.runAndTranslate {
+            RadioButton(
+                checked = true,
+                onClick = null,
+                text = "RadioButton",
+                colors = radioButtonColors(
+                    checkedColor = ColorProvider(day = Color.Blue, night = Color.Red),
+                    uncheckedColor = ColorProvider(day = Color.Green, night = Color.Yellow),
+                )
+            )
+        }
+
+        val radioButtonRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
+        assertThat(radioButtonRoot.radioImageView).hasColorFilter(Color.Blue)
+    }
+
+    @Test
+    fun canTranslateRadioButton_onClick_null() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            RadioButton(
+                checked = true,
+                onClick = null,
+                text = "RadioButton",
+            )
+        }
+
+        val radioButtonRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+        assertThat(radioButtonRoot.hasOnClickListeners()).isFalse()
+    }
+
+    @Test
+    fun canTranslateRadioButton_onClick_withAction() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            RadioButton(
+                checked = true,
+                onClick = actionRunCallback<ActionCallback>(),
+                text = "RadioButton",
+            )
+        }
+
+        val radioButtonRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+        assertThat(radioButtonRoot.hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun canTranslateRadioButton_text() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            RadioButton(
+                checked = true,
+                onClick = actionRunCallback<ActionCallback>(),
+                text = "RadioButton",
+            )
+        }
+
+        val radioButtonRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+        val text = radioButtonRoot.findViewByType<TextView>()
+        assertThat(text).isNotNull()
+        assertThat(text?.text).isEqualTo("RadioButton")
+    }
+
+    @Test
+    fun canTranslateRadioButton_disabled() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            RadioButton(
+                checked = true,
+                onClick = actionRunCallback<ActionCallback>(),
+                enabled = false,
+                text = "RadioButton",
+            )
+        }
+
+        val radioButtonRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+        assertThat(radioButtonRoot.hasOnClickListeners()).isFalse()
+    }
+
+    @Test
+    fun canTranslateRadioButtonWithSemanticsModifier_contentDescription() =
+        fakeCoroutineScope.runTest {
+            val rv = context.runAndTranslate {
+                RadioButton(
+                    checked = true,
+                    onClick = actionRunCallback<ActionCallback>(),
+                    text = "RadioButton",
+                    modifier = GlanceModifier.semantics {
+                        contentDescription = "Custom radio button description"
+                    },
+                )
+            }
+
+            val radioButtonRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+            assertThat(radioButtonRoot.contentDescription)
+                .isEqualTo("Custom radio button description")
+        }
+
+    private val ViewGroup.radioImageView: ImageView?
+        get() = findView {
+            shadowOf(it.drawable).createdFromResId ==
+                androidx.glance.appwidget.R.drawable.glance_btn_radio_material_anim
+        }
+}
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslatorTest.kt
deleted file mode 100644
index 892b5ed..0000000
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslatorTest.kt
+++ /dev/null
@@ -1,295 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.glance.appwidget.translators
-
-import android.content.Context
-import android.content.res.Configuration
-import android.view.ViewGroup
-import android.widget.ImageView
-import android.widget.TextView
-import androidx.compose.ui.graphics.Color
-import androidx.glance.GlanceModifier
-import androidx.glance.appwidget.ImageViewSubject.Companion.assertThat
-import androidx.glance.appwidget.RadioButton
-import androidx.glance.appwidget.radioButtonColors
-import androidx.glance.appwidget.action.ActionCallback
-import androidx.glance.appwidget.action.actionRunCallback
-import androidx.glance.appwidget.applyRemoteViews
-import androidx.glance.appwidget.configurationContext
-import androidx.glance.appwidget.findView
-import androidx.glance.appwidget.findViewByType
-import androidx.glance.appwidget.runAndTranslate
-import androidx.glance.color.ColorProvider
-import androidx.glance.semantics.contentDescription
-import androidx.glance.semantics.semantics
-import androidx.glance.unit.ColorProvider
-import androidx.test.core.app.ApplicationProvider
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.robolectric.RobolectricTestRunner
-import org.robolectric.Shadows.shadowOf
-import org.robolectric.annotation.Config
-import kotlin.test.assertIs
-
-@Config(sdk = [29])
-@OptIn(ExperimentalCoroutinesApi::class)
-@RunWith(RobolectricTestRunner::class)
-class RadioButtonTranslatorTest {
-
-    private lateinit var fakeCoroutineScope: TestScope
-    private val context = ApplicationProvider.getApplicationContext<Context>()
-    private val lightContext = configurationContext { uiMode = Configuration.UI_MODE_NIGHT_NO }
-    private val darkContext = configurationContext { uiMode = Configuration.UI_MODE_NIGHT_YES }
-
-    @Before
-    fun setUp() {
-        fakeCoroutineScope = TestScope()
-    }
-
-    @Test
-    fun canTranslateRadioButton_fixed_unchecked() = fakeCoroutineScope.runTest {
-        val rv = context.runAndTranslate {
-            RadioButton(
-                checked = false,
-                onClick = null,
-                text = "RadioButton",
-                colors = radioButtonColors(
-                    checkedColor = ColorProvider(Color.Blue),
-                    uncheckedColor = ColorProvider(Color.Red),
-                )
-            )
-        }
-
-        val radioButtonRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
-        assertThat(radioButtonRoot.radioImageView).hasColorFilter(Color.Red)
-    }
-
-    @Test
-    fun canTranslateRadioButton_fixed_checked() = fakeCoroutineScope.runTest {
-        val rv = context.runAndTranslate {
-            RadioButton(
-                checked = true,
-                onClick = null,
-                text = "RadioButton",
-                colors = radioButtonColors(
-                    checkedColor = ColorProvider(Color.Blue),
-                    uncheckedColor = ColorProvider(Color.Red),
-                )
-            )
-        }
-
-        val radioButtonRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
-        assertThat(radioButtonRoot.radioImageView).hasColorFilter(Color.Blue)
-    }
-
-    @Test
-    fun canTranslateRadioButton_dayNight_unchecked_day() = fakeCoroutineScope.runTest {
-        val rv = lightContext.runAndTranslate {
-            RadioButton(
-                checked = false,
-                onClick = null,
-                text = "RadioButton",
-                colors = radioButtonColors(
-                    checkedColor = ColorProvider(day = Color.Blue, night = Color.Red),
-                    uncheckedColor = ColorProvider(day = Color.Green, night = Color.Yellow),
-                )
-            )
-        }
-
-        val radioButtonRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
-        assertThat(radioButtonRoot.radioImageView).hasColorFilter(Color.Green)
-    }
-
-    @Test
-    fun canTranslateRadioButton_dayNight_unchecked_night() = fakeCoroutineScope.runTest {
-        val rv = darkContext.runAndTranslate {
-            RadioButton(
-                checked = false,
-                onClick = null,
-                text = "RadioButton",
-                colors = radioButtonColors(
-                    checkedColor = ColorProvider(day = Color.Blue, night = Color.Red),
-                    uncheckedColor = ColorProvider(day = Color.Green, night = Color.Yellow),
-                )
-            )
-        }
-
-        val radioButtonRoot = assertIs<ViewGroup>(darkContext.applyRemoteViews(rv))
-        assertThat(radioButtonRoot.radioImageView).hasColorFilter(Color.Yellow)
-    }
-
-    @Test
-    fun canTranslateRadioButton_dayNight_checked_day() = fakeCoroutineScope.runTest {
-        val rv = lightContext.runAndTranslate {
-            RadioButton(
-                checked = true,
-                onClick = null,
-                text = "RadioButton",
-                colors = radioButtonColors(
-                    checkedColor = ColorProvider(day = Color.Blue, night = Color.Red),
-                    uncheckedColor = ColorProvider(day = Color.Green, night = Color.Yellow),
-                )
-            )
-        }
-
-        val radioButtonRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
-        assertThat(radioButtonRoot.radioImageView).hasColorFilter(Color.Blue)
-    }
-
-    @Test
-    fun canTranslateRadioButton_dayNight_checked_night() = fakeCoroutineScope.runTest {
-        val rv = darkContext.runAndTranslate {
-            RadioButton(
-                checked = true,
-                onClick = null,
-                text = "RadioButton",
-                colors = radioButtonColors(
-                    checkedColor = ColorProvider(day = Color.Blue, night = Color.Red),
-                    uncheckedColor = ColorProvider(day = Color.Green, night = Color.Yellow),
-                )
-            )
-        }
-
-        val radioButtonRoot = assertIs<ViewGroup>(darkContext.applyRemoteViews(rv))
-        assertThat(radioButtonRoot.radioImageView).hasColorFilter(Color.Red)
-    }
-
-    @Test
-    fun canTranslateRadioButton_resource_unchecked() = fakeCoroutineScope.runTest {
-        val rv = lightContext.runAndTranslate {
-            RadioButton(
-                checked = false,
-                onClick = null,
-                text = "RadioButton",
-                colors = radioButtonColors(
-                    checkedColor = ColorProvider(day = Color.Blue, night = Color.Red),
-                    uncheckedColor = ColorProvider(day = Color.Green, night = Color.Yellow),
-                )
-            )
-        }
-
-        val radioButtonRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
-        assertThat(radioButtonRoot.radioImageView).hasColorFilter(Color.Green)
-    }
-
-    @Test
-    fun canTranslateRadioButton_resource_checked() = fakeCoroutineScope.runTest {
-        val rv = lightContext.runAndTranslate {
-            RadioButton(
-                checked = true,
-                onClick = null,
-                text = "RadioButton",
-                colors = radioButtonColors(
-                    checkedColor = ColorProvider(day = Color.Blue, night = Color.Red),
-                    uncheckedColor = ColorProvider(day = Color.Green, night = Color.Yellow),
-                )
-            )
-        }
-
-        val radioButtonRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
-        assertThat(radioButtonRoot.radioImageView).hasColorFilter(Color.Blue)
-    }
-
-    @Test
-    fun canTranslateRadioButton_onClick_null() = fakeCoroutineScope.runTest {
-        val rv = context.runAndTranslate {
-            RadioButton(
-                checked = true,
-                onClick = null,
-                text = "RadioButton",
-            )
-        }
-
-        val radioButtonRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
-        assertThat(radioButtonRoot.hasOnClickListeners()).isFalse()
-    }
-
-    @Test
-    fun canTranslateRadioButton_onClick_withAction() = fakeCoroutineScope.runTest {
-        val rv = context.runAndTranslate {
-            RadioButton(
-                checked = true,
-                onClick = actionRunCallback<ActionCallback>(),
-                text = "RadioButton",
-            )
-        }
-
-        val radioButtonRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
-        assertThat(radioButtonRoot.hasOnClickListeners()).isTrue()
-    }
-
-    @Test
-    fun canTranslateRadioButton_text() = fakeCoroutineScope.runTest {
-        val rv = context.runAndTranslate {
-            RadioButton(
-                checked = true,
-                onClick = actionRunCallback<ActionCallback>(),
-                text = "RadioButton",
-            )
-        }
-
-        val radioButtonRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
-        val text = radioButtonRoot.findViewByType<TextView>()
-        assertThat(text).isNotNull()
-        assertThat(text?.text).isEqualTo("RadioButton")
-    }
-
-    @Test
-    fun canTranslateRadioButton_disabled() = fakeCoroutineScope.runTest {
-        val rv = context.runAndTranslate {
-            RadioButton(
-                checked = true,
-                onClick = actionRunCallback<ActionCallback>(),
-                enabled = false,
-                text = "RadioButton",
-            )
-        }
-
-        val radioButtonRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
-        assertThat(radioButtonRoot.hasOnClickListeners()).isFalse()
-    }
-
-    @Test
-    fun canTranslateRadioButtonWithSemanticsModifier_contentDescription() =
-        fakeCoroutineScope.runTest {
-            val rv = context.runAndTranslate {
-                RadioButton(
-                    checked = true,
-                    onClick = actionRunCallback<ActionCallback>(),
-                    text = "RadioButton",
-                    modifier = GlanceModifier.semantics {
-                        contentDescription = "Custom radio button description"
-                    },
-                )
-            }
-
-            val radioButtonRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
-            assertThat(radioButtonRoot.contentDescription)
-                .isEqualTo("Custom radio button description")
-        }
-
-    private val ViewGroup.radioImageView: ImageView?
-        get() = findView {
-            shadowOf(it.drawable).createdFromResId ==
-                androidx.glance.appwidget.R.drawable.glance_btn_radio_material_anim
-        }
-}
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchBackportTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchBackportTranslatorTest.kt
new file mode 100644
index 0000000..d4ba832
--- /dev/null
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchBackportTranslatorTest.kt
@@ -0,0 +1,306 @@
+/*
+ * 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.glance.appwidget.translators
+
+import android.content.Context
+import android.content.res.Configuration
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.compose.ui.graphics.Color
+import androidx.glance.GlanceModifier
+import androidx.glance.appwidget.ImageViewSubject.Companion.assertThat
+import androidx.glance.appwidget.Switch
+import androidx.glance.appwidget.action.ActionCallback
+import androidx.glance.appwidget.action.actionRunCallback
+import androidx.glance.appwidget.applyRemoteViews
+import androidx.glance.appwidget.configurationContext
+import androidx.glance.appwidget.findView
+import androidx.glance.appwidget.runAndTranslate
+import androidx.glance.appwidget.switchColors
+import androidx.glance.color.ColorProvider
+import androidx.glance.semantics.contentDescription
+import androidx.glance.semantics.semantics
+import androidx.glance.unit.ColorProvider
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertIs
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.annotation.Config
+
+@Config(minSdk = 23, maxSdk = 30)
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(RobolectricTestRunner::class)
+class SwitchBackportTranslatorTest {
+
+    private lateinit var fakeCoroutineScope: TestScope
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private val lightContext = configurationContext { uiMode = Configuration.UI_MODE_NIGHT_NO }
+    private val darkContext = configurationContext { uiMode = Configuration.UI_MODE_NIGHT_YES }
+
+    @Before
+    fun setUp() {
+        fakeCoroutineScope = TestScope()
+    }
+
+    @Test
+    fun canTranslateSwitch_fixed_unchecked() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            Switch(
+                checked = false,
+                onCheckedChange = null,
+                text = "Switch",
+                colors = switchColors(
+                    checkedThumbColor = ColorProvider(Color.Blue),
+                    uncheckedThumbColor = ColorProvider(Color.Red),
+                    checkedTrackColor = ColorProvider(Color.Green),
+                    uncheckedTrackColor = ColorProvider(Color.Yellow)
+                )
+            )
+        }
+
+        val switchRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+        assertThat(switchRoot.thumbImageView).hasColorFilter(Color.Red)
+        assertThat(switchRoot.trackImageView).hasColorFilter(Color.Yellow)
+    }
+
+    @Test
+    fun canTranslateSwitch_fixed_checked() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            Switch(
+                checked = true,
+                onCheckedChange = null,
+                text = "Switch",
+                colors = switchColors(
+                    checkedThumbColor = ColorProvider(Color.Blue),
+                    uncheckedThumbColor = ColorProvider(Color.Red),
+                    checkedTrackColor = ColorProvider(Color.Green),
+                    uncheckedTrackColor = ColorProvider(Color.Yellow)
+                )
+            )
+        }
+
+        val switchRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+        assertThat(switchRoot.thumbImageView).hasColorFilter(Color.Blue)
+        assertThat(switchRoot.trackImageView).hasColorFilter(Color.Green)
+    }
+
+    @Test
+    fun canTranslateSwitch_dayNight_unchecked_day() = fakeCoroutineScope.runTest {
+        val thumbColor = Color.Blue
+        val trackColor = Color.Cyan
+        val otherColor = Color.White
+
+        val rv = lightContext.runAndTranslate {
+            Switch(
+                checked = false,
+                onCheckedChange = null,
+                text = "Switch",
+                colors = switchColors(
+                    checkedThumbColor = ColorProvider(otherColor),
+                    uncheckedThumbColor = ColorProvider(day = thumbColor, night = otherColor),
+                    checkedTrackColor = ColorProvider(otherColor),
+                    uncheckedTrackColor = ColorProvider(day = trackColor, night = otherColor)
+                )
+            )
+        }
+
+        val switchRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
+        assertThat(switchRoot.thumbImageView).hasColorFilter(thumbColor)
+        assertThat(switchRoot.trackImageView).hasColorFilter(trackColor)
+    }
+
+    @Test
+    fun canTranslateSwitch_dayNight_unchecked_night() = fakeCoroutineScope.runTest {
+        val thumbColor = Color.Blue
+        val trackColor = Color.Cyan
+        val otherColor = Color.White
+
+        val rv = darkContext.runAndTranslate {
+            Switch(
+                checked = false,
+                onCheckedChange = null,
+                text = "Switch",
+                colors = switchColors(
+                    checkedThumbColor = ColorProvider(otherColor),
+                    uncheckedThumbColor = ColorProvider(day = otherColor, night = thumbColor),
+                    checkedTrackColor = ColorProvider(otherColor),
+                    uncheckedTrackColor = ColorProvider(day = otherColor, night = trackColor)
+                )
+            )
+        }
+
+        val switchRoot = assertIs<ViewGroup>(darkContext.applyRemoteViews(rv))
+        assertThat(switchRoot.thumbImageView).hasColorFilter(thumbColor)
+        assertThat(switchRoot.trackImageView).hasColorFilter(trackColor)
+    }
+
+    @Test
+    fun canTranslateSwitch_dayNight_checked_day() = fakeCoroutineScope.runTest {
+        val thumbColor = Color.Blue
+        val trackColor = Color.Cyan
+        val otherColor = Color.White
+
+        val rv = lightContext.runAndTranslate {
+            Switch(
+                checked = true,
+                onCheckedChange = null,
+                text = "Switch",
+                colors = switchColors(
+                    checkedThumbColor = ColorProvider(day = thumbColor, night = otherColor),
+                    uncheckedThumbColor = ColorProvider(day = otherColor, night = otherColor),
+                    checkedTrackColor = ColorProvider(day = trackColor, night = otherColor),
+                    uncheckedTrackColor = ColorProvider(day = otherColor, night = otherColor)
+                )
+            )
+        }
+
+        val switchRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
+        assertThat(switchRoot.thumbImageView).hasColorFilter(thumbColor)
+        assertThat(switchRoot.trackImageView).hasColorFilter(trackColor)
+    }
+
+    @Test
+    fun canTranslateSwitch_dayNight_checked_night() = fakeCoroutineScope.runTest {
+        val thumbColor = Color.Blue
+        val trackColor = Color.Cyan
+        val otherColor = Color.White
+
+        val rv = darkContext.runAndTranslate {
+            Switch(
+                checked = true,
+                onCheckedChange = null,
+                text = "Switch",
+                colors = switchColors(
+                    checkedThumbColor = ColorProvider(day = otherColor, night = thumbColor),
+                    uncheckedThumbColor = ColorProvider(otherColor),
+                    checkedTrackColor = ColorProvider(day = otherColor, night = trackColor),
+                    uncheckedTrackColor = ColorProvider(otherColor)
+                )
+            )
+        }
+
+        val switchRoot = assertIs<ViewGroup>(darkContext.applyRemoteViews(rv))
+        assertThat(switchRoot.thumbImageView).hasColorFilter(thumbColor)
+        assertThat(switchRoot.trackImageView).hasColorFilter(trackColor)
+    }
+
+    @Test
+    fun canTranslateSwitch_resource_unchecked() = fakeCoroutineScope.runTest {
+        val rv = lightContext.runAndTranslate {
+            Switch(
+                checked = false,
+                onCheckedChange = null,
+                text = "Switch",
+                colors = switchColors(
+                    checkedThumbColor = ColorProvider(Color.Magenta),
+                    uncheckedThumbColor = ColorProvider(Color.Red),
+                    checkedTrackColor = ColorProvider(Color.Magenta),
+                    uncheckedTrackColor = ColorProvider(Color.Blue)
+                )
+            )
+        }
+
+        val switchRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
+        assertThat(switchRoot.thumbImageView).hasColorFilter(Color.Red)
+        assertThat(switchRoot.trackImageView).hasColorFilter(Color.Blue)
+    }
+
+    @Test
+    fun canTranslateSwitch_resource_checked() = fakeCoroutineScope.runTest {
+        val rv = lightContext.runAndTranslate {
+            Switch(
+                checked = true,
+                onCheckedChange = null,
+                text = "Switch",
+                colors = switchColors(
+                    checkedThumbColor = ColorProvider(Color.Red),
+                    uncheckedThumbColor = ColorProvider(Color.Magenta),
+                    checkedTrackColor = ColorProvider(Color.Blue),
+                    uncheckedTrackColor = ColorProvider(Color.Magenta),
+                )
+            )
+        }
+
+        val switchRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
+        assertThat(switchRoot.thumbImageView).hasColorFilter(Color.Red)
+        assertThat(switchRoot.trackImageView).hasColorFilter(Color.Blue)
+    }
+
+    @Test
+    fun canTranslateSwitch_onCheckedChange_null() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            Switch(
+                checked = true,
+                onCheckedChange = null,
+                text = "Switch",
+            )
+        }
+
+        val switchRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+        assertThat(switchRoot.hasOnClickListeners()).isFalse()
+    }
+
+    @Test
+    fun canTranslateSwitch_onCheckedChange_withAction() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            Switch(
+                checked = true,
+                onCheckedChange = actionRunCallback<ActionCallback>(),
+                text = "Switch",
+            )
+        }
+
+        val switchRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+        assertThat(switchRoot.hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun canTranslateSwitchWithSemanticsModifier_contentDescription() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            Switch(
+                checked = true,
+                onCheckedChange = actionRunCallback<ActionCallback>(),
+                text = "Switch",
+                modifier = GlanceModifier.semantics {
+                    contentDescription = "Custom switch description"
+                },
+            )
+        }
+
+        val switchRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+        assertThat(switchRoot.contentDescription).isEqualTo("Custom switch description")
+    }
+
+    private val ViewGroup.thumbImageView: ImageView?
+        get() = findView {
+            shadowOf(it.drawable).createdFromResId ==
+                androidx.glance.appwidget.R.drawable.glance_switch_thumb_animated
+        }
+
+    private val ViewGroup.trackImageView: ImageView?
+        get() = findView {
+            shadowOf(it.drawable).createdFromResId ==
+                androidx.glance.appwidget.R.drawable.glance_switch_track
+        }
+}
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchTranslatorTest.kt
deleted file mode 100644
index 9dce352..0000000
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchTranslatorTest.kt
+++ /dev/null
@@ -1,306 +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.glance.appwidget.translators
-
-import android.content.Context
-import android.content.res.Configuration
-import android.view.ViewGroup
-import android.widget.ImageView
-import androidx.compose.ui.graphics.Color
-import androidx.glance.GlanceModifier
-import androidx.glance.appwidget.ImageViewSubject.Companion.assertThat
-import androidx.glance.appwidget.Switch
-import androidx.glance.appwidget.action.ActionCallback
-import androidx.glance.appwidget.action.actionRunCallback
-import androidx.glance.appwidget.applyRemoteViews
-import androidx.glance.appwidget.configurationContext
-import androidx.glance.appwidget.findView
-import androidx.glance.appwidget.runAndTranslate
-import androidx.glance.appwidget.switchColors
-import androidx.glance.color.ColorProvider
-import androidx.glance.semantics.contentDescription
-import androidx.glance.semantics.semantics
-import androidx.glance.unit.ColorProvider
-import androidx.test.core.app.ApplicationProvider
-import com.google.common.truth.Truth.assertThat
-import kotlin.test.assertIs
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.robolectric.RobolectricTestRunner
-import org.robolectric.Shadows.shadowOf
-import org.robolectric.annotation.Config
-
-@Config(sdk = [29])
-@OptIn(ExperimentalCoroutinesApi::class)
-@RunWith(RobolectricTestRunner::class)
-class SwitchTranslatorTest {
-
-    private lateinit var fakeCoroutineScope: TestScope
-    private val context = ApplicationProvider.getApplicationContext<Context>()
-    private val lightContext = configurationContext { uiMode = Configuration.UI_MODE_NIGHT_NO }
-    private val darkContext = configurationContext { uiMode = Configuration.UI_MODE_NIGHT_YES }
-
-    @Before
-    fun setUp() {
-        fakeCoroutineScope = TestScope()
-    }
-
-    @Test
-    fun canTranslateSwitch_fixed_unchecked() = fakeCoroutineScope.runTest {
-        val rv = context.runAndTranslate {
-            Switch(
-                checked = false,
-                onCheckedChange = null,
-                text = "Switch",
-                colors = switchColors(
-                    checkedThumbColor = ColorProvider(Color.Blue),
-                    uncheckedThumbColor = ColorProvider(Color.Red),
-                    checkedTrackColor = ColorProvider(Color.Green),
-                    uncheckedTrackColor = ColorProvider(Color.Yellow)
-                )
-            )
-        }
-
-        val switchRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
-        assertThat(switchRoot.thumbImageView).hasColorFilter(Color.Red)
-        assertThat(switchRoot.trackImageView).hasColorFilter(Color.Yellow)
-    }
-
-    @Test
-    fun canTranslateSwitch_fixed_checked() = fakeCoroutineScope.runTest {
-        val rv = context.runAndTranslate {
-            Switch(
-                checked = true,
-                onCheckedChange = null,
-                text = "Switch",
-                colors = switchColors(
-                    checkedThumbColor = ColorProvider(Color.Blue),
-                    uncheckedThumbColor = ColorProvider(Color.Red),
-                    checkedTrackColor = ColorProvider(Color.Green),
-                    uncheckedTrackColor = ColorProvider(Color.Yellow)
-                )
-            )
-        }
-
-        val switchRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
-        assertThat(switchRoot.thumbImageView).hasColorFilter(Color.Blue)
-        assertThat(switchRoot.trackImageView).hasColorFilter(Color.Green)
-    }
-
-    @Test
-    fun canTranslateSwitch_dayNight_unchecked_day() = fakeCoroutineScope.runTest {
-        val thumbColor = Color.Blue
-        val trackColor = Color.Cyan
-        val otherColor = Color.White
-
-        val rv = lightContext.runAndTranslate {
-            Switch(
-                checked = false,
-                onCheckedChange = null,
-                text = "Switch",
-                colors = switchColors(
-                    checkedThumbColor = ColorProvider(otherColor),
-                    uncheckedThumbColor = ColorProvider(day = thumbColor, night = otherColor),
-                    checkedTrackColor = ColorProvider(otherColor),
-                    uncheckedTrackColor = ColorProvider(day = trackColor, night = otherColor)
-                )
-            )
-        }
-
-        val switchRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
-        assertThat(switchRoot.thumbImageView).hasColorFilter(thumbColor)
-        assertThat(switchRoot.trackImageView).hasColorFilter(trackColor)
-    }
-
-    @Test
-    fun canTranslateSwitch_dayNight_unchecked_night() = fakeCoroutineScope.runTest {
-        val thumbColor = Color.Blue
-        val trackColor = Color.Cyan
-        val otherColor = Color.White
-
-        val rv = darkContext.runAndTranslate {
-            Switch(
-                checked = false,
-                onCheckedChange = null,
-                text = "Switch",
-                colors = switchColors(
-                    checkedThumbColor = ColorProvider(otherColor),
-                    uncheckedThumbColor = ColorProvider(day = otherColor, night = thumbColor),
-                    checkedTrackColor = ColorProvider(otherColor),
-                    uncheckedTrackColor = ColorProvider(day = otherColor, night = trackColor)
-                )
-            )
-        }
-
-        val switchRoot = assertIs<ViewGroup>(darkContext.applyRemoteViews(rv))
-        assertThat(switchRoot.thumbImageView).hasColorFilter(thumbColor)
-        assertThat(switchRoot.trackImageView).hasColorFilter(trackColor)
-    }
-
-    @Test
-    fun canTranslateSwitch_dayNight_checked_day() = fakeCoroutineScope.runTest {
-        val thumbColor = Color.Blue
-        val trackColor = Color.Cyan
-        val otherColor = Color.White
-
-        val rv = lightContext.runAndTranslate {
-            Switch(
-                checked = true,
-                onCheckedChange = null,
-                text = "Switch",
-                colors = switchColors(
-                    checkedThumbColor = ColorProvider(day = thumbColor, night = otherColor),
-                    uncheckedThumbColor = ColorProvider(day = otherColor, night = otherColor),
-                    checkedTrackColor = ColorProvider(day = trackColor, night = otherColor),
-                    uncheckedTrackColor = ColorProvider(day = otherColor, night = otherColor)
-                )
-            )
-        }
-
-        val switchRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
-        assertThat(switchRoot.thumbImageView).hasColorFilter(thumbColor)
-        assertThat(switchRoot.trackImageView).hasColorFilter(trackColor)
-    }
-
-    @Test
-    fun canTranslateSwitch_dayNight_checked_night() = fakeCoroutineScope.runTest {
-        val thumbColor = Color.Blue
-        val trackColor = Color.Cyan
-        val otherColor = Color.White
-
-        val rv = darkContext.runAndTranslate {
-            Switch(
-                checked = true,
-                onCheckedChange = null,
-                text = "Switch",
-                colors = switchColors(
-                    checkedThumbColor = ColorProvider(day = otherColor, night = thumbColor),
-                    uncheckedThumbColor = ColorProvider(otherColor),
-                    checkedTrackColor = ColorProvider(day = otherColor, night = trackColor),
-                    uncheckedTrackColor = ColorProvider(otherColor)
-                )
-            )
-        }
-
-        val switchRoot = assertIs<ViewGroup>(darkContext.applyRemoteViews(rv))
-        assertThat(switchRoot.thumbImageView).hasColorFilter(thumbColor)
-        assertThat(switchRoot.trackImageView).hasColorFilter(trackColor)
-    }
-
-    @Test
-    fun canTranslateSwitch_resource_unchecked() = fakeCoroutineScope.runTest {
-        val rv = lightContext.runAndTranslate {
-            Switch(
-                checked = false,
-                onCheckedChange = null,
-                text = "Switch",
-                colors = switchColors(
-                    checkedThumbColor = ColorProvider(Color.Magenta),
-                    uncheckedThumbColor = ColorProvider(Color.Red),
-                    checkedTrackColor = ColorProvider(Color.Magenta),
-                    uncheckedTrackColor = ColorProvider(Color.Blue)
-                )
-            )
-        }
-
-        val switchRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
-        assertThat(switchRoot.thumbImageView).hasColorFilter(Color.Red)
-        assertThat(switchRoot.trackImageView).hasColorFilter(Color.Blue)
-    }
-
-    @Test
-    fun canTranslateSwitch_resource_checked() = fakeCoroutineScope.runTest {
-        val rv = lightContext.runAndTranslate {
-            Switch(
-                checked = true,
-                onCheckedChange = null,
-                text = "Switch",
-                colors = switchColors(
-                    checkedThumbColor = ColorProvider(Color.Red),
-                    uncheckedThumbColor = ColorProvider(Color.Magenta),
-                    checkedTrackColor = ColorProvider(Color.Blue),
-                    uncheckedTrackColor = ColorProvider(Color.Magenta),
-                )
-            )
-        }
-
-        val switchRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
-        assertThat(switchRoot.thumbImageView).hasColorFilter(Color.Red)
-        assertThat(switchRoot.trackImageView).hasColorFilter(Color.Blue)
-    }
-
-    @Test
-    fun canTranslateSwitch_onCheckedChange_null() = fakeCoroutineScope.runTest {
-        val rv = context.runAndTranslate {
-            Switch(
-                checked = true,
-                onCheckedChange = null,
-                text = "Switch",
-            )
-        }
-
-        val switchRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
-        assertThat(switchRoot.hasOnClickListeners()).isFalse()
-    }
-
-    @Test
-    fun canTranslateSwitch_onCheckedChange_withAction() = fakeCoroutineScope.runTest {
-        val rv = context.runAndTranslate {
-            Switch(
-                checked = true,
-                onCheckedChange = actionRunCallback<ActionCallback>(),
-                text = "Switch",
-            )
-        }
-
-        val switchRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
-        assertThat(switchRoot.hasOnClickListeners()).isTrue()
-    }
-
-    @Test
-    fun canTranslateSwitchWithSemanticsModifier_contentDescription() = fakeCoroutineScope.runTest {
-        val rv = context.runAndTranslate {
-            Switch(
-                checked = true,
-                onCheckedChange = actionRunCallback<ActionCallback>(),
-                text = "Switch",
-                modifier = GlanceModifier.semantics {
-                    contentDescription = "Custom switch description"
-                },
-            )
-        }
-
-        val switchRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
-        assertThat(switchRoot.contentDescription).isEqualTo("Custom switch description")
-    }
-
-    private val ViewGroup.thumbImageView: ImageView?
-        get() = findView {
-            shadowOf(it.drawable).createdFromResId ==
-                androidx.glance.appwidget.R.drawable.glance_switch_thumb_animated
-        }
-
-    private val ViewGroup.trackImageView: ImageView?
-        get() = findView {
-            shadowOf(it.drawable).createdFromResId ==
-                androidx.glance.appwidget.R.drawable.glance_switch_track
-        }
-}
\ No newline at end of file
diff --git a/glance/glance-wear-tiles/build.gradle b/glance/glance-wear-tiles/build.gradle
index 3b48948..a4b6f3b 100644
--- a/glance/glance-wear-tiles/build.gradle
+++ b/glance/glance-wear-tiles/build.gradle
@@ -33,13 +33,13 @@
     api("androidx.compose.runtime:runtime:1.1.1")
     api("androidx.compose.ui:ui-graphics:1.1.1")
     api("androidx.compose.ui:ui-unit:1.1.1")
-    api("androidx.wear.tiles:tiles:1.0.0")
+    api("androidx.wear.tiles:tiles:1.1.0")
 
     implementation(libs.kotlinStdlib)
     implementation(libs.kotlinCoroutinesGuava)
-    implementation "androidx.lifecycle:lifecycle-runtime:2.3.1"
-    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
-    implementation "androidx.lifecycle:lifecycle-service:2.3.1"
+    implementation(projectOrArtifact(":lifecycle:lifecycle-runtime"))
+    implementation(projectOrArtifact(":lifecycle:lifecycle-runtime-ktx"))
+    implementation(projectOrArtifact(":lifecycle:lifecycle-service"))
 
     testImplementation(libs.testCore)
     testImplementation(libs.testRules)
diff --git a/glance/glance-wear-tiles/integration-tests/demos/src/main/java/androidx/glance/wear/tiles/demos/CountTileService.kt b/glance/glance-wear-tiles/integration-tests/demos/src/main/java/androidx/glance/wear/tiles/demos/CountTileService.kt
index 6f67ce1..b83d447 100644
--- a/glance/glance-wear-tiles/integration-tests/demos/src/main/java/androidx/glance/wear/tiles/demos/CountTileService.kt
+++ b/glance/glance-wear-tiles/integration-tests/demos/src/main/java/androidx/glance/wear/tiles/demos/CountTileService.kt
@@ -44,6 +44,7 @@
 import androidx.glance.text.FontWeight
 import androidx.glance.text.Text
 import androidx.glance.text.TextStyle
+import androidx.glance.unit.ColorProvider
 import androidx.glance.wear.tiles.state.updateWearTileState
 
 private val prefsCountKey = intPreferencesKey("count")
@@ -64,6 +65,7 @@
             Text(
                 text = currentCount.toString(),
                 style = TextStyle(
+                    color = ColorProvider(Color.Gray),
                     fontWeight = FontWeight.Bold,
                     fontSize = 20.sp
                 )
diff --git a/glance/glance-wear-tiles/integration-tests/demos/src/main/java/androidx/glance/wear/tiles/demos/HelloTileService.kt b/glance/glance-wear-tiles/integration-tests/demos/src/main/java/androidx/glance/wear/tiles/demos/HelloTileService.kt
index 319c1bf0..bf46887 100644
--- a/glance/glance-wear-tiles/integration-tests/demos/src/main/java/androidx/glance/wear/tiles/demos/HelloTileService.kt
+++ b/glance/glance-wear-tiles/integration-tests/demos/src/main/java/androidx/glance/wear/tiles/demos/HelloTileService.kt
@@ -20,6 +20,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import androidx.glance.ColorFilter
 import androidx.glance.GlanceModifier
 import androidx.glance.Image
 import androidx.glance.ImageProvider
@@ -36,6 +37,7 @@
 import androidx.glance.text.FontWeight
 import androidx.glance.text.Text
 import androidx.glance.text.TextStyle
+import androidx.glance.unit.ColorProvider
 import androidx.glance.wear.tiles.GlanceTileService
 
 class HelloTileService : GlanceTileService() {
@@ -50,15 +52,17 @@
             verticalAlignment = Alignment.CenterVertically
         ) {
             Image(
-                provider = ImageProvider(R.mipmap.ic_launcher),
+                provider = ImageProvider(R.drawable.ic_waving_hand),
                 modifier = GlanceModifier.size(imageSize.width, imageSize.height),
-                contentScale = ContentScale.FillBounds,
+                contentScale = ContentScale.Fit,
+                colorFilter = ColorFilter.tint(ColorProvider(Color.Yellow)),
                 contentDescription = "Hello tile icon"
             )
             Spacer(GlanceModifier.height(10.dp))
             Text(
                 text = context.getString(R.string.hello_tile_greeting),
                 style = TextStyle(
+                    color = ColorProvider(Color.White),
                     fontWeight = FontWeight.Bold,
                     fontSize = 20.sp
                 )
diff --git a/glance/glance-wear-tiles/integration-tests/demos/src/main/res/drawable/ic_waving_hand.xml b/glance/glance-wear-tiles/integration-tests/demos/src/main/res/drawable/ic_waving_hand.xml
new file mode 100644
index 0000000..4ef53b4
--- /dev/null
+++ b/glance/glance-wear-tiles/integration-tests/demos/src/main/res/drawable/ic_waving_hand.xml
@@ -0,0 +1,20 @@
+<!--
+  Copyright 2023 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT 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: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="M23,17c0,3.31 -2.69,6 -6,6v-1.5c2.48,0 4.5,-2.02 4.5,-4.5H23zM1,7c0,-3.31 2.69,-6 6,-6v1.5C4.52,2.5 2.5,4.52 2.5,7H1zM8.01,4.32l-4.6,4.6c-3.22,3.22 -3.22,8.45 0,11.67s8.45,3.22 11.67,0l7.07,-7.07c0.49,-0.49 0.49,-1.28 0,-1.77c-0.49,-0.49 -1.28,-0.49 -1.77,0l-4.42,4.42l-0.71,-0.71l6.54,-6.54c0.49,-0.49 0.49,-1.28 0,-1.77s-1.28,-0.49 -1.77,0l-5.83,5.83l-0.71,-0.71l6.89,-6.89c0.49,-0.49 0.49,-1.28 0,-1.77s-1.28,-0.49 -1.77,0l-6.89,6.89L11.02,9.8l5.48,-5.48c0.49,-0.49 0.49,-1.28 0,-1.77s-1.28,-0.49 -1.77,0l-7.62,7.62c1.22,1.57 1.11,3.84 -0.33,5.28l-0.71,-0.71c1.17,-1.17 1.17,-3.07 0,-4.24l-0.35,-0.35l4.07,-4.07c0.49,-0.49 0.49,-1.28 0,-1.77C9.29,3.83 8.5,3.83 8.01,4.32z"/>
+</vector>
diff --git a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/GlanceTileService.kt b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/GlanceTileService.kt
index bc9c2a8..1b0a310 100644
--- a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/GlanceTileService.kt
+++ b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/GlanceTileService.kt
@@ -29,7 +29,6 @@
 import androidx.glance.state.GlanceState
 import androidx.glance.state.GlanceStateDefinition
 import androidx.glance.wear.tiles.action.RunCallbackAction
-import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.LifecycleRegistry
 import androidx.lifecycle.ServiceLifecycleDispatcher
@@ -78,9 +77,8 @@
 ) : TileService() {
     private val lifecycleOwner = object : LifecycleOwner {
         val localLifecycle = LifecycleRegistry(this)
-        override fun getLifecycle(): Lifecycle = localLifecycle
+        override val lifecycle: LifecycleRegistry = localLifecycle
     }
-
     private val lifecycleDispatcher = ServiceLifecycleDispatcher(lifecycleOwner)
     private val coroutineScope =
         CoroutineScope(
diff --git a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
index 22b630f..71db58d 100644
--- a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
+++ b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
@@ -30,12 +30,11 @@
 import androidx.glance.EmittableButton
 import androidx.glance.EmittableImage
 import androidx.glance.GlanceModifier
-import androidx.glance.semantics.SemanticsModifier
+import androidx.glance.TintColorFilterParams
 import androidx.glance.VisibilityModifier
 import androidx.glance.action.Action
 import androidx.glance.action.ActionModifier
 import androidx.glance.action.LambdaAction
-import androidx.glance.wear.tiles.action.RunCallbackAction
 import androidx.glance.action.StartActivityAction
 import androidx.glance.action.StartActivityClassAction
 import androidx.glance.action.StartActivityComponentAction
@@ -51,6 +50,7 @@
 import androidx.glance.layout.PaddingModifier
 import androidx.glance.layout.WidthModifier
 import androidx.glance.layout.collectPaddingInDp
+import androidx.glance.semantics.SemanticsModifier
 import androidx.glance.semantics.SemanticsProperties
 import androidx.glance.text.EmittableText
 import androidx.glance.text.FontStyle
@@ -61,8 +61,9 @@
 import androidx.glance.toEmittableText
 import androidx.glance.unit.ColorProvider
 import androidx.glance.unit.Dimension
-import androidx.glance.wear.tiles.curved.AnchorType
+import androidx.glance.wear.tiles.action.RunCallbackAction
 import androidx.glance.wear.tiles.curved.ActionCurvedModifier
+import androidx.glance.wear.tiles.curved.AnchorType
 import androidx.glance.wear.tiles.curved.CurvedTextStyle
 import androidx.glance.wear.tiles.curved.EmittableCurvedChild
 import androidx.glance.wear.tiles.curved.EmittableCurvedLine
@@ -573,6 +574,19 @@
             }
         )
 
+    element.colorFilterParams?.let { colorFilterParams ->
+        when (colorFilterParams) {
+            is TintColorFilterParams -> {
+                imageBuilder.setColorFilter(
+                    LayoutElementBuilders.ColorFilter.Builder()
+                        .setTint(argb(colorFilterParams.colorProvider.getColorAsArgb(context)))
+                        .build()
+                )
+            }
+
+            else -> throw IllegalArgumentException("An unsupported ColorFilter was used.")
+        }
+    }
     return imageBuilder.build()
 }
 
diff --git a/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/WearCompositionTranslatorTest.kt b/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/WearCompositionTranslatorTest.kt
index 8b47c36..b8d34c6 100644
--- a/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/WearCompositionTranslatorTest.kt
+++ b/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/WearCompositionTranslatorTest.kt
@@ -27,6 +27,7 @@
 import androidx.core.graphics.drawable.toBitmap
 import androidx.glance.Button
 import androidx.glance.ButtonColors
+import androidx.glance.ColorFilter
 import androidx.glance.GlanceId
 import androidx.glance.GlanceModifier
 import androidx.glance.Image
@@ -90,6 +91,7 @@
 import java.io.ByteArrayOutputStream
 import java.util.Arrays
 import kotlin.test.assertIs
+import kotlin.test.assertNotNull
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(RobolectricTestRunner::class)
@@ -759,6 +761,58 @@
     }
 
     @Test
+    fun translateImage_noColorFilter() = fakeCoroutineScope.runTest {
+        val compositionResult = runAndTranslate {
+            Image(
+                provider = ImageProvider(R.drawable.oval),
+                contentDescription = null,
+                modifier = GlanceModifier.width(R.dimen.dimension1).height(R.dimen.dimension2),
+            )
+        }
+
+        val content = compositionResult.layout
+        val image = (content as LayoutElementBuilders.Box).contents[0] as
+            LayoutElementBuilders.Image
+        assertThat(image.colorFilter).isNull()
+    }
+
+    @Test
+    fun translateImage_colorFilter() = fakeCoroutineScope.runTest {
+        val compositionResult = runAndTranslate {
+            Image(
+                provider = ImageProvider(R.drawable.oval),
+                contentDescription = null,
+                modifier = GlanceModifier.width(R.dimen.dimension1).height(R.dimen.dimension2),
+                colorFilter = ColorFilter.tint(ColorProvider(Color.Gray))
+            )
+        }
+
+        val content = compositionResult.layout
+        val image = (content as LayoutElementBuilders.Box).contents[0] as
+            LayoutElementBuilders.Image
+        val tint = assertNotNull(image.colorFilter?.tint)
+        assertThat(tint.argb).isEqualTo(Color.Gray.toArgb())
+    }
+
+    @Test
+    fun translateImage_colorFilterWithResource() = fakeCoroutineScope.runTest {
+        val compositionResult = runAndTranslate {
+            Image(
+                provider = ImageProvider(R.drawable.oval),
+                contentDescription = null,
+                modifier = GlanceModifier.width(R.dimen.dimension1).height(R.dimen.dimension2),
+                colorFilter = ColorFilter.tint(ColorProvider(R.color.color1))
+            )
+        }
+
+        val content = compositionResult.layout
+        val image = (content as LayoutElementBuilders.Box).contents[0] as
+            LayoutElementBuilders.Image
+        val tint = assertNotNull(image.colorFilter?.tint)
+        assertThat(tint.argb).isEqualTo(android.graphics.Color.rgb(0xC0, 0xFF, 0xEE))
+    }
+
+    @Test
     fun setSizeFromResource() = fakeCoroutineScope.runTest {
         val content = runAndTranslate {
             Column(
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index 4d59f01..e92fd04 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -21,6 +21,14 @@
     method @androidx.compose.runtime.Composable public static void Button(String text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.text.TextStyle? style, optional androidx.glance.ButtonColors colors, optional int maxLines);
   }
 
+  public final class ColorFilter {
+    field public static final androidx.glance.ColorFilter.Companion Companion;
+  }
+
+  public static final class ColorFilter.Companion {
+    method public androidx.glance.ColorFilter tint(androidx.glance.unit.ColorProvider colorProvider);
+  }
+
   public final class CombinedGlanceModifier implements androidx.glance.GlanceModifier {
     ctor public CombinedGlanceModifier(androidx.glance.GlanceModifier outer, androidx.glance.GlanceModifier inner);
     method public boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
@@ -85,7 +93,7 @@
   }
 
   public final class ImageKt {
-    method @androidx.compose.runtime.Composable public static void Image(androidx.glance.ImageProvider provider, String? contentDescription, optional androidx.glance.GlanceModifier modifier, optional int contentScale);
+    method @androidx.compose.runtime.Composable public static void Image(androidx.glance.ImageProvider provider, String? contentDescription, optional androidx.glance.GlanceModifier modifier, optional int contentScale, optional androidx.glance.ColorFilter? colorFilter);
     method public static androidx.glance.ImageProvider ImageProvider(@DrawableRes int resId);
     method public static androidx.glance.ImageProvider ImageProvider(android.graphics.Bitmap bitmap);
     method @RequiresApi(android.os.Build.VERSION_CODES.M) public static androidx.glance.ImageProvider ImageProvider(android.graphics.drawable.Icon icon);
diff --git a/glance/glance/api/public_plus_experimental_current.txt b/glance/glance/api/public_plus_experimental_current.txt
index 4d59f01..e92fd04 100644
--- a/glance/glance/api/public_plus_experimental_current.txt
+++ b/glance/glance/api/public_plus_experimental_current.txt
@@ -21,6 +21,14 @@
     method @androidx.compose.runtime.Composable public static void Button(String text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.text.TextStyle? style, optional androidx.glance.ButtonColors colors, optional int maxLines);
   }
 
+  public final class ColorFilter {
+    field public static final androidx.glance.ColorFilter.Companion Companion;
+  }
+
+  public static final class ColorFilter.Companion {
+    method public androidx.glance.ColorFilter tint(androidx.glance.unit.ColorProvider colorProvider);
+  }
+
   public final class CombinedGlanceModifier implements androidx.glance.GlanceModifier {
     ctor public CombinedGlanceModifier(androidx.glance.GlanceModifier outer, androidx.glance.GlanceModifier inner);
     method public boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
@@ -85,7 +93,7 @@
   }
 
   public final class ImageKt {
-    method @androidx.compose.runtime.Composable public static void Image(androidx.glance.ImageProvider provider, String? contentDescription, optional androidx.glance.GlanceModifier modifier, optional int contentScale);
+    method @androidx.compose.runtime.Composable public static void Image(androidx.glance.ImageProvider provider, String? contentDescription, optional androidx.glance.GlanceModifier modifier, optional int contentScale, optional androidx.glance.ColorFilter? colorFilter);
     method public static androidx.glance.ImageProvider ImageProvider(@DrawableRes int resId);
     method public static androidx.glance.ImageProvider ImageProvider(android.graphics.Bitmap bitmap);
     method @RequiresApi(android.os.Build.VERSION_CODES.M) public static androidx.glance.ImageProvider ImageProvider(android.graphics.drawable.Icon icon);
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index 4d59f01..e92fd04 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -21,6 +21,14 @@
     method @androidx.compose.runtime.Composable public static void Button(String text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.text.TextStyle? style, optional androidx.glance.ButtonColors colors, optional int maxLines);
   }
 
+  public final class ColorFilter {
+    field public static final androidx.glance.ColorFilter.Companion Companion;
+  }
+
+  public static final class ColorFilter.Companion {
+    method public androidx.glance.ColorFilter tint(androidx.glance.unit.ColorProvider colorProvider);
+  }
+
   public final class CombinedGlanceModifier implements androidx.glance.GlanceModifier {
     ctor public CombinedGlanceModifier(androidx.glance.GlanceModifier outer, androidx.glance.GlanceModifier inner);
     method public boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
@@ -85,7 +93,7 @@
   }
 
   public final class ImageKt {
-    method @androidx.compose.runtime.Composable public static void Image(androidx.glance.ImageProvider provider, String? contentDescription, optional androidx.glance.GlanceModifier modifier, optional int contentScale);
+    method @androidx.compose.runtime.Composable public static void Image(androidx.glance.ImageProvider provider, String? contentDescription, optional androidx.glance.GlanceModifier modifier, optional int contentScale, optional androidx.glance.ColorFilter? colorFilter);
     method public static androidx.glance.ImageProvider ImageProvider(@DrawableRes int resId);
     method public static androidx.glance.ImageProvider ImageProvider(android.graphics.Bitmap bitmap);
     method @RequiresApi(android.os.Build.VERSION_CODES.M) public static androidx.glance.ImageProvider ImageProvider(android.graphics.drawable.Icon icon);
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/GlanceComposable.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/GlanceComposable.kt
index ea07394..b3b877c 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/GlanceComposable.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/GlanceComposable.kt
@@ -16,7 +16,7 @@
 package androidx.glance
 import androidx.compose.runtime.ComposableTargetMarker
 /**
- * An annotation that can be used to mark an composable function as being expected to be use in a
+ * An annotation that can be used to mark a composable function as being expected to be use in a
  * composable function that is also marked or inferred to be marked as a [GlanceComposable].
  *
  * Using this annotation explicitly is rarely necessary as the Compose compiler plugin will infer
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/Image.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/Image.kt
index 7869e12..440722f 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/Image.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/Image.kt
@@ -26,6 +26,7 @@
 import androidx.glance.layout.ContentScale
 import androidx.glance.semantics.contentDescription
 import androidx.glance.semantics.semantics
+import androidx.glance.unit.ColorProvider
 
 /**
  * Interface representing an Image source which can be used with a Glance [Image] element.
@@ -76,21 +77,49 @@
 
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 /** @suppress */
+interface ColorFilterParams
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+/** @suppress */
+class TintColorFilterParams(val colorProvider: ColorProvider) : ColorFilterParams {
+    override fun toString() =
+        "TintColorFilterParams(colorProvider=$colorProvider))"
+}
+
+/**
+ * Effects used to modify the color of an image.
+ */
+class ColorFilter internal constructor(internal val colorFilterParams: ColorFilterParams) {
+    companion object {
+        /**
+         * Set a tinting option for the image using the platform-specific default blending mode.
+         *
+         * @param colorProvider Provider used to get the color for blending the source content.
+         */
+        fun tint(colorProvider: ColorProvider): ColorFilter =
+            ColorFilter(TintColorFilterParams(colorProvider))
+    }
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+/** @suppress */
 class EmittableImage : Emittable {
     override var modifier: GlanceModifier = GlanceModifier
-
     var provider: ImageProvider? = null
+    var colorFilterParams: ColorFilterParams? = null
     var contentScale: ContentScale = ContentScale.Fit
 
     override fun copy(): Emittable = EmittableImage().also {
         it.modifier = modifier
         it.provider = provider
+        it.colorFilterParams = colorFilterParams
         it.contentScale = contentScale
     }
 
     override fun toString(): String = "EmittableImage(" +
         "modifier=$modifier, " +
         "provider=$provider, " +
+        "colorFilterParams=$colorFilterParams, " +
         "contentScale=$contentScale" +
         ")"
 }
@@ -108,13 +137,15 @@
  * @param modifier Modifier used to adjust the layout algorithm or draw decoration content.
  * @param contentScale How to lay the image out with respect to its bounds, if the bounds are
  *   smaller than the image.
+ * @param colorFilter The effects to use to modify the color of an image.
  */
 @Composable
 fun Image(
     provider: ImageProvider,
     contentDescription: String?,
     modifier: GlanceModifier = GlanceModifier,
-    contentScale: ContentScale = ContentScale.Fit
+    contentScale: ContentScale = ContentScale.Fit,
+    colorFilter: ColorFilter? = null
 ) {
     val finalModifier = if (contentDescription != null) {
         modifier.semantics {
@@ -130,6 +161,7 @@
             this.set(provider) { this.provider = it }
             this.set(finalModifier) { this.modifier = it }
             this.set(contentScale) { this.contentScale = it }
+            this.set(colorFilter) { this.colorFilterParams = it?.colorFilterParams }
         }
     )
 }
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/layout/Box.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/layout/Box.kt
index 66a85b1..22328de 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/layout/Box.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/layout/Box.kt
@@ -51,6 +51,9 @@
  * subject to the [contentAlignment]. When the [content] has more than one layout child, all of
  * the children will be stacked on top of each other in the composition order.
  *
+ * Note for App Widgets: [Box] supports up to 10 child elements. Any additional elements will be
+ * truncated from the output.
+ *
  * @param modifier The modifier to be applied to the layout.
  * @param contentAlignment The alignment of children within the [Box].
  * @param content The content inside the [Box].
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/layout/Column.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/layout/Column.kt
index ba2441a..17fa31d 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/layout/Column.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/layout/Column.kt
@@ -69,6 +69,9 @@
  * been provided. When children are smaller than the size of the [Column], they will be placed
  * within the available space subject to [horizontalAlignment] and [verticalAlignment].
  *
+ * Note for App Widgets: [Column] supports up to 10 child elements. Any additional elements will be
+ * truncated from the output.
+ *
  * @param modifier The modifier to be applied to the layout.
  * @param verticalAlignment The vertical alignment to apply to the set of children, when they do not
  *   consume the full height of the [Column] (i.e. whether to push the children towards the top,
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/layout/Row.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/layout/Row.kt
index 0486cec..0259c9a9 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/layout/Row.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/layout/Row.kt
@@ -70,6 +70,9 @@
  * been provided. When children are smaller than the size of the [Row], they will be placed
  * within the available space subject to [verticalAlignment] and [horizontalAlignment].
  *
+ * Note for App Widgets: [Row] supports up to 10 child elements. Any additional elements will be
+ * truncated from the output.
+ *
  * @param modifier The modifier to be applied to the layout.
  * @param horizontalAlignment The horizontal alignment to apply to the set of children, when they do
  *   not consume the full width of the [Row] (i.e. whether to push the children towards the start,
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/session/InteractiveFrameClock.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/session/InteractiveFrameClock.kt
index 795030c..806d974 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/session/InteractiveFrameClock.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/session/InteractiveFrameClock.kt
@@ -26,6 +26,7 @@
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withTimeoutOrNull
+import kotlinx.coroutines.yield
 
 /**
  * A frame clock implementation that supports interactive mode.
@@ -100,10 +101,21 @@
             period = now - lastFrame
             minPeriod = NANOSECONDS_PER_SECOND / currentHz
         }
-        if (period >= minPeriod) {
-            sendFrame(now)
-        } else {
-            scope.launch {
+        scope.launch {
+            if (period >= minPeriod) {
+                // Our SessionWorker updates the Session whenever Recomposer.currentState is Idle.
+                // When a new frame is awaiting, it usually means that the currentState is
+                // PendingWork. Once the frame is run, the currentState will return to Idle.
+                // Sometimes, the currentState can transition from Idle to PendingWork and back to
+                // Idle without suspending, which means that the SessionWorker cannot collect the
+                // intermediate PendingWork state. Because currentState is a StateFlow,
+                // SessionWorker will not be notified of the second Idle because it is the same
+                // as the state that was collected last.
+                // Yielding here gives the SessionWorker an opportunity to collect the PendingWork
+                // state and the following Idle state as distinct states.
+                yield()
+                sendFrame(now)
+            } else {
                 delay((minPeriod - period) / NANOSECONDS_PER_MILLISECOND)
                 sendFrame(nanoTime())
             }
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/session/Session.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/session/Session.kt
index cedec4c..28c7ad1 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/session/Session.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/session/Session.kt
@@ -51,8 +51,12 @@
      * [provideGlance].
      *
      * This will also be called for the results of future recompositions.
+     * @return true if the tree has been processed and the session is ready to handle events.
      */
-    abstract suspend fun processEmittableTree(context: Context, root: EmittableWithChildren)
+    abstract suspend fun processEmittableTree(
+        context: Context,
+        root: EmittableWithChildren
+    ): Boolean
 
     /**
      * Process an event that was sent to this session.
@@ -79,7 +83,8 @@
                 block(event)
                 processEvent(context, event)
             }
-        } catch (_: ClosedReceiveChannelException) {}
+        } catch (_: ClosedReceiveChannelException) {
+        }
     }
 
     suspend fun close() {
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionWorker.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionWorker.kt
index 586e6fa..5a5728e 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionWorker.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionWorker.kt
@@ -82,11 +82,11 @@
                         // Also update the session if we have not sent an initial tree yet.
                         if (recomposer.changeCount > lastRecomposeCount || !uiReady.value) {
                             if (DEBUG) Log.d(TAG, "UI tree updated (${session.key})")
-                            session.processEmittableTree(
+                            val processed = session.processEmittableTree(
                                 applicationContext,
                                 root.copy() as EmittableWithChildren
                             )
-                            if (!uiReady.value) uiReady.emit(true)
+                            if (!uiReady.value && processed) uiReady.emit(true)
                         }
                         lastRecomposeCount = recomposer.changeCount
                     }
diff --git a/glance/glance/src/test/kotlin/androidx/glance/ImageTest.kt b/glance/glance/src/test/kotlin/androidx/glance/ImageTest.kt
index 38c08f1..95473a6 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/ImageTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/ImageTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.glance
 
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
 import androidx.glance.layout.ContentScale
 import androidx.glance.layout.PaddingModifier
@@ -23,6 +24,7 @@
 import androidx.glance.layout.runTestingComposition
 import androidx.glance.semantics.SemanticsModifier
 import androidx.glance.semantics.SemanticsProperties
+import androidx.glance.unit.ColorProvider
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
@@ -64,5 +66,29 @@
             .containsExactly("Hello World")
         assertThat(img.contentScale).isEqualTo(ContentScale.FillBounds)
         assertThat(img.modifier.findModifier<PaddingModifier>()).isNotNull()
+        assertThat(img.colorFilterParams).isNull()
+    }
+
+    @Test
+    fun createImage_tintColorFilter() {
+        val colorProvider = ColorProvider(Color.Gray)
+        fakeCoroutineScope.runTest {
+            val root = runTestingComposition {
+                Image(
+                    provider = ImageProvider(5),
+                    contentDescription = "Hello World",
+                    modifier = GlanceModifier.padding(5.dp),
+                    colorFilter = ColorFilter.tint(colorProvider)
+                )
+            }
+
+            assertThat(root.children).hasSize(1)
+            assertThat(root.children[0]).isInstanceOf(EmittableImage::class.java)
+
+            val img = root.children[0] as EmittableImage
+
+            val colorFilterParams = assertIs<TintColorFilterParams>(img.colorFilterParams)
+            assertThat(colorFilterParams.colorProvider).isEqualTo(colorProvider)
+        }
     }
 }
\ No newline at end of file
diff --git a/glance/glance/src/test/kotlin/androidx/glance/session/InteractiveFrameClockTest.kt b/glance/glance/src/test/kotlin/androidx/glance/session/InteractiveFrameClockTest.kt
index 2c55ac0..640af9f 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/session/InteractiveFrameClockTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/session/InteractiveFrameClockTest.kt
@@ -23,6 +23,7 @@
 import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.advanceUntilIdle
 import kotlinx.coroutines.test.currentTime
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import kotlinx.coroutines.yield
 import org.junit.Test
@@ -49,7 +50,9 @@
         // awaiter1 will be sent immediately, awaiter2 & awaiter3 will be sent together at least
         // 1/5th of a second later.
         val awaiter1 = async { clock.withFrameNanos { it } }
+        runCurrent()
         val awaiter2 = async { clock.withFrameNanos { it } }
+        runCurrent()
         val awaiter3 = async { clock.withFrameNanos { it } }
         advanceUntilIdle()
         val frame1 = awaiter1.await()
@@ -69,7 +72,9 @@
         // awaiter1 will be sent immediately, awaiter2 & awaiter3 will be sent together at least
         // 1/20th of a second later.
         val awaiter1 = async { clock.withFrameNanos { it } }
+        runCurrent()
         val awaiter2 = async { clock.withFrameNanos { it } }
+        runCurrent()
         val awaiter3 = async { clock.withFrameNanos { it } }
         advanceUntilIdle()
         val frame1 = awaiter1.await()
diff --git a/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt b/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
index 9ceb156..09d5058 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
@@ -52,7 +52,10 @@
             TODO("Not yet implemented")
         }
 
-        override suspend fun processEmittableTree(context: Context, root: EmittableWithChildren) {
+        override suspend fun processEmittableTree(
+            context: Context,
+            root: EmittableWithChildren
+        ): Boolean {
             TODO("Not yet implemented")
         }
 
diff --git a/glance/glance/src/test/kotlin/androidx/glance/session/SessionWorkerTest.kt b/glance/glance/src/test/kotlin/androidx/glance/session/SessionWorkerTest.kt
index 24a428a..208c6f1 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/session/SessionWorkerTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/session/SessionWorkerTest.kt
@@ -202,8 +202,12 @@
         return content
     }
 
-    override suspend fun processEmittableTree(context: Context, root: EmittableWithChildren) {
+    override suspend fun processEmittableTree(
+        context: Context,
+        root: EmittableWithChildren
+    ): Boolean {
         onUiFlow?.emit(root)
+        return true
     }
 
     override suspend fun processEvent(context: Context, event: Any) {
diff --git a/gradle.properties b/gradle.properties
index 975ac71..5868acd 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -57,9 +57,14 @@
 kotlin.mpp.stability.nowarn=true
 # b/227307216
 kotlin.mpp.absentAndroidTarget.nowarn=true
+# b/261241595
+kotlin.mpp.androidSourceSetLayoutVersion=1
+kotlin.mpp.androidSourceSetLayoutVersion1.nowarn=true
 # As of October 3 2022, AGP 7.4.0-alpha08 is higher than AGP 7.3
 # Presumably complains if using a non-stable AGP, which we are regularly doing to test pre-stable.
 kotlin.mpp.androidGradlePluginCompatibility.nowarn=true
+# Until we get a newer AGP which doesn't do this
+kotlin.options.suppressFreeCompilerArgsModificationWarning=true
 
 # Properties we often want to toggle
 # ksp.version.check=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 14270a5..d0edcbe 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -31,17 +31,18 @@
 dokka = "1.7.20"
 espresso = "3.5.1"
 espressoDevice = "1.0.0-alpha03"
+grpc = "1.52.0"
 guavaJre = "31.1-jre"
 hilt = "2.44"
 incap = "0.2"
 jcodec = "0.2.5"
-kotlin = "1.7.21"
+kotlin = "1.8.0"
 kotlinBenchmark = "0.4.7"
-kotlinNative = "1.7.21"
+kotlinNative = "1.8.0"
 kotlinCompileTesting = "1.4.9"
 kotlinCoroutines = "1.6.4"
 kotlinSerialization = "1.3.3"
-ksp = "1.7.21-1.0.8"
+ksp = "1.8.0-1.0.8"
 ktlint = "0.46.0-20220520.192227-74"
 leakcanary = "2.8.1"
 media3 = "1.0.0-beta03"
@@ -57,6 +58,7 @@
 wire = "4.4.1"
 
 [libraries]
+agpTestingPlatformCoreProto =  { module = "com.google.testing.platform:core-proto", version = "0.0.8-alpha08" }
 androidAccessibilityFramework = { module = "com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework", version = { strictly = "2.1" } }
 androidBuilderModelMin = { module = "com.android.tools.build:builder-model", version.ref = "androidGradlePluginMin" }
 androidGradlePluginz = { module = "com.android.tools.build:gradle", version.ref = "androidGradlePlugin" }
@@ -93,7 +95,7 @@
 checkerframework = { module = "org.checkerframework:checker-qual", version = "2.5.3" }
 checkmark = { module = "net.saff.checkmark:checkmark", version = "0.1.6" }
 constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.0.1"}
-dackka = { module = "com.google.devsite:dackka", version = "1.1.0" }
+dackka = { module = "com.google.devsite:dackka", version = "1.2.0" }
 dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
 daggerCompiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" }
 dexmakerMockito = { module = "com.linkedin.dexmaker:dexmaker-mockito", version.ref = "dexmaker" }
@@ -138,14 +140,19 @@
 junit = { module = "junit:junit", version = "4.13.2" }
 gcmNetworkManager = { module = "com.google.android.gms:play-services-gcm", version = "17.0.0" }
 googleCompileTesting = { module = "com.google.testing.compile:compile-testing", version = "0.18" }
+grpcBinder = { module = "io.grpc:grpc-binder", version.ref = "grpc" }
+grpcAndroid = { module = "io.grpc:grpc-android", version.ref = "grpc" }
+grpcStub = { module = "io.grpc:grpc-stub", version.ref = "grpc" }
 gson = { module = "com.google.code.gson:gson", version = "2.9.0" }
 guava = { module = "com.google.guava:guava", version.ref = "guavaJre" }
 guavaAndroid = { module = "com.google.guava:guava", version = "31.1-android" }
 guavaListenableFuture = { module = "com.google.guava:listenablefuture", version = "1.0" }
+guavaTestlib = { module = "com.google.guava:guava-testlib", version.ref = "guavaJre" }
 gradleIncapHelper = { module = "net.ltgt.gradle.incap:incap", version.ref = "incap" }
 gradleIncapHelperProcessor = { module = "net.ltgt.gradle.incap:incap-processor", version.ref = "incap" }
 kotlinAnnotationProcessingEmbeddable = { module = "org.jetbrains.kotlin:kotlin-annotation-processing-embeddable", version.ref = "kotlin" }
 kotlinBenchmarkRuntime = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "kotlinBenchmark" }
+kotlinBom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" }
 kotlinCompiler = { module = "org.jetbrains.kotlin:kotlin-compiler", version.ref = "kotlin" }
 kotlinCompilerEmbeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" }
 kotlinCompileTesting = { module = "com.github.tschuchortdev:kotlin-compile-testing", version.ref = "kotlinCompileTesting" }
@@ -160,7 +167,7 @@
 kotlinCoroutinesRx3 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx3", version.ref = "kotlinCoroutines" }
 kotlinDaemonEmbeddable = { module = "org.jetbrains.kotlin:kotlin-daemon-embeddable", version.ref = "kotlin" }
 kotlinKlibCommonizer = { module = "org.jetbrains.kotlin:kotlin-klib-commonizer", version.ref = "kotlin" }
-kotlinMetadataJvm = { module = "org.jetbrains.kotlinx:kotlinx-metadata-jvm", version = "0.5.0" }
+kotlinMetadataJvm = { module = "org.jetbrains.kotlinx:kotlinx-metadata-jvm", version = "0.6.0" }
 kotlinSerializationCore = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinSerialization" }
 kotlinSerializationProtobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinSerialization" }
 kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index c569cdd..1cf4e56 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -453,6 +453,7 @@
          <trusted-key id="e0cb7823cfd00fbf" group="com.jakewharton.android.repackaged"/>
          <trusted-key id="e0d98c5fd55a8af232290e58dee12b9896f97e34" group="org.pcollections"/>
          <trusted-key id="e16ab52d79fd224f" group="com.google.api.grpc"/>
+         <trusted-key id="e3a9f95079e84ce201f7cf60bede11eaf1164480" group="org.hamcrest"/>
          <trusted-key id="e62231331bca7e1f292c9b88c1b12a5d99c0729d" group="org.jetbrains"/>
          <trusted-key id="e77417ac194160a3fabd04969a259c7ee636c5ed">
             <trusting group="com.google.errorprone"/>
@@ -910,21 +911,21 @@
             <sha256 value="4e54622f5dc0f8b6c51e28650268f001e3b55d076c8e3a9d9731c050820c0a3d" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.7.21" androidx:reason="Unsigned, b/227204920">
-         <artifact name="kotlin-native-prebuilt-linux-x86_64-1.7.21.tar.gz">
-            <sha256 value="b6a4aef343c029ac1b7d3e70101c45356a02a30b10fdd0813fb085b29cc714f4" origin="Hand-built using sha256sum kotlin-native-prebuilt-linux-x86_64-1.7.21.tar.gz"/>
+      <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.8.0" androidx:reason="Unsigned, b/227204920">
+         <artifact name="kotlin-native-prebuilt-linux-x86_64-1.8.0.tar.gz">
+            <sha256 value="841582a0259eb0440e90f8ac6c71bd164ac1dcd01eef02c66ac9852179a86d9f" origin="Hand-built using sha256sum kotlin-native-prebuilt-linux-x86_64-1.7.21.tar.gz"/>
          </artifact>
       </component>
 
-      <component group="" name="kotlin-native-prebuilt-macos-aarch64" version="1.7.21">
-         <artifact name="kotlin-native-prebuilt-macos-aarch64-1.7.21.tar.gz">
-            <sha256 value="6953ddaa5ed2466ac606bd81338475af8064dce6a932aa51baaa0d3bff64bcc2" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-aarch64-1.7.21.tar.gz"/>
+      <component group="" name="kotlin-native-prebuilt-macos-aarch64" version="1.8.0">
+         <artifact name="kotlin-native-prebuilt-macos-aarch64-1.8.0.tar.gz">
+            <sha256 value="83022c4b47d5e0c261dd4844ec775c3cedc998d08e8cff07a9318b309ca7fbf1" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-aarch64-1.7.21.tar.gz"/>
          </artifact>
       </component>
 
-      <component group="" name="kotlin-native-prebuilt-macos-x86_64" version="1.7.21">
-         <artifact name="kotlin-native-prebuilt-macos-x86_64-1.7.21.tar.gz">
-            <sha256 value="ee01ebb7f44f8acc0aad87b61219f292dd766a06acb6ecba9fb21c5834c747eb" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-x86_64-1.7.21.tar.gz"/>
+      <component group="" name="kotlin-native-prebuilt-macos-x86_64" version="1.8.0">
+         <artifact name="kotlin-native-prebuilt-macos-x86_64-1.8.0.tar.gz">
+            <sha256 value="27f9b7de732ce36b3daf291f7970762ba7e538cf1eb75603dd2377ae8ebf9513" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-x86_64-1.7.21.tar.gz"/>
          </artifact>
       </component>
    </components>
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index edf4011..61a015a 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -2,5 +2,5 @@
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=../../../../tools/external/gradle/gradle-8.0-rc-1-bin.zip
-distributionSha256Sum=d5bb29e784426547e4f455fbc0e6512d7a6a67d7d890cf24d601309287128b79
+distributionUrl=../../../../tools/external/gradle/gradle-8.0-rc-2-bin.zip
+distributionSha256Sum=28ebe9afc20564bcdc39bfe36f6b60a373e40be2c3c307a0028b545b8ccf6ba0
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..92d9c70 100644
--- a/graphics/graphics-core/api/current.txt
+++ b/graphics/graphics-core/api/current.txt
@@ -1,6 +1,52 @@
 // Signature format: 4.0
 package androidx.graphics.lowlatency {
 
+  public final class BufferInfo {
+    method public int getFrameBufferId();
+    method public int getHeight();
+    method public int getWidth();
+    property public final int frameBufferId;
+    property public final int height;
+    property public final int width;
+  }
+
+  public final class FrontBufferSyncStrategy implements androidx.graphics.opengl.SyncStrategy {
+    ctor public FrontBufferSyncStrategy(long usageFlags);
+    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
+    method public boolean isVisible();
+    method public void setVisible(boolean);
+    property public final boolean isVisible;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.Q) public final class GLFrontBufferedRenderer<T> {
+    ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback, optional androidx.graphics.opengl.GLRenderer? glRenderer);
+    ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback);
+    method public void cancel();
+    method public void clear();
+    method public void commit();
+    method public void execute(Runnable runnable);
+    method public boolean isValid();
+    method public void release(boolean cancelPending, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onReleaseComplete);
+    method public void release(boolean cancelPending);
+    method public void renderDoubleBufferedLayer(java.util.Collection<? extends T> params);
+    method public void renderFrontBufferedLayer(T? param);
+    field public static final androidx.graphics.lowlatency.GLFrontBufferedRenderer.Companion Companion;
+  }
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
+    method @WorkerThread public default void onDoubleBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
+    method @WorkerThread public void onDrawDoubleBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, java.util.Collection<? extends T> params);
+    method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, T? param);
+    method @WorkerThread public default void onFrontBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
+  }
+
+  public static final class GLFrontBufferedRenderer.Companion {
+  }
+
+}
+
+package androidx.graphics.opengl {
+
   @RequiresApi(android.os.Build.VERSION_CODES.O) public final class FrameBuffer implements java.lang.AutoCloseable {
     ctor public FrameBuffer(androidx.graphics.opengl.egl.EGLSpec egl, android.hardware.HardwareBuffer hardwareBuffer);
     method public void close();
@@ -12,80 +58,17 @@
   }
 
   @RequiresApi(android.os.Build.VERSION_CODES.O) public final class FrameBufferRenderer implements androidx.graphics.opengl.GLRenderer.RenderCallback {
-    ctor public FrameBufferRenderer(androidx.graphics.lowlatency.FrameBufferRenderer.RenderCallback frameBufferRendererCallbacks, optional androidx.graphics.lowlatency.SyncStrategy syncStrategy);
+    ctor public FrameBufferRenderer(androidx.graphics.opengl.FrameBufferRenderer.RenderCallback frameBufferRendererCallbacks, optional androidx.graphics.opengl.SyncStrategy syncStrategy);
     method public void clear();
     method public void onDrawFrame(androidx.graphics.opengl.egl.EGLManager eglManager);
   }
 
   public static interface FrameBufferRenderer.RenderCallback {
-    method public androidx.graphics.lowlatency.FrameBuffer obtainFrameBuffer(androidx.graphics.opengl.egl.EGLSpec egl);
+    method public androidx.graphics.opengl.FrameBuffer obtainFrameBuffer(androidx.graphics.opengl.egl.EGLSpec egl);
     method public void onDraw(androidx.graphics.opengl.egl.EGLManager eglManager);
-    method public void onDrawComplete(androidx.graphics.lowlatency.FrameBuffer frameBuffer, androidx.graphics.lowlatency.SyncFenceCompat? syncFenceCompat);
+    method public void onDrawComplete(androidx.graphics.opengl.FrameBuffer frameBuffer, androidx.hardware.SyncFenceCompat? syncFenceCompat);
   }
 
-  public final class FrontBufferSyncStrategy implements androidx.graphics.lowlatency.SyncStrategy {
-    ctor public FrontBufferSyncStrategy(long usageFlags);
-    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.graphics.lowlatency.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
-    method public boolean isVisible();
-    method public void setVisible(boolean);
-    property public final boolean isVisible;
-  }
-
-  @RequiresApi(android.os.Build.VERSION_CODES.Q) public final class GLFrontBufferedRenderer<T> {
-    ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback, optional androidx.graphics.opengl.GLRenderer? glRenderer);
-    ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback);
-    method public void clear();
-    method public void commit();
-    method public boolean isValid();
-    method public void release(boolean cancelPending, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onReleaseComplete);
-    method public void release(boolean cancelPending);
-    method public void renderFrontBufferedLayer(T? param);
-    field public static final androidx.graphics.lowlatency.GLFrontBufferedRenderer.Companion Companion;
-  }
-
-  @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
-    method @WorkerThread public default void onDoubleBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
-    method @WorkerThread public void onDrawDoubleBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, java.util.Collection<? extends T> params);
-    method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, T? param);
-    method @WorkerThread public default void onFrontBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
-  }
-
-  public static final class GLFrontBufferedRenderer.Companion {
-  }
-
-  @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public final class SyncFenceCompat implements java.lang.AutoCloseable {
-    method public boolean await(long timeoutNanos);
-    method public boolean awaitForever();
-    method public void close();
-    method public static androidx.graphics.lowlatency.SyncFenceCompat createNativeSyncFence(androidx.graphics.opengl.egl.EGLSpec egl);
-    method @RequiresApi(android.os.Build.VERSION_CODES.O) public long getSignalTimeNanos();
-    method public boolean isValid();
-    field public static final androidx.graphics.lowlatency.SyncFenceCompat.Companion Companion;
-    field public static final long SIGNAL_TIME_INVALID = -1L; // 0xffffffffffffffffL
-    field public static final long SIGNAL_TIME_PENDING = 9223372036854775807L; // 0x7fffffffffffffffL
-  }
-
-  public static final class SyncFenceCompat.Companion {
-    method public androidx.graphics.lowlatency.SyncFenceCompat createNativeSyncFence(androidx.graphics.opengl.egl.EGLSpec egl);
-  }
-
-  public final class SyncFenceCompatKt {
-    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) @kotlin.jvm.JvmSynthetic public static androidx.graphics.lowlatency.SyncFenceCompat createNativeSyncFence(androidx.graphics.opengl.egl.EGLSpec);
-  }
-
-  public interface SyncStrategy {
-    method public androidx.graphics.lowlatency.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
-    field public static final androidx.graphics.lowlatency.SyncStrategy ALWAYS;
-    field public static final androidx.graphics.lowlatency.SyncStrategy.Companion Companion;
-  }
-
-  public static final class SyncStrategy.Companion {
-  }
-
-}
-
-package androidx.graphics.opengl {
-
   public final class GLRenderer {
     ctor public GLRenderer(optional kotlin.jvm.functions.Function0<? extends androidx.graphics.opengl.egl.EGLSpec> eglSpecFactory, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.egl.EGLManager,? extends android.opengl.EGLConfig> eglConfigFactory);
     method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.Surface surface, int width, int height, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
@@ -94,6 +77,7 @@
     method public androidx.graphics.opengl.GLRenderer.RenderTarget createRenderTarget(int width, int height, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
     method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending, optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onDetachComplete);
     method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending);
+    method public void execute(Runnable runnable);
     method public boolean isRunning();
     method public void registerEGLContextCallback(androidx.graphics.opengl.GLRenderer.EGLContextCallback callback);
     method public void requestRender(androidx.graphics.opengl.GLRenderer.RenderTarget target, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onRenderComplete);
@@ -131,6 +115,15 @@
     method public void resize(int width, int height);
   }
 
+  public interface SyncStrategy {
+    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
+    field public static final androidx.graphics.opengl.SyncStrategy ALWAYS;
+    field public static final androidx.graphics.opengl.SyncStrategy.Companion Companion;
+  }
+
+  public static final class SyncStrategy.Companion {
+  }
+
 }
 
 package androidx.graphics.opengl.egl {
@@ -210,7 +203,6 @@
     method public boolean eglDestroyImageKHR(androidx.opengl.EGLImageKHR image);
     method public boolean eglDestroySurface(android.opengl.EGLSurface surface);
     method public boolean eglDestroySyncKHR(androidx.opengl.EGLSyncKHR sync);
-    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFence eglDupNativeFenceFDANDROID(androidx.opengl.EGLSyncKHR sync);
     method public android.opengl.EGLSurface eglGetCurrentDrawSurface();
     method public android.opengl.EGLSurface eglGetCurrentReadSurface();
     method public int eglGetError();
@@ -270,6 +262,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 {
@@ -284,8 +277,8 @@
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction reparent(androidx.graphics.surface.SurfaceControlCompat surfaceControl, androidx.graphics.surface.SurfaceControlCompat? newParent);
     method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public androidx.graphics.surface.SurfaceControlCompat.Transaction reparent(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.view.AttachedSurfaceControl attachedSurfaceControl);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setAlpha(androidx.graphics.surface.SurfaceControlCompat surfaceControl, float alpha);
-    method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.graphics.lowlatency.SyncFenceCompat? fence, optional kotlin.jvm.functions.Function0<kotlin.Unit>? releaseCallback);
-    method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.graphics.lowlatency.SyncFenceCompat? fence);
+    method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.hardware.SyncFenceCompat? fence, optional kotlin.jvm.functions.Function0<kotlin.Unit>? releaseCallback);
+    method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.hardware.SyncFenceCompat? fence);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBufferTransform(androidx.graphics.surface.SurfaceControlCompat surfaceControl, int transformation);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setCrop(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.graphics.Rect? crop);
@@ -305,20 +298,20 @@
 
 package androidx.hardware {
 
-  @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public final class SyncFence implements java.lang.AutoCloseable {
-    ctor public SyncFence(int fd);
+  @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public final class SyncFenceCompat implements java.lang.AutoCloseable {
     method public boolean await(long timeoutNanos);
     method public boolean awaitForever();
     method public void close();
-    method protected void finalize();
-    method @RequiresApi(android.os.Build.VERSION_CODES.O) public long getSignalTime();
+    method public static androidx.hardware.SyncFenceCompat createNativeSyncFence();
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public long getSignalTimeNanos();
     method public boolean isValid();
-    field public static final androidx.hardware.SyncFence.Companion Companion;
+    field public static final androidx.hardware.SyncFenceCompat.Companion Companion;
     field public static final long SIGNAL_TIME_INVALID = -1L; // 0xffffffffffffffffL
     field public static final long SIGNAL_TIME_PENDING = 9223372036854775807L; // 0x7fffffffffffffffL
   }
 
-  public static final class SyncFence.Companion {
+  public static final class SyncFenceCompat.Companion {
+    method public androidx.hardware.SyncFenceCompat createNativeSyncFence();
   }
 
 }
@@ -331,7 +324,6 @@
     method public static androidx.opengl.EGLSyncKHR? eglCreateSyncKHR(android.opengl.EGLDisplay eglDisplay, int type, androidx.graphics.opengl.egl.EGLConfigAttributes? attributes);
     method public static boolean eglDestroyImageKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLImageKHR image);
     method public static boolean eglDestroySyncKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR eglSync);
-    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public static androidx.hardware.SyncFence eglDupNativeFenceFDANDROID(android.opengl.EGLDisplay display, androidx.opengl.EGLSyncKHR sync);
     method public static boolean eglGetSyncAttribKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR sync, int attribute, int[] value, int offset);
     method public static void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
     method public static java.util.Set<java.lang.String> parseExtensions(String queryString);
@@ -376,7 +368,6 @@
     method public androidx.opengl.EGLSyncKHR? eglCreateSyncKHR(android.opengl.EGLDisplay eglDisplay, int type, androidx.graphics.opengl.egl.EGLConfigAttributes? attributes);
     method public boolean eglDestroyImageKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLImageKHR image);
     method public boolean eglDestroySyncKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR eglSync);
-    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFence eglDupNativeFenceFDANDROID(android.opengl.EGLDisplay display, androidx.opengl.EGLSyncKHR sync);
     method public boolean eglGetSyncAttribKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR sync, int attribute, int[] value, int offset);
     method public void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
     method public java.util.Set<java.lang.String> parseExtensions(String queryString);
diff --git a/graphics/graphics-core/api/public_plus_experimental_current.txt b/graphics/graphics-core/api/public_plus_experimental_current.txt
index a0cb87a..92d9c70 100644
--- a/graphics/graphics-core/api/public_plus_experimental_current.txt
+++ b/graphics/graphics-core/api/public_plus_experimental_current.txt
@@ -1,6 +1,52 @@
 // Signature format: 4.0
 package androidx.graphics.lowlatency {
 
+  public final class BufferInfo {
+    method public int getFrameBufferId();
+    method public int getHeight();
+    method public int getWidth();
+    property public final int frameBufferId;
+    property public final int height;
+    property public final int width;
+  }
+
+  public final class FrontBufferSyncStrategy implements androidx.graphics.opengl.SyncStrategy {
+    ctor public FrontBufferSyncStrategy(long usageFlags);
+    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
+    method public boolean isVisible();
+    method public void setVisible(boolean);
+    property public final boolean isVisible;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.Q) public final class GLFrontBufferedRenderer<T> {
+    ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback, optional androidx.graphics.opengl.GLRenderer? glRenderer);
+    ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback);
+    method public void cancel();
+    method public void clear();
+    method public void commit();
+    method public void execute(Runnable runnable);
+    method public boolean isValid();
+    method public void release(boolean cancelPending, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onReleaseComplete);
+    method public void release(boolean cancelPending);
+    method public void renderDoubleBufferedLayer(java.util.Collection<? extends T> params);
+    method public void renderFrontBufferedLayer(T? param);
+    field public static final androidx.graphics.lowlatency.GLFrontBufferedRenderer.Companion Companion;
+  }
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
+    method @WorkerThread public default void onDoubleBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
+    method @WorkerThread public void onDrawDoubleBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, java.util.Collection<? extends T> params);
+    method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, T? param);
+    method @WorkerThread public default void onFrontBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
+  }
+
+  public static final class GLFrontBufferedRenderer.Companion {
+  }
+
+}
+
+package androidx.graphics.opengl {
+
   @RequiresApi(android.os.Build.VERSION_CODES.O) public final class FrameBuffer implements java.lang.AutoCloseable {
     ctor public FrameBuffer(androidx.graphics.opengl.egl.EGLSpec egl, android.hardware.HardwareBuffer hardwareBuffer);
     method public void close();
@@ -12,80 +58,17 @@
   }
 
   @RequiresApi(android.os.Build.VERSION_CODES.O) public final class FrameBufferRenderer implements androidx.graphics.opengl.GLRenderer.RenderCallback {
-    ctor public FrameBufferRenderer(androidx.graphics.lowlatency.FrameBufferRenderer.RenderCallback frameBufferRendererCallbacks, optional androidx.graphics.lowlatency.SyncStrategy syncStrategy);
+    ctor public FrameBufferRenderer(androidx.graphics.opengl.FrameBufferRenderer.RenderCallback frameBufferRendererCallbacks, optional androidx.graphics.opengl.SyncStrategy syncStrategy);
     method public void clear();
     method public void onDrawFrame(androidx.graphics.opengl.egl.EGLManager eglManager);
   }
 
   public static interface FrameBufferRenderer.RenderCallback {
-    method public androidx.graphics.lowlatency.FrameBuffer obtainFrameBuffer(androidx.graphics.opengl.egl.EGLSpec egl);
+    method public androidx.graphics.opengl.FrameBuffer obtainFrameBuffer(androidx.graphics.opengl.egl.EGLSpec egl);
     method public void onDraw(androidx.graphics.opengl.egl.EGLManager eglManager);
-    method public void onDrawComplete(androidx.graphics.lowlatency.FrameBuffer frameBuffer, androidx.graphics.lowlatency.SyncFenceCompat? syncFenceCompat);
+    method public void onDrawComplete(androidx.graphics.opengl.FrameBuffer frameBuffer, androidx.hardware.SyncFenceCompat? syncFenceCompat);
   }
 
-  public final class FrontBufferSyncStrategy implements androidx.graphics.lowlatency.SyncStrategy {
-    ctor public FrontBufferSyncStrategy(long usageFlags);
-    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.graphics.lowlatency.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
-    method public boolean isVisible();
-    method public void setVisible(boolean);
-    property public final boolean isVisible;
-  }
-
-  @RequiresApi(android.os.Build.VERSION_CODES.Q) public final class GLFrontBufferedRenderer<T> {
-    ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback, optional androidx.graphics.opengl.GLRenderer? glRenderer);
-    ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback);
-    method public void clear();
-    method public void commit();
-    method public boolean isValid();
-    method public void release(boolean cancelPending, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onReleaseComplete);
-    method public void release(boolean cancelPending);
-    method public void renderFrontBufferedLayer(T? param);
-    field public static final androidx.graphics.lowlatency.GLFrontBufferedRenderer.Companion Companion;
-  }
-
-  @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
-    method @WorkerThread public default void onDoubleBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
-    method @WorkerThread public void onDrawDoubleBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, java.util.Collection<? extends T> params);
-    method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, T? param);
-    method @WorkerThread public default void onFrontBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
-  }
-
-  public static final class GLFrontBufferedRenderer.Companion {
-  }
-
-  @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public final class SyncFenceCompat implements java.lang.AutoCloseable {
-    method public boolean await(long timeoutNanos);
-    method public boolean awaitForever();
-    method public void close();
-    method public static androidx.graphics.lowlatency.SyncFenceCompat createNativeSyncFence(androidx.graphics.opengl.egl.EGLSpec egl);
-    method @RequiresApi(android.os.Build.VERSION_CODES.O) public long getSignalTimeNanos();
-    method public boolean isValid();
-    field public static final androidx.graphics.lowlatency.SyncFenceCompat.Companion Companion;
-    field public static final long SIGNAL_TIME_INVALID = -1L; // 0xffffffffffffffffL
-    field public static final long SIGNAL_TIME_PENDING = 9223372036854775807L; // 0x7fffffffffffffffL
-  }
-
-  public static final class SyncFenceCompat.Companion {
-    method public androidx.graphics.lowlatency.SyncFenceCompat createNativeSyncFence(androidx.graphics.opengl.egl.EGLSpec egl);
-  }
-
-  public final class SyncFenceCompatKt {
-    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) @kotlin.jvm.JvmSynthetic public static androidx.graphics.lowlatency.SyncFenceCompat createNativeSyncFence(androidx.graphics.opengl.egl.EGLSpec);
-  }
-
-  public interface SyncStrategy {
-    method public androidx.graphics.lowlatency.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
-    field public static final androidx.graphics.lowlatency.SyncStrategy ALWAYS;
-    field public static final androidx.graphics.lowlatency.SyncStrategy.Companion Companion;
-  }
-
-  public static final class SyncStrategy.Companion {
-  }
-
-}
-
-package androidx.graphics.opengl {
-
   public final class GLRenderer {
     ctor public GLRenderer(optional kotlin.jvm.functions.Function0<? extends androidx.graphics.opengl.egl.EGLSpec> eglSpecFactory, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.egl.EGLManager,? extends android.opengl.EGLConfig> eglConfigFactory);
     method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.Surface surface, int width, int height, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
@@ -94,6 +77,7 @@
     method public androidx.graphics.opengl.GLRenderer.RenderTarget createRenderTarget(int width, int height, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
     method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending, optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onDetachComplete);
     method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending);
+    method public void execute(Runnable runnable);
     method public boolean isRunning();
     method public void registerEGLContextCallback(androidx.graphics.opengl.GLRenderer.EGLContextCallback callback);
     method public void requestRender(androidx.graphics.opengl.GLRenderer.RenderTarget target, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onRenderComplete);
@@ -131,6 +115,15 @@
     method public void resize(int width, int height);
   }
 
+  public interface SyncStrategy {
+    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
+    field public static final androidx.graphics.opengl.SyncStrategy ALWAYS;
+    field public static final androidx.graphics.opengl.SyncStrategy.Companion Companion;
+  }
+
+  public static final class SyncStrategy.Companion {
+  }
+
 }
 
 package androidx.graphics.opengl.egl {
@@ -210,7 +203,6 @@
     method public boolean eglDestroyImageKHR(androidx.opengl.EGLImageKHR image);
     method public boolean eglDestroySurface(android.opengl.EGLSurface surface);
     method public boolean eglDestroySyncKHR(androidx.opengl.EGLSyncKHR sync);
-    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFence eglDupNativeFenceFDANDROID(androidx.opengl.EGLSyncKHR sync);
     method public android.opengl.EGLSurface eglGetCurrentDrawSurface();
     method public android.opengl.EGLSurface eglGetCurrentReadSurface();
     method public int eglGetError();
@@ -270,6 +262,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 {
@@ -284,8 +277,8 @@
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction reparent(androidx.graphics.surface.SurfaceControlCompat surfaceControl, androidx.graphics.surface.SurfaceControlCompat? newParent);
     method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public androidx.graphics.surface.SurfaceControlCompat.Transaction reparent(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.view.AttachedSurfaceControl attachedSurfaceControl);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setAlpha(androidx.graphics.surface.SurfaceControlCompat surfaceControl, float alpha);
-    method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.graphics.lowlatency.SyncFenceCompat? fence, optional kotlin.jvm.functions.Function0<kotlin.Unit>? releaseCallback);
-    method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.graphics.lowlatency.SyncFenceCompat? fence);
+    method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.hardware.SyncFenceCompat? fence, optional kotlin.jvm.functions.Function0<kotlin.Unit>? releaseCallback);
+    method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.hardware.SyncFenceCompat? fence);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBufferTransform(androidx.graphics.surface.SurfaceControlCompat surfaceControl, int transformation);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setCrop(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.graphics.Rect? crop);
@@ -305,20 +298,20 @@
 
 package androidx.hardware {
 
-  @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public final class SyncFence implements java.lang.AutoCloseable {
-    ctor public SyncFence(int fd);
+  @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public final class SyncFenceCompat implements java.lang.AutoCloseable {
     method public boolean await(long timeoutNanos);
     method public boolean awaitForever();
     method public void close();
-    method protected void finalize();
-    method @RequiresApi(android.os.Build.VERSION_CODES.O) public long getSignalTime();
+    method public static androidx.hardware.SyncFenceCompat createNativeSyncFence();
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public long getSignalTimeNanos();
     method public boolean isValid();
-    field public static final androidx.hardware.SyncFence.Companion Companion;
+    field public static final androidx.hardware.SyncFenceCompat.Companion Companion;
     field public static final long SIGNAL_TIME_INVALID = -1L; // 0xffffffffffffffffL
     field public static final long SIGNAL_TIME_PENDING = 9223372036854775807L; // 0x7fffffffffffffffL
   }
 
-  public static final class SyncFence.Companion {
+  public static final class SyncFenceCompat.Companion {
+    method public androidx.hardware.SyncFenceCompat createNativeSyncFence();
   }
 
 }
@@ -331,7 +324,6 @@
     method public static androidx.opengl.EGLSyncKHR? eglCreateSyncKHR(android.opengl.EGLDisplay eglDisplay, int type, androidx.graphics.opengl.egl.EGLConfigAttributes? attributes);
     method public static boolean eglDestroyImageKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLImageKHR image);
     method public static boolean eglDestroySyncKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR eglSync);
-    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public static androidx.hardware.SyncFence eglDupNativeFenceFDANDROID(android.opengl.EGLDisplay display, androidx.opengl.EGLSyncKHR sync);
     method public static boolean eglGetSyncAttribKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR sync, int attribute, int[] value, int offset);
     method public static void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
     method public static java.util.Set<java.lang.String> parseExtensions(String queryString);
@@ -376,7 +368,6 @@
     method public androidx.opengl.EGLSyncKHR? eglCreateSyncKHR(android.opengl.EGLDisplay eglDisplay, int type, androidx.graphics.opengl.egl.EGLConfigAttributes? attributes);
     method public boolean eglDestroyImageKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLImageKHR image);
     method public boolean eglDestroySyncKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR eglSync);
-    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFence eglDupNativeFenceFDANDROID(android.opengl.EGLDisplay display, androidx.opengl.EGLSyncKHR sync);
     method public boolean eglGetSyncAttribKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR sync, int attribute, int[] value, int offset);
     method public void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
     method public java.util.Set<java.lang.String> parseExtensions(String queryString);
diff --git a/graphics/graphics-core/api/restricted_current.txt b/graphics/graphics-core/api/restricted_current.txt
index 9a4cd3c..47f6f95 100644
--- a/graphics/graphics-core/api/restricted_current.txt
+++ b/graphics/graphics-core/api/restricted_current.txt
@@ -1,6 +1,52 @@
 // Signature format: 4.0
 package androidx.graphics.lowlatency {
 
+  public final class BufferInfo {
+    method public int getFrameBufferId();
+    method public int getHeight();
+    method public int getWidth();
+    property public final int frameBufferId;
+    property public final int height;
+    property public final int width;
+  }
+
+  public final class FrontBufferSyncStrategy implements androidx.graphics.opengl.SyncStrategy {
+    ctor public FrontBufferSyncStrategy(long usageFlags);
+    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
+    method public boolean isVisible();
+    method public void setVisible(boolean);
+    property public final boolean isVisible;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.Q) public final class GLFrontBufferedRenderer<T> {
+    ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback, optional androidx.graphics.opengl.GLRenderer? glRenderer);
+    ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback);
+    method public void cancel();
+    method public void clear();
+    method public void commit();
+    method public void execute(Runnable runnable);
+    method public boolean isValid();
+    method public void release(boolean cancelPending, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onReleaseComplete);
+    method public void release(boolean cancelPending);
+    method public void renderDoubleBufferedLayer(java.util.Collection<? extends T> params);
+    method public void renderFrontBufferedLayer(T? param);
+    field public static final androidx.graphics.lowlatency.GLFrontBufferedRenderer.Companion Companion;
+  }
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
+    method @WorkerThread public default void onDoubleBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
+    method @WorkerThread public void onDrawDoubleBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, java.util.Collection<? extends T> params);
+    method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, T? param);
+    method @WorkerThread public default void onFrontBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
+  }
+
+  public static final class GLFrontBufferedRenderer.Companion {
+  }
+
+}
+
+package androidx.graphics.opengl {
+
   @RequiresApi(android.os.Build.VERSION_CODES.O) public final class FrameBuffer implements java.lang.AutoCloseable {
     ctor public FrameBuffer(androidx.graphics.opengl.egl.EGLSpec egl, android.hardware.HardwareBuffer hardwareBuffer);
     method public void close();
@@ -12,80 +58,17 @@
   }
 
   @RequiresApi(android.os.Build.VERSION_CODES.O) public final class FrameBufferRenderer implements androidx.graphics.opengl.GLRenderer.RenderCallback {
-    ctor public FrameBufferRenderer(androidx.graphics.lowlatency.FrameBufferRenderer.RenderCallback frameBufferRendererCallbacks, optional androidx.graphics.lowlatency.SyncStrategy syncStrategy);
+    ctor public FrameBufferRenderer(androidx.graphics.opengl.FrameBufferRenderer.RenderCallback frameBufferRendererCallbacks, optional androidx.graphics.opengl.SyncStrategy syncStrategy);
     method public void clear();
     method public void onDrawFrame(androidx.graphics.opengl.egl.EGLManager eglManager);
   }
 
   public static interface FrameBufferRenderer.RenderCallback {
-    method public androidx.graphics.lowlatency.FrameBuffer obtainFrameBuffer(androidx.graphics.opengl.egl.EGLSpec egl);
+    method public androidx.graphics.opengl.FrameBuffer obtainFrameBuffer(androidx.graphics.opengl.egl.EGLSpec egl);
     method public void onDraw(androidx.graphics.opengl.egl.EGLManager eglManager);
-    method public void onDrawComplete(androidx.graphics.lowlatency.FrameBuffer frameBuffer, androidx.graphics.lowlatency.SyncFenceCompat? syncFenceCompat);
+    method public void onDrawComplete(androidx.graphics.opengl.FrameBuffer frameBuffer, androidx.hardware.SyncFenceCompat? syncFenceCompat);
   }
 
-  public final class FrontBufferSyncStrategy implements androidx.graphics.lowlatency.SyncStrategy {
-    ctor public FrontBufferSyncStrategy(long usageFlags);
-    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.graphics.lowlatency.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
-    method public boolean isVisible();
-    method public void setVisible(boolean);
-    property public final boolean isVisible;
-  }
-
-  @RequiresApi(android.os.Build.VERSION_CODES.Q) public final class GLFrontBufferedRenderer<T> {
-    ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback, optional androidx.graphics.opengl.GLRenderer? glRenderer);
-    ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback);
-    method public void clear();
-    method public void commit();
-    method public boolean isValid();
-    method public void release(boolean cancelPending, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onReleaseComplete);
-    method public void release(boolean cancelPending);
-    method public void renderFrontBufferedLayer(T? param);
-    field public static final androidx.graphics.lowlatency.GLFrontBufferedRenderer.Companion Companion;
-  }
-
-  @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
-    method @WorkerThread public default void onDoubleBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
-    method @WorkerThread public void onDrawDoubleBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, java.util.Collection<? extends T> params);
-    method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, T? param);
-    method @WorkerThread public default void onFrontBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
-  }
-
-  public static final class GLFrontBufferedRenderer.Companion {
-  }
-
-  @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public final class SyncFenceCompat implements java.lang.AutoCloseable {
-    method public boolean await(long timeoutNanos);
-    method public boolean awaitForever();
-    method public void close();
-    method public static androidx.graphics.lowlatency.SyncFenceCompat createNativeSyncFence(androidx.graphics.opengl.egl.EGLSpec egl);
-    method @RequiresApi(android.os.Build.VERSION_CODES.O) public long getSignalTimeNanos();
-    method public boolean isValid();
-    field public static final androidx.graphics.lowlatency.SyncFenceCompat.Companion Companion;
-    field public static final long SIGNAL_TIME_INVALID = -1L; // 0xffffffffffffffffL
-    field public static final long SIGNAL_TIME_PENDING = 9223372036854775807L; // 0x7fffffffffffffffL
-  }
-
-  public static final class SyncFenceCompat.Companion {
-    method public androidx.graphics.lowlatency.SyncFenceCompat createNativeSyncFence(androidx.graphics.opengl.egl.EGLSpec egl);
-  }
-
-  public final class SyncFenceCompatKt {
-    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) @kotlin.jvm.JvmSynthetic public static androidx.graphics.lowlatency.SyncFenceCompat createNativeSyncFence(androidx.graphics.opengl.egl.EGLSpec);
-  }
-
-  public interface SyncStrategy {
-    method public androidx.graphics.lowlatency.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
-    field public static final androidx.graphics.lowlatency.SyncStrategy ALWAYS;
-    field public static final androidx.graphics.lowlatency.SyncStrategy.Companion Companion;
-  }
-
-  public static final class SyncStrategy.Companion {
-  }
-
-}
-
-package androidx.graphics.opengl {
-
   public final class GLRenderer {
     ctor public GLRenderer(optional kotlin.jvm.functions.Function0<? extends androidx.graphics.opengl.egl.EGLSpec> eglSpecFactory, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.egl.EGLManager,? extends android.opengl.EGLConfig> eglConfigFactory);
     method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.Surface surface, int width, int height, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
@@ -94,6 +77,7 @@
     method public androidx.graphics.opengl.GLRenderer.RenderTarget createRenderTarget(int width, int height, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
     method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending, optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onDetachComplete);
     method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending);
+    method public void execute(Runnable runnable);
     method public boolean isRunning();
     method public void registerEGLContextCallback(androidx.graphics.opengl.GLRenderer.EGLContextCallback callback);
     method public void requestRender(androidx.graphics.opengl.GLRenderer.RenderTarget target, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onRenderComplete);
@@ -131,6 +115,15 @@
     method public void resize(int width, int height);
   }
 
+  public interface SyncStrategy {
+    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
+    field public static final androidx.graphics.opengl.SyncStrategy ALWAYS;
+    field public static final androidx.graphics.opengl.SyncStrategy.Companion Companion;
+  }
+
+  public static final class SyncStrategy.Companion {
+  }
+
 }
 
 package androidx.graphics.opengl.egl {
@@ -211,7 +204,6 @@
     method public boolean eglDestroyImageKHR(androidx.opengl.EGLImageKHR image);
     method public boolean eglDestroySurface(android.opengl.EGLSurface surface);
     method public boolean eglDestroySyncKHR(androidx.opengl.EGLSyncKHR sync);
-    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFence eglDupNativeFenceFDANDROID(androidx.opengl.EGLSyncKHR sync);
     method public android.opengl.EGLSurface eglGetCurrentDrawSurface();
     method public android.opengl.EGLSurface eglGetCurrentReadSurface();
     method public int eglGetError();
@@ -271,6 +263,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 {
@@ -285,8 +278,8 @@
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction reparent(androidx.graphics.surface.SurfaceControlCompat surfaceControl, androidx.graphics.surface.SurfaceControlCompat? newParent);
     method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public androidx.graphics.surface.SurfaceControlCompat.Transaction reparent(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.view.AttachedSurfaceControl attachedSurfaceControl);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setAlpha(androidx.graphics.surface.SurfaceControlCompat surfaceControl, float alpha);
-    method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.graphics.lowlatency.SyncFenceCompat? fence, optional kotlin.jvm.functions.Function0<kotlin.Unit>? releaseCallback);
-    method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.graphics.lowlatency.SyncFenceCompat? fence);
+    method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.hardware.SyncFenceCompat? fence, optional kotlin.jvm.functions.Function0<kotlin.Unit>? releaseCallback);
+    method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.hardware.SyncFenceCompat? fence);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBufferTransform(androidx.graphics.surface.SurfaceControlCompat surfaceControl, int transformation);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setCrop(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.graphics.Rect? crop);
@@ -306,20 +299,20 @@
 
 package androidx.hardware {
 
-  @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public final class SyncFence implements java.lang.AutoCloseable {
-    ctor public SyncFence(int fd);
+  @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public final class SyncFenceCompat implements java.lang.AutoCloseable {
     method public boolean await(long timeoutNanos);
     method public boolean awaitForever();
     method public void close();
-    method protected void finalize();
-    method @RequiresApi(android.os.Build.VERSION_CODES.O) public long getSignalTime();
+    method public static androidx.hardware.SyncFenceCompat createNativeSyncFence();
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public long getSignalTimeNanos();
     method public boolean isValid();
-    field public static final androidx.hardware.SyncFence.Companion Companion;
+    field public static final androidx.hardware.SyncFenceCompat.Companion Companion;
     field public static final long SIGNAL_TIME_INVALID = -1L; // 0xffffffffffffffffL
     field public static final long SIGNAL_TIME_PENDING = 9223372036854775807L; // 0x7fffffffffffffffL
   }
 
-  public static final class SyncFence.Companion {
+  public static final class SyncFenceCompat.Companion {
+    method public androidx.hardware.SyncFenceCompat createNativeSyncFence();
   }
 
 }
@@ -332,7 +325,6 @@
     method public static androidx.opengl.EGLSyncKHR? eglCreateSyncKHR(android.opengl.EGLDisplay eglDisplay, int type, androidx.graphics.opengl.egl.EGLConfigAttributes? attributes);
     method public static boolean eglDestroyImageKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLImageKHR image);
     method public static boolean eglDestroySyncKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR eglSync);
-    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public static androidx.hardware.SyncFence eglDupNativeFenceFDANDROID(android.opengl.EGLDisplay display, androidx.opengl.EGLSyncKHR sync);
     method public static boolean eglGetSyncAttribKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR sync, int attribute, int[] value, int offset);
     method public static void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
     method public static java.util.Set<java.lang.String> parseExtensions(String queryString);
@@ -377,7 +369,6 @@
     method public androidx.opengl.EGLSyncKHR? eglCreateSyncKHR(android.opengl.EGLDisplay eglDisplay, int type, androidx.graphics.opengl.egl.EGLConfigAttributes? attributes);
     method public boolean eglDestroyImageKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLImageKHR image);
     method public boolean eglDestroySyncKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR eglSync);
-    method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFence eglDupNativeFenceFDANDROID(android.opengl.EGLDisplay display, androidx.opengl.EGLSyncKHR sync);
     method public boolean eglGetSyncAttribKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR sync, int attribute, int[] value, int offset);
     method public void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
     method public java.util.Set<java.lang.String> parseExtensions(String queryString);
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/FrameBufferPoolTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/FrameBufferPoolTest.kt
deleted file mode 100644
index b6b2055..0000000
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/FrameBufferPoolTest.kt
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.graphics.lowlatency
-
-import android.hardware.HardwareBuffer
-import android.os.Build
-import androidx.graphics.opengl.egl.EGLManager
-import androidx.graphics.opengl.egl.EGLSpec
-import androidx.graphics.opengl.egl.supportsNativeAndroidFence
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
-import androidx.test.filters.SmallTest
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import kotlin.concurrent.thread
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
-@RunWith(AndroidJUnit4::class)
-@SmallTest
-internal class FrameBufferPoolTest {
-
-    @Test
-    fun testHardwareBufferMatchesConfig() {
-        withEGLSpec { eglSpec ->
-            val width = 2
-            val height = 3
-            val format = HardwareBuffer.RGB_565
-            val usage = HardwareBuffer.USAGE_GPU_COLOR_OUTPUT
-            val pool = createPool(
-                width,
-                height,
-                format,
-                usage
-            )
-            try {
-                val buffer = pool.obtain(eglSpec).hardwareBuffer
-                assertEquals(width, buffer.width)
-                assertEquals(height, buffer.height)
-                assertEquals(format, buffer.format)
-                assertEquals(usage, buffer.usage)
-            } finally {
-                pool.close()
-            }
-        }
-    }
-
-    @Test
-    fun testCloseReleasesFrameBuffer() {
-        withEGLSpec { egl ->
-            val pool = createPool()
-            val frameBuffer = pool.obtain(egl)
-            pool.release(frameBuffer)
-            pool.close()
-            assertTrue(frameBuffer.isClosed)
-        }
-    }
-
-    @Test
-    fun testAllocationAtMaxPoolSizeBlocks() {
-        withEGLSpec { egl ->
-            val poolSize = 2
-            val latch = CountDownLatch(1)
-            thread {
-                val pool = createPool(maxPoolSize = poolSize)
-                // Attempting to allocate 1 additional buffer than
-                // maximum specified pool size should block
-                repeat(poolSize + 1) {
-                    pool.obtain(egl)
-                }
-                latch.countDown()
-            }
-            assertFalse(latch.await(3, TimeUnit.SECONDS))
-        }
-    }
-
-    @Test
-    fun testReleaseAtMaxPoolSizeUnblocks() {
-        withEGLSpec { egl ->
-            val poolSize = 2
-            val latch = CountDownLatch(1)
-            val pool = createPool(maxPoolSize = poolSize)
-            val b1 = pool.obtain(egl)
-            pool.obtain(egl)
-            var b3: FrameBuffer? = null
-            thread {
-                b3 = pool.obtain(egl)
-                latch.countDown()
-            }
-            pool.release(b1)
-            assertTrue(latch.await(3, TimeUnit.SECONDS))
-            assertTrue(b1 === b3)
-        }
-    }
-
-    fun createPool(
-        width: Int = 2,
-        height: Int = 3,
-        format: Int = HardwareBuffer.RGB_565,
-        usage: Long = HardwareBuffer.USAGE_GPU_COLOR_OUTPUT,
-        maxPoolSize: Int = 2
-    ): FrameBufferPool =
-        FrameBufferPool(
-            width,
-            height,
-            format,
-            usage,
-            maxPoolSize
-        )
-
-    private fun withEGLSpec(
-        block: (egl: EGLSpec) -> Unit = {}
-    ) {
-        with(EGLManager()) {
-            initialize()
-            if (supportsNativeAndroidFence()) {
-                block(eglSpec)
-            }
-            release()
-        }
-    }
-}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
index 5ccbdf2..9ed71b2 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
@@ -37,10 +37,12 @@
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.Executors
 import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotEquals
 import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -52,6 +54,7 @@
         val TAG = "GLFrontBufferedRenderer"
     }
 
+    @Ignore("b/262909049")
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
     @Test
     fun testFrontBufferedLayerRender() {
@@ -63,19 +66,18 @@
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
-                bufferWidth: Int,
-                bufferHeight: Int,
+                bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Any
             ) {
-                GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
                 Matrix.orthoM(
                     mOrthoMatrix,
                     0,
                     0f,
-                    bufferWidth.toFloat(),
+                    bufferInfo.width.toFloat(),
                     0f,
-                    bufferHeight.toFloat(),
+                    bufferInfo.height.toFloat(),
                     -1f,
                     1f
                 )
@@ -85,19 +87,18 @@
 
             override fun onDrawDoubleBufferedLayer(
                 eglManager: EGLManager,
-                bufferWidth: Int,
-                bufferHeight: Int,
+                bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Any>
             ) {
-                GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
                 Matrix.orthoM(
                     mOrthoMatrix,
                     0,
                     0f,
-                    bufferWidth.toFloat(),
+                    bufferInfo.width.toFloat(),
                     0f,
-                    bufferHeight.toFloat(),
+                    bufferInfo.height.toFloat(),
                     -1f,
                     1f
                 )
@@ -167,19 +168,18 @@
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
-                bufferWidth: Int,
-                bufferHeight: Int,
+                bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Any
             ) {
-                GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
                 Matrix.orthoM(
                     mOrthoMatrix,
                     0,
                     0f,
-                    bufferWidth.toFloat(),
+                    bufferInfo.width.toFloat(),
                     0f,
-                    bufferHeight.toFloat(),
+                    bufferInfo.height.toFloat(),
                     -1f,
                     1f
                 )
@@ -189,19 +189,18 @@
 
             override fun onDrawDoubleBufferedLayer(
                 eglManager: EGLManager,
-                bufferWidth: Int,
-                bufferHeight: Int,
+                bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Any>
             ) {
-                GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
                 Matrix.orthoM(
                     mOrthoMatrix,
                     0,
                     0f,
-                    bufferWidth.toFloat(),
+                    bufferInfo.width.toFloat(),
                     0f,
-                    bufferHeight.toFloat(),
+                    bufferInfo.height.toFloat(),
                     -1f,
                     1f
                 )
@@ -282,6 +281,471 @@
         }
     }
 
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testRenderDoubleBufferLayer() {
+        val squareSize = 100f
+        val renderLatch = CountDownLatch(1)
+        val callbacks = object : GLFrontBufferedRenderer.Callback<Int> {
+
+            private val mOrthoMatrix = FloatArray(16)
+            private val mProjectionMatrix = FloatArray(16)
+
+            override fun onDrawFrontBufferedLayer(
+                eglManager: EGLManager,
+                bufferInfo: BufferInfo,
+                transform: FloatArray,
+                param: Int
+            ) {
+                // NO-OP we do not render to the front buffered layer in this test case
+            }
+
+            override fun onDrawDoubleBufferedLayer(
+                eglManager: EGLManager,
+                bufferInfo: BufferInfo,
+                transform: FloatArray,
+                params: Collection<Int>
+            ) {
+                GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
+                Matrix.orthoM(
+                    mOrthoMatrix,
+                    0,
+                    0f,
+                    bufferInfo.width.toFloat(),
+                    0f,
+                    bufferInfo.height.toFloat(),
+                    -1f,
+                    1f
+                )
+                Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+                assertEquals(params.size, 4)
+                with(Rectangle()) {
+                    draw(mProjectionMatrix, params.elementAt(0),
+                        0f, 0f, squareSize / 2f, squareSize / 2f)
+                    draw(mProjectionMatrix, params.elementAt(1),
+                        squareSize / 2f, 0f, squareSize, squareSize / 2f)
+                    draw(mProjectionMatrix, params.elementAt(2),
+                        0f, squareSize / 2f, squareSize / 2f, squareSize)
+                    draw(mProjectionMatrix, params.elementAt(3),
+                        squareSize / 2f, squareSize / 2f, squareSize, squareSize)
+                }
+            }
+
+            override fun onDoubleBufferedLayerRenderComplete(
+                frontBufferedLayerSurfaceControl: SurfaceControlCompat,
+                transaction: SurfaceControlCompat.Transaction
+            ) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                    transaction.addTransactionCommittedListener(
+                        Executors.newSingleThreadExecutor(),
+                        object : SurfaceControlCompat.TransactionCommittedListener {
+                            override fun onTransactionCommitted() {
+                                renderLatch.countDown()
+                            }
+                        })
+                } else {
+                    renderLatch.countDown()
+                }
+            }
+        }
+        var renderer: GLFrontBufferedRenderer<Int>? = null
+        var surfaceView: SurfaceView? = null
+        try {
+            val scenario = ActivityScenario.launch(FrontBufferedRendererTestActivity::class.java)
+                .moveToState(Lifecycle.State.CREATED)
+                .onActivity {
+                    surfaceView = it.getSurfaceView()
+                    renderer = GLFrontBufferedRenderer(surfaceView!!, callbacks)
+                }
+
+            scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+                val colors = listOf(Color.RED, Color.BLACK, Color.YELLOW, Color.BLUE)
+                renderer?.renderDoubleBufferedLayer(colors)
+            }
+            assertTrue(renderLatch.await(3000, TimeUnit.MILLISECONDS))
+
+            val coords = IntArray(2)
+            with(surfaceView!!) {
+                getLocationOnScreen(coords)
+            }
+
+            SurfaceControlUtils.validateOutput { bitmap ->
+                val topLeft = bitmap.getPixel(
+                    coords[0] + (squareSize / 4).toInt(),
+                    coords[1] + (squareSize / 4).toInt()
+                )
+                val topRight = bitmap.getPixel(
+                    coords[0] + (squareSize * 3f / 4f).roundToInt(),
+                    coords[1] + (squareSize / 4).toInt()
+                )
+                val bottomLeft = bitmap.getPixel(
+                    coords[0] + (squareSize / 4f).toInt(),
+                    coords[1] + (squareSize * 3f / 4f).roundToInt()
+                )
+                val bottomRight = bitmap.getPixel(
+                    coords[0] + (squareSize * 3f / 4f).roundToInt(),
+                    coords[1] + (squareSize * 3f / 4f).roundToInt()
+                )
+                Color.RED == topLeft &&
+                    Color.BLACK == topRight &&
+                    Color.YELLOW == bottomLeft &&
+                    Color.BLUE == bottomRight
+            }
+        } finally {
+            renderer.blockingRelease()
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testBufferRetargetingFrontBufferLayer() {
+        val squareSize = 100f
+        val renderLatch = CountDownLatch(1)
+        val callbacks = object : GLFrontBufferedRenderer.Callback<Int> {
+
+            private val mOrthoMatrix = FloatArray(16)
+            private val mProjectionMatrix = FloatArray(16)
+
+            override fun onDrawFrontBufferedLayer(
+                eglManager: EGLManager,
+                bufferInfo: BufferInfo,
+                transform: FloatArray,
+                param: Int
+            ) {
+                GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
+                Matrix.orthoM(
+                    mOrthoMatrix,
+                    0,
+                    0f,
+                    bufferInfo.width.toFloat(),
+                    0f,
+                    bufferInfo.height.toFloat(),
+                    -1f,
+                    1f
+                )
+                Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+                val buffer = IntArray(1)
+                GLES20.glGenFramebuffers(1, buffer, 0)
+                GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, buffer[0])
+                Rectangle().draw(transform, Color.RED, 0f, 0f, squareSize, squareSize)
+
+                GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, bufferInfo.frameBufferId)
+                Rectangle().draw(mProjectionMatrix, param, 0f, 0f, squareSize, squareSize)
+            }
+
+            override fun onFrontBufferedLayerRenderComplete(
+                frontBufferedLayerSurfaceControl: SurfaceControlCompat,
+                transaction: SurfaceControlCompat.Transaction
+            ) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                    transaction.addTransactionCommittedListener(
+                        Executors.newSingleThreadExecutor(),
+                        object : SurfaceControlCompat.TransactionCommittedListener {
+                            override fun onTransactionCommitted() {
+                                renderLatch.countDown()
+                            }
+                        })
+                } else {
+                    renderLatch.countDown()
+                }
+            }
+
+            override fun onDrawDoubleBufferedLayer(
+                eglManager: EGLManager,
+                bufferInfo: BufferInfo,
+                transform: FloatArray,
+                params: Collection<Int>
+            ) {
+                // NO-OP
+            }
+        }
+        var renderer: GLFrontBufferedRenderer<Int>? = null
+        var surfaceView: SurfaceView? = null
+        try {
+            val scenario = ActivityScenario.launch(FrontBufferedRendererTestActivity::class.java)
+                .moveToState(Lifecycle.State.CREATED)
+                .onActivity {
+                    surfaceView = it.getSurfaceView()
+                    renderer = GLFrontBufferedRenderer(surfaceView!!, callbacks)
+                }
+
+            scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+                renderer?.renderFrontBufferedLayer(Color.BLUE)
+            }
+            assertTrue(renderLatch.await(3000, TimeUnit.MILLISECONDS))
+
+            val coords = IntArray(2)
+            with(surfaceView!!) {
+                getLocationOnScreen(coords)
+            }
+
+            SurfaceControlUtils.validateOutput { bitmap ->
+                val center = bitmap.getPixel(
+                    coords[0] + (squareSize / 2).toInt(),
+                    coords[1] + (squareSize / 2).toInt()
+                )
+                Color.BLUE == center
+            }
+        } finally {
+            renderer.blockingRelease()
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testBufferRetargetingDoubleBufferedLayer() {
+        val squareSize = 100f
+        val renderLatch = CountDownLatch(1)
+        val callbacks = object : GLFrontBufferedRenderer.Callback<Int> {
+
+            private val mOrthoMatrix = FloatArray(16)
+            private val mProjectionMatrix = FloatArray(16)
+
+            override fun onDrawFrontBufferedLayer(
+                eglManager: EGLManager,
+                bufferInfo: BufferInfo,
+                transform: FloatArray,
+                param: Int
+            ) {
+                // NO-OP
+            }
+
+            override fun onDrawDoubleBufferedLayer(
+                eglManager: EGLManager,
+                bufferInfo: BufferInfo,
+                transform: FloatArray,
+                params: Collection<Int>
+            ) {
+                GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
+                Matrix.orthoM(
+                    mOrthoMatrix,
+                    0,
+                    0f,
+                    bufferInfo.width.toFloat(),
+                    0f,
+                    bufferInfo.height.toFloat(),
+                    -1f,
+                    1f
+                )
+                Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+                val buffer = IntArray(1)
+                GLES20.glGenFramebuffers(1, buffer, 0)
+                GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, buffer[0])
+                Rectangle().draw(transform, Color.RED, 0f, 0f, squareSize, squareSize)
+
+                GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, bufferInfo.frameBufferId)
+                for (param in params) {
+                    Rectangle().draw(mProjectionMatrix, param, 0f, 0f, squareSize, squareSize)
+                }
+            }
+
+            override fun onDoubleBufferedLayerRenderComplete(
+                frontBufferedLayerSurfaceControl: SurfaceControlCompat,
+                transaction: SurfaceControlCompat.Transaction
+            ) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                    transaction.addTransactionCommittedListener(
+                        Executors.newSingleThreadExecutor(),
+                        object : SurfaceControlCompat.TransactionCommittedListener {
+                            override fun onTransactionCommitted() {
+                                renderLatch.countDown()
+                            }
+                        })
+                } else {
+                    renderLatch.countDown()
+                }
+            }
+        }
+
+        var renderer: GLFrontBufferedRenderer<Int>? = null
+        var surfaceView: SurfaceView? = null
+        try {
+            val scenario = ActivityScenario.launch(FrontBufferedRendererTestActivity::class.java)
+                .moveToState(Lifecycle.State.CREATED)
+                .onActivity {
+                    surfaceView = it.getSurfaceView()
+                    renderer = GLFrontBufferedRenderer(surfaceView!!, callbacks)
+                }
+
+            scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+                renderer?.renderFrontBufferedLayer(Color.BLUE)
+                renderer?.commit()
+            }
+            assertTrue(renderLatch.await(3000, TimeUnit.MILLISECONDS))
+
+            val coords = IntArray(2)
+            with(surfaceView!!) {
+                getLocationOnScreen(coords)
+            }
+
+            SurfaceControlUtils.validateOutput { bitmap ->
+                val center = bitmap.getPixel(
+                    coords[0] + (squareSize / 2).toInt(),
+                    coords[1] + (squareSize / 2).toInt()
+                )
+                Color.BLUE == center
+            }
+        } finally {
+            renderer.blockingRelease()
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testCancelFrontBufferLayerRender() {
+        val squareSize = 100f
+        val renderLatch = CountDownLatch(1)
+        val callbacks = object : GLFrontBufferedRenderer.Callback<Int> {
+
+            private val mOrthoMatrix = FloatArray(16)
+            private val mProjectionMatrix = FloatArray(16)
+
+            override fun onDrawFrontBufferedLayer(
+                eglManager: EGLManager,
+                bufferInfo: BufferInfo,
+                transform: FloatArray,
+                param: Int
+            ) {
+                GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
+                Matrix.orthoM(
+                    mOrthoMatrix,
+                    0,
+                    0f,
+                    bufferInfo.width.toFloat(),
+                    0f,
+                    bufferInfo.height.toFloat(),
+                    -1f,
+                    1f
+                )
+                Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+                Rectangle().draw(mProjectionMatrix, param, 0f, 0f, squareSize, squareSize)
+            }
+
+            override fun onDrawDoubleBufferedLayer(
+                eglManager: EGLManager,
+                bufferInfo: BufferInfo,
+                transform: FloatArray,
+                params: Collection<Int>
+            ) {
+
+                GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
+                Matrix.orthoM(
+                    mOrthoMatrix,
+                    0,
+                    0f,
+                    bufferInfo.width.toFloat(),
+                    0f,
+                    bufferInfo.height.toFloat(),
+                    -1f,
+                    1f
+                )
+                Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+                for (p in params) {
+                    Rectangle().draw(mProjectionMatrix, p, 0f, 0f, squareSize, squareSize)
+                }
+            }
+
+            override fun onDoubleBufferedLayerRenderComplete(
+                frontBufferedLayerSurfaceControl: SurfaceControlCompat,
+                transaction: SurfaceControlCompat.Transaction
+            ) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                    transaction.addTransactionCommittedListener(
+                        Executors.newSingleThreadExecutor(),
+                        object : SurfaceControlCompat.TransactionCommittedListener {
+                            override fun onTransactionCommitted() {
+                                renderLatch.countDown()
+                            }
+                        })
+                } else {
+                    renderLatch.countDown()
+                }
+            }
+        }
+        var renderer: GLFrontBufferedRenderer<Int>? = null
+        var surfaceView: SurfaceView? = null
+        try {
+            val scenario = ActivityScenario.launch(FrontBufferedRendererTestActivity::class.java)
+                .moveToState(Lifecycle.State.CREATED)
+                .onActivity {
+                    surfaceView = it.getSurfaceView()
+                    renderer = GLFrontBufferedRenderer(surfaceView!!, callbacks)
+                }
+
+            scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+                with(renderer!!) {
+                    renderFrontBufferedLayer(Color.BLUE)
+                    commit()
+                    renderFrontBufferedLayer(Color.RED)
+                    cancel()
+                }
+            }
+            assertTrue(renderLatch.await(3000, TimeUnit.MILLISECONDS))
+
+            val coords = IntArray(2)
+            with(surfaceView!!) {
+                getLocationOnScreen(coords)
+            }
+
+            SurfaceControlUtils.validateOutput { bitmap ->
+                val pixel = bitmap.getPixel(
+                    coords[0] + (squareSize / 2).toInt(),
+                    coords[1] + (squareSize / 2).toInt()
+                )
+                // After cancel is invoked the front buffered layer should not be visible
+                Color.BLUE == pixel
+            }
+        } finally {
+            renderer.blockingRelease()
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testExecute() {
+        val executeLatch = CountDownLatch(1)
+        val callbacks = object : GLFrontBufferedRenderer.Callback<Int> {
+
+            override fun onDrawFrontBufferedLayer(
+                eglManager: EGLManager,
+                bufferInfo: BufferInfo,
+                transform: FloatArray,
+                param: Int
+            ) {
+                // NO-OP
+            }
+
+            override fun onDrawDoubleBufferedLayer(
+                eglManager: EGLManager,
+                bufferInfo: BufferInfo,
+                transform: FloatArray,
+                params: Collection<Int>
+            ) {
+                // NO-OP
+            }
+        }
+        var renderer: GLFrontBufferedRenderer<Int>? = null
+        var surfaceView: SurfaceView?
+        try {
+            val scenario = ActivityScenario.launch(FrontBufferedRendererTestActivity::class.java)
+                .moveToState(Lifecycle.State.CREATED)
+                .onActivity {
+                    surfaceView = it.getSurfaceView()
+                    renderer = GLFrontBufferedRenderer(surfaceView!!, callbacks)
+                }
+
+            scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+                renderer?.execute {
+                    executeLatch.countDown()
+                }
+            }
+
+            assertTrue(executeLatch.await(3000, TimeUnit.MILLISECONDS))
+        } finally {
+            renderer.blockingRelease()
+        }
+    }
+
     @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
     fun testUsageFlagContainsFrontBufferUsage() {
@@ -336,19 +800,18 @@
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
-                bufferWidth: Int,
-                bufferHeight: Int,
+                bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Any
             ) {
-                GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
                 Matrix.orthoM(
                     mOrthoMatrix,
                     0,
                     0f,
-                    bufferWidth.toFloat(),
+                    bufferInfo.width.toFloat(),
                     0f,
-                    bufferHeight.toFloat(),
+                    bufferInfo.height.toFloat(),
                     -1f,
                     1f
                 )
@@ -363,19 +826,18 @@
 
             override fun onDrawDoubleBufferedLayer(
                 eglManager: EGLManager,
-                bufferWidth: Int,
-                bufferHeight: Int,
+                bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Any>
             ) {
-                GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
                 Matrix.orthoM(
                     mOrthoMatrix,
                     0,
                     0f,
-                    bufferWidth.toFloat(),
+                    bufferInfo.width.toFloat(),
                     0f,
-                    bufferHeight.toFloat(),
+                    bufferInfo.height.toFloat(),
                     -1f,
                     1f
                 )
@@ -416,19 +878,18 @@
         val callbacks = object : GLFrontBufferedRenderer.Callback<Any> {
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
-                bufferWidth: Int,
-                bufferHeight: Int,
+                bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Any
             ) {
-                GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
                 Matrix.orthoM(
                     mOrthoMatrix,
                     0,
                     0f,
-                    bufferWidth.toFloat(),
+                    bufferInfo.width.toFloat(),
                     0f,
-                    bufferHeight.toFloat(),
+                    bufferInfo.height.toFloat(),
                     -1f,
                     1f
                 )
@@ -444,19 +905,18 @@
 
             override fun onDrawDoubleBufferedLayer(
                 eglManager: EGLManager,
-                bufferWidth: Int,
-                bufferHeight: Int,
+                bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Any>
             ) {
-                GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
                 Matrix.orthoM(
                     mOrthoMatrix,
                     0,
                     0f,
-                    bufferWidth.toFloat(),
+                    bufferInfo.width.toFloat(),
                     0f,
-                    bufferHeight.toFloat(),
+                    bufferInfo.height.toFloat(),
                     -1f,
                     1f
                 )
@@ -531,6 +991,7 @@
         }
     }
 
+    @Ignore("b/262909049")
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
     @Test
     fun testRenderAfterPauseAndResume() {
@@ -542,19 +1003,18 @@
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
-                bufferWidth: Int,
-                bufferHeight: Int,
+                bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Any
             ) {
-                GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
                 Matrix.orthoM(
                     mOrthoMatrix,
                     0,
                     0f,
-                    bufferWidth.toFloat(),
+                    bufferInfo.width.toFloat(),
                     0f,
-                    bufferHeight.toFloat(),
+                    bufferInfo.height.toFloat(),
                     -1f,
                     1f
                 )
@@ -564,19 +1024,18 @@
 
             override fun onDrawDoubleBufferedLayer(
                 eglManager: EGLManager,
-                bufferWidth: Int,
-                bufferHeight: Int,
+                bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Any>
             ) {
-                GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
                 Matrix.orthoM(
                     mOrthoMatrix,
                     0,
                     0f,
-                    bufferWidth.toFloat(),
+                    bufferInfo.width.toFloat(),
                     0f,
-                    bufferHeight.toFloat(),
+                    bufferInfo.height.toFloat(),
                     -1f,
                     1f
                 )
@@ -673,19 +1132,18 @@
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
-                bufferWidth: Int,
-                bufferHeight: Int,
+                bufferInfo: BufferInfo,
                 transform: FloatArray,
                 param: Any
             ) {
-                GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
                 Matrix.orthoM(
                     mOrthoMatrix,
                     0,
                     0f,
-                    bufferWidth.toFloat(),
+                    bufferInfo.width.toFloat(),
                     0f,
-                    bufferHeight.toFloat(),
+                    bufferInfo.height.toFloat(),
                     -1f,
                     1f
                 )
@@ -695,19 +1153,18 @@
 
             override fun onDrawDoubleBufferedLayer(
                 eglManager: EGLManager,
-                bufferWidth: Int,
-                bufferHeight: Int,
+                bufferInfo: BufferInfo,
                 transform: FloatArray,
                 params: Collection<Any>
             ) {
-                GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
                 Matrix.orthoM(
                     mOrthoMatrix,
                     0,
                     0f,
-                    bufferWidth.toFloat(),
+                    bufferInfo.width.toFloat(),
                     0f,
-                    bufferHeight.toFloat(),
+                    bufferInfo.height.toFloat(),
                     -1f,
                     1f
                 )
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt
index 0e5642c..160db4b 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt
@@ -63,19 +63,18 @@
 
         override fun onDrawFrontBufferedLayer(
             eglManager: EGLManager,
-            bufferWidth: Int,
-            bufferHeight: Int,
+            bufferInfo: BufferInfo,
             transform: FloatArray,
             param: FloatArray
         ) {
-            GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+            GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
             Matrix.orthoM(
                 mMVPMatrix,
                 0,
                 0f,
-                bufferWidth.toFloat(),
+                bufferInfo.width.toFloat(),
                 0f,
-                bufferHeight.toFloat(),
+                bufferInfo.height.toFloat(),
                 -1f,
                 1f
             )
@@ -94,21 +93,20 @@
 
         override fun onDrawDoubleBufferedLayer(
             eglManager: EGLManager,
-            bufferWidth: Int,
-            bufferHeight: Int,
+            bufferInfo: BufferInfo,
             transform: FloatArray,
             params: Collection<FloatArray>
         ) {
-            GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+            GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
             GLES20.glClearColor(0f, 0f, 0f, 0f)
             GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
             Matrix.orthoM(
                 mMVPMatrix,
                 0,
                 0f,
-                bufferWidth.toFloat(),
+                bufferInfo.width.toFloat(),
                 0f,
-                bufferHeight.toFloat(),
+                bufferInfo.height.toFloat(),
                 -1f,
                 1f
             )
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SyncStrategyTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SyncStrategyTest.kt
deleted file mode 100644
index b8a00d2..0000000
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SyncStrategyTest.kt
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.graphics.lowlatency
-
-import android.opengl.EGL14
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.graphics.opengl.egl.EGLConfigAttributes
-import androidx.graphics.opengl.egl.EGLManager
-import androidx.graphics.opengl.egl.EGLSpec
-import androidx.graphics.opengl.egl.EGLVersion
-import androidx.graphics.opengl.egl.supportsNativeAndroidFence
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import org.junit.Assert
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-@SmallTest
-@RequiresApi(Build.VERSION_CODES.Q)
-class SyncStrategyTest {
-    private val mUsageFlags = GLFrontBufferedRenderer.obtainHardwareBufferUsageFlags()
-
-    @RequiresApi(Build.VERSION_CODES.O)
-    @Test
-    fun testSyncStrategy_Always() {
-        val egl = createAndSetupEGLManager(EGLSpec.V14)
-        if (egl.supportsNativeAndroidFence()) {
-            val strategy = SyncStrategy.ALWAYS
-            val fence = strategy.createSyncFence(egl.eglSpec)
-            assertTrue(fence != null)
-            fence?.close()
-        }
-    }
-
-    @Test
-    fun testSyncStrategy_onFirstShow_FrontBufferUsageOff_Invisible() {
-        val egl = createAndSetupEGLManager(EGLSpec.V14)
-        if (egl.supportsNativeAndroidFence()) {
-            val strategy = FrontBufferSyncStrategy(0L)
-            val fence = strategy.createSyncFence(EGLSpec.V14)
-            assertTrue(fence != null)
-            fence?.close()
-        }
-    }
-
-    @Test
-    fun testSyncStrategy_onFirstShow_FrontBufferUsageOff_Visible() {
-        val egl = createAndSetupEGLManager(EGLSpec.V14)
-        if (egl.supportsNativeAndroidFence()) {
-            val strategy = FrontBufferSyncStrategy(0L)
-            strategy.isVisible = true
-            val fence = strategy.createSyncFence(EGLSpec.V14)
-            assertTrue(fence == null)
-            fence?.close()
-        }
-    }
-
-    @Test
-    fun testSyncStrategy_onFirstShow_FrontBufferUsageOn_Invisible() {
-        val egl = createAndSetupEGLManager(EGLSpec.V14)
-        if (egl.supportsNativeAndroidFence()) {
-            val strategy = FrontBufferSyncStrategy(mUsageFlags)
-            val fence = strategy.createSyncFence(egl.eglSpec)
-            assertTrue(fence != null)
-            fence?.close()
-        }
-    }
-
-    @Test
-    fun testSyncStrategy_onFirstShow_FrontBufferUsageOn_Visible() {
-        val egl = createAndSetupEGLManager(EGLSpec.V14)
-        if (egl.supportsNativeAndroidFence()) {
-            val strategy = FrontBufferSyncStrategy(mUsageFlags)
-            strategy.isVisible = true
-            val fence = strategy.createSyncFence(EGLSpec.V14)
-            assertTrue(fence == null)
-            fence?.close()
-        }
-    }
-
-    // Helper method to create and initialize an EGLManager
-    fun createAndSetupEGLManager(eglSpec: EGLSpec = EGLSpec.V14): EGLManager {
-        val egl = EGLManager(eglSpec)
-        Assert.assertEquals(EGLVersion.Unknown, egl.eglVersion)
-        Assert.assertEquals(EGL14.EGL_NO_CONTEXT, egl.eglContext)
-
-        egl.initialize()
-
-        val config = egl.loadConfig(EGLConfigAttributes.RGBA_8888)
-        if (config == null) {
-            Assert.fail("Config 888 should be supported")
-        }
-
-        egl.createContext(config!!)
-        return egl
-    }
-
-    // Helper method to release EGLManager
-    fun releaseEGLManager(egl: EGLManager) {
-        egl.release()
-        Assert.assertEquals(EGLVersion.Unknown, egl.eglVersion)
-        Assert.assertEquals(EGL14.EGL_NO_CONTEXT, egl.eglContext)
-    }
-}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/FrameBufferPoolTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/FrameBufferPoolTest.kt
new file mode 100644
index 0000000..95d473d
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/FrameBufferPoolTest.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.graphics.opengl
+
+import android.hardware.HardwareBuffer
+import android.os.Build
+import androidx.graphics.opengl.egl.EGLManager
+import androidx.graphics.opengl.egl.EGLSpec
+import androidx.graphics.opengl.egl.supportsNativeAndroidFence
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlin.concurrent.thread
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+internal class FrameBufferPoolTest {
+
+    @Test
+    fun testHardwareBufferMatchesConfig() {
+        withEGLSpec { eglSpec ->
+            val width = 2
+            val height = 3
+            val format = HardwareBuffer.RGB_565
+            val usage = HardwareBuffer.USAGE_GPU_COLOR_OUTPUT
+            val pool = createPool(
+                width,
+                height,
+                format,
+                usage
+            )
+            try {
+                val buffer = pool.obtain(eglSpec).hardwareBuffer
+                assertEquals(width, buffer.width)
+                assertEquals(height, buffer.height)
+                assertEquals(format, buffer.format)
+                assertEquals(usage, buffer.usage)
+            } finally {
+                pool.close()
+            }
+        }
+    }
+
+    @Test
+    fun testCloseReleasesFrameBuffer() {
+        withEGLSpec { egl ->
+            val pool = createPool()
+            val frameBuffer = pool.obtain(egl)
+            pool.release(frameBuffer)
+            pool.close()
+            assertTrue(frameBuffer.isClosed)
+        }
+    }
+
+    @Test
+    fun testAllocationAtMaxPoolSizeBlocks() {
+        withEGLSpec { egl ->
+            val poolSize = 2
+            val latch = CountDownLatch(1)
+            thread {
+                val pool = createPool(maxPoolSize = poolSize)
+                // Attempting to allocate 1 additional buffer than
+                // maximum specified pool size should block
+                repeat(poolSize + 1) {
+                    pool.obtain(egl)
+                }
+                latch.countDown()
+            }
+            assertFalse(latch.await(3, TimeUnit.SECONDS))
+        }
+    }
+
+    @Test
+    fun testReleaseAtMaxPoolSizeUnblocks() {
+        withEGLSpec { egl ->
+            val poolSize = 2
+            val latch = CountDownLatch(1)
+            val pool = createPool(maxPoolSize = poolSize)
+            val b1 = pool.obtain(egl)
+            pool.obtain(egl)
+            var b3: FrameBuffer? = null
+            thread {
+                b3 = pool.obtain(egl)
+                latch.countDown()
+            }
+            pool.release(b1)
+            assertTrue(latch.await(3, TimeUnit.SECONDS))
+            assertTrue(b1 === b3)
+        }
+    }
+
+    fun createPool(
+        width: Int = 2,
+        height: Int = 3,
+        format: Int = HardwareBuffer.RGB_565,
+        usage: Long = HardwareBuffer.USAGE_GPU_COLOR_OUTPUT,
+        maxPoolSize: Int = 2
+    ): FrameBufferPool =
+        FrameBufferPool(
+            width,
+            height,
+            format,
+            usage,
+            maxPoolSize
+        )
+
+    private fun withEGLSpec(
+        block: (egl: EGLSpec) -> Unit = {}
+    ) {
+        with(EGLManager()) {
+            initialize()
+            if (supportsNativeAndroidFence()) {
+                block(eglSpec)
+            }
+            release()
+        }
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
index 313ac2d..7fb8eb5 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
@@ -37,11 +37,9 @@
 import android.view.TextureView
 import androidx.annotation.RequiresApi
 import androidx.annotation.WorkerThread
-import androidx.graphics.lowlatency.FrameBufferRenderer
-import androidx.graphics.lowlatency.FrameBuffer
 import androidx.graphics.lowlatency.LineRenderer
 import androidx.graphics.lowlatency.Rectangle
-import androidx.graphics.lowlatency.SyncFenceCompat
+import androidx.hardware.SyncFenceCompat
 import androidx.graphics.opengl.egl.EGLManager
 import androidx.graphics.opengl.egl.EGLSpec
 import androidx.graphics.opengl.egl.supportsNativeAndroidFence
@@ -367,6 +365,18 @@
     }
 
     @Test
+    fun testExecute() {
+        val countDownLatch = CountDownLatch(1)
+        GLRenderer().apply {
+            start()
+            execute {
+                countDownLatch.countDown()
+            }
+        }
+        assertTrue(countDownLatch.await(3000, TimeUnit.MILLISECONDS))
+    }
+
+    @Test
     fun testNonStartedGLRendererIsNotRunning() {
         assertFalse(GLRenderer().isRunning())
     }
@@ -759,7 +769,7 @@
                         GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f)
                         GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
                         GLES20.glFlush()
-                        syncFenceCompat = SyncFenceCompat.createNativeSyncFence(egl)
+                        syncFenceCompat = SyncFenceCompat.createNativeSyncFence()
                         syncFenceCompat.await(TimeUnit.SECONDS.toNanos(3))
                     } finally {
                         syncFenceCompat?.close()
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncFenceCompatTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncFenceCompatTest.kt
deleted file mode 100644
index f1610d8..0000000
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncFenceCompatTest.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.graphics.opengl
-
-import android.opengl.EGL14
-import android.opengl.GLES20
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.graphics.lowlatency.SyncFenceCompat
-import androidx.graphics.opengl.egl.EGLConfigAttributes
-import androidx.graphics.opengl.egl.EGLManager
-import androidx.graphics.opengl.egl.EGLSpec
-import androidx.graphics.opengl.egl.EGLVersion
-import androidx.graphics.opengl.egl.supportsNativeAndroidFence
-import androidx.hardware.SyncFence
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
-import androidx.test.filters.SmallTest
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Assert.fail
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-@SmallTest
-@RequiresApi(Build.VERSION_CODES.O)
-class SyncFenceCompatTest {
-    @Test
-    fun testSyncFenceCompat_Create() {
-        testEglManager {
-            initializeWithDefaultConfig()
-            if (supportsNativeAndroidFence()) {
-                val syncFenceCompat = SyncFenceCompat.createNativeSyncFence(this.eglSpec)
-                assert(syncFenceCompat.isValid())
-                syncFenceCompat.close()
-            }
-        }
-    }
-
-    @Test
-    fun testSyncFenceCompat_Await() {
-        testEglManager {
-            initializeWithDefaultConfig()
-            if (supportsNativeAndroidFence()) {
-
-                val syncFenceCompat = SyncFenceCompat.createNativeSyncFence(this.eglSpec)
-                assert(syncFenceCompat.isValid())
-                GLES20.glFlush()
-                assertTrue(syncFenceCompat.await(1000))
-
-                syncFenceCompat.close()
-            }
-        }
-    }
-
-    @Test
-    fun testSyncFenceCompat_AwaitForever() {
-        testEglManager {
-            initializeWithDefaultConfig()
-            if (supportsNativeAndroidFence()) {
-                val syncFenceCompat = SyncFenceCompat.createNativeSyncFence(this.eglSpec)
-                assert(syncFenceCompat.isValid())
-                assertTrue(syncFenceCompat.awaitForever())
-
-                syncFenceCompat.close()
-            }
-        }
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    fun testSyncFenceCompat_SignalTime() {
-        testEglManager {
-            initializeWithDefaultConfig()
-            if (supportsNativeAndroidFence()) {
-                val start = System.nanoTime()
-                val syncFenceCompat = SyncFenceCompat.createNativeSyncFence(this.eglSpec)
-                assertTrue(syncFenceCompat.isValid())
-                assertTrue(syncFenceCompat.getSignalTimeNanos() != SyncFence.SIGNAL_TIME_INVALID)
-                assertTrue(syncFenceCompat.awaitForever())
-
-                assertTrue(syncFenceCompat.getSignalTimeNanos() > start)
-                assertTrue(
-                    syncFenceCompat.getSignalTimeNanos() !=
-                        SyncFenceCompat.SIGNAL_TIME_PENDING
-                )
-
-                syncFenceCompat.close()
-            }
-        }
-    }
-
-    // Helper method used in testing to initialize EGL and default
-    // EGLConfig to the ARGB8888 configuration
-    private fun EGLManager.initializeWithDefaultConfig() {
-        initialize()
-        val config = loadConfig(EGLConfigAttributes.RGBA_8888)
-        if (config == null) {
-            fail("Config 8888 should be supported")
-        }
-        createContext(config!!)
-    }
-
-    /**
-     * Helper method to ensure EglManager has the corresponding release calls
-     * made to it and verifies that no exceptions were thrown as part of the test.
-     */
-    private fun testEglManager(
-        eglSpec: EGLSpec = EGLSpec.V14,
-        block: EGLManager.() -> Unit = {}
-    ) {
-        with(EGLManager(eglSpec)) {
-            assertEquals(EGLVersion.Unknown, eglVersion)
-            assertEquals(EGL14.EGL_NO_CONTEXT, eglContext)
-            block()
-            release()
-            assertEquals(EGLVersion.Unknown, eglVersion)
-            assertEquals(EGL14.EGL_NO_CONTEXT, eglContext)
-        }
-    }
-}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncStrategyTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncStrategyTest.kt
new file mode 100644
index 0000000..f47e622
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncStrategyTest.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.graphics.opengl
+
+import android.opengl.EGL14
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.graphics.lowlatency.FrontBufferSyncStrategy
+import androidx.graphics.lowlatency.GLFrontBufferedRenderer
+import androidx.graphics.opengl.egl.EGLConfigAttributes
+import androidx.graphics.opengl.egl.EGLManager
+import androidx.graphics.opengl.egl.EGLSpec
+import androidx.graphics.opengl.egl.EGLVersion
+import androidx.graphics.opengl.egl.supportsNativeAndroidFence
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.Assert
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@RequiresApi(Build.VERSION_CODES.Q)
+class SyncStrategyTest {
+    private val mUsageFlags = GLFrontBufferedRenderer.obtainHardwareBufferUsageFlags()
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun testSyncStrategy_Always() {
+        val egl = createAndSetupEGLManager(EGLSpec.V14)
+        if (egl.supportsNativeAndroidFence()) {
+            val strategy = SyncStrategy.ALWAYS
+            val fence = strategy.createSyncFence(egl.eglSpec)
+            assertTrue(fence != null)
+            fence?.close()
+        }
+    }
+
+    @Test
+    fun testSyncStrategy_onFirstShow_FrontBufferUsageOff_Invisible() {
+        val egl = createAndSetupEGLManager(EGLSpec.V14)
+        if (egl.supportsNativeAndroidFence()) {
+            val strategy = FrontBufferSyncStrategy(0L)
+            val fence = strategy.createSyncFence(EGLSpec.V14)
+            assertTrue(fence != null)
+            fence?.close()
+        }
+    }
+
+    @Test
+    fun testSyncStrategy_onFirstShow_FrontBufferUsageOff_Visible() {
+        val egl = createAndSetupEGLManager(EGLSpec.V14)
+        if (egl.supportsNativeAndroidFence()) {
+            val strategy = FrontBufferSyncStrategy(0L)
+            strategy.isVisible = true
+            val fence = strategy.createSyncFence(EGLSpec.V14)
+            assertTrue(fence == null)
+            fence?.close()
+        }
+    }
+
+    @Test
+    fun testSyncStrategy_onFirstShow_FrontBufferUsageOn_Invisible() {
+        val egl = createAndSetupEGLManager(EGLSpec.V14)
+        if (egl.supportsNativeAndroidFence()) {
+            val strategy = FrontBufferSyncStrategy(mUsageFlags)
+            val fence = strategy.createSyncFence(egl.eglSpec)
+            assertTrue(fence != null)
+            fence?.close()
+        }
+    }
+
+    @Test
+    fun testSyncStrategy_onFirstShow_FrontBufferUsageOn_Visible() {
+        val egl = createAndSetupEGLManager(EGLSpec.V14)
+        if (egl.supportsNativeAndroidFence()) {
+            val strategy = FrontBufferSyncStrategy(mUsageFlags)
+            strategy.isVisible = true
+            val fence = strategy.createSyncFence(EGLSpec.V14)
+            assertTrue(fence == null)
+            fence?.close()
+        }
+    }
+
+    // Helper method to create and initialize an EGLManager
+    fun createAndSetupEGLManager(eglSpec: EGLSpec = EGLSpec.V14): EGLManager {
+        val egl = EGLManager(eglSpec)
+        Assert.assertEquals(EGLVersion.Unknown, egl.eglVersion)
+        Assert.assertEquals(EGL14.EGL_NO_CONTEXT, egl.eglContext)
+
+        egl.initialize()
+
+        val config = egl.loadConfig(EGLConfigAttributes.RGBA_8888)
+        if (config == null) {
+            Assert.fail("Config 888 should be supported")
+        }
+
+        egl.createContext(config!!)
+        return egl
+    }
+
+    // Helper method to release EGLManager
+    fun releaseEGLManager(egl: EGLManager) {
+        egl.release()
+        Assert.assertEquals(EGLVersion.Unknown, egl.eglVersion)
+        Assert.assertEquals(EGL14.EGL_NO_CONTEXT, egl.eglContext)
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
index 6e92aef..f7e5a2b 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
@@ -28,6 +28,7 @@
 import android.os.Build
 import android.view.Surface
 import androidx.annotation.RequiresApi
+import androidx.hardware.SyncFenceCompat
 import androidx.opengl.EGLBindings
 import androidx.opengl.EGLExt
 import androidx.opengl.EGLExt.Companion.EGL_ANDROID_CLIENT_BUFFER
@@ -57,6 +58,7 @@
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -219,6 +221,7 @@
         }
     }
 
+    @Ignore // b/266736718
     @Test
     fun testCreatePBufferSurface() {
         testEGLManager {
@@ -701,7 +704,8 @@
                 GLES20.glFlush()
                 assertEquals("glFlush failed", GLES20.GL_NO_ERROR, GLES20.glGetError())
 
-                val syncFence = eglSpec.eglDupNativeFenceFDANDROID(sync!!)
+                val display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
+                val syncFence = EGLExt.eglDupNativeFenceFDANDROID(display, sync!!)
                 assertTrue(syncFence.isValid())
                 assertTrue(syncFence.await(TimeUnit.MILLISECONDS.toNanos(3000)))
 
@@ -726,16 +730,17 @@
                 GLES20.glFlush()
                 assertEquals("glFlush failed", GLES20.GL_NO_ERROR, GLES20.glGetError())
 
-                val syncFence = eglSpec.eglDupNativeFenceFDANDROID(sync!!)
+                val display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
+                val syncFence = EGLExt.eglDupNativeFenceFDANDROID(display, sync!!)
                 assertTrue(syncFence.isValid())
-                assertNotEquals(SyncFence.SIGNAL_TIME_INVALID, syncFence.getSignalTime())
+                assertNotEquals(SyncFenceCompat.SIGNAL_TIME_INVALID, syncFence.getSignalTimeNanos())
                 assertTrue(syncFence.awaitForever())
 
                 assertTrue(eglSpec.eglDestroySyncKHR(sync))
                 assertEquals("eglDestroySyncKHR failed", EGL14.EGL_SUCCESS, EGL14.eglGetError())
                 syncFence.close()
                 assertFalse(syncFence.isValid())
-                assertEquals(SyncFence.SIGNAL_TIME_INVALID, syncFence.getSignalTime())
+                assertEquals(SyncFence.SIGNAL_TIME_INVALID, syncFence.getSignalTimeNanos())
             }
         }
     }
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 8c8ade1..b475f0b 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
@@ -24,7 +24,7 @@
 import android.os.Build
 import android.os.SystemClock
 import android.view.SurfaceHolder
-import androidx.graphics.lowlatency.SyncFenceCompat
+import androidx.hardware.SyncFenceCompat
 import androidx.graphics.opengl.egl.EGLConfigAttributes
 import androidx.graphics.opengl.egl.EGLManager
 import androidx.graphics.opengl.egl.EGLSpec
@@ -45,6 +45,7 @@
 import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -99,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()
@@ -458,7 +495,7 @@
                         assertNotNull(buffer)
 
                         val fence = if (manager.supportsNativeAndroidFence()) {
-                            SyncFenceCompat.createNativeSyncFence(manager.eglSpec)
+                            SyncFenceCompat.createNativeSyncFence()
                         } else {
                             null
                         }
@@ -532,8 +569,13 @@
     }
 
     @Test
+    @Ignore("b/262903415")
     @SdkSuppress(minSdkVersion = 29, maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun testTransactionSetBuffer_singleReleaseCallback() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         val releaseLatch = CountDownLatch(1)
         val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
             .moveToState(
@@ -591,9 +633,14 @@
         }
     }
 
+    @Ignore("b/262909049")
     @Test
     @SdkSuppress(minSdkVersion = 29, maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun testTransactionSetBuffer_multipleReleaseCallbacksAndOverwriteWithSingleSC() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         val releaseLatch = CountDownLatch(1)
         val releaseLatch2 = CountDownLatch(1)
         val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
@@ -735,6 +782,7 @@
         }
     }
 
+    @Ignore("b/262909049")
     @Test
     fun testTransactionSetBuffer_ReleaseCallbacksAndOverwriteWithMultipleSC() {
         val releaseLatch = CountDownLatch(1)
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceCompatTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceCompatTest.kt
new file mode 100644
index 0000000..af174be
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceCompatTest.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.hardware
+
+import android.opengl.EGL14
+import android.opengl.GLES20
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.graphics.opengl.egl.EGLConfigAttributes
+import androidx.graphics.opengl.egl.EGLManager
+import androidx.graphics.opengl.egl.EGLSpec
+import androidx.graphics.opengl.egl.EGLVersion
+import androidx.graphics.opengl.egl.supportsNativeAndroidFence
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@RequiresApi(Build.VERSION_CODES.O)
+class SyncFenceCompatTest {
+    @Test
+    fun testSyncFenceCompat_Create() {
+        testEglManager {
+            initializeWithDefaultConfig()
+            if (supportsNativeAndroidFence()) {
+                val syncFenceCompat = SyncFenceCompat.createNativeSyncFence()
+                assert(syncFenceCompat.isValid())
+                syncFenceCompat.close()
+            }
+        }
+    }
+
+    @Test
+    fun testSyncFenceCompat_Await() {
+        testEglManager {
+            initializeWithDefaultConfig()
+            if (supportsNativeAndroidFence()) {
+
+                val syncFenceCompat = SyncFenceCompat.createNativeSyncFence()
+                assert(syncFenceCompat.isValid())
+                GLES20.glFlush()
+                assertTrue(syncFenceCompat.await(1000))
+
+                syncFenceCompat.close()
+            }
+        }
+    }
+
+    @Test
+    fun testSyncFenceCompat_AwaitForever() {
+        testEglManager {
+            initializeWithDefaultConfig()
+            if (supportsNativeAndroidFence()) {
+                val syncFenceCompat = SyncFenceCompat.createNativeSyncFence()
+                assert(syncFenceCompat.isValid())
+                assertTrue(syncFenceCompat.awaitForever())
+
+                syncFenceCompat.close()
+            }
+        }
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    fun testSyncFenceCompat_SignalTime() {
+        testEglManager {
+            initializeWithDefaultConfig()
+            if (supportsNativeAndroidFence()) {
+                val start = System.nanoTime()
+                val syncFenceCompat = SyncFenceCompat.createNativeSyncFence()
+                assertTrue(syncFenceCompat.isValid())
+                assertTrue(syncFenceCompat.getSignalTimeNanos() !=
+                    SyncFenceCompat.SIGNAL_TIME_INVALID)
+                assertTrue(syncFenceCompat.awaitForever())
+
+                assertTrue(syncFenceCompat.getSignalTimeNanos() > start)
+                assertTrue(
+                    syncFenceCompat.getSignalTimeNanos() !=
+                        SyncFenceCompat.SIGNAL_TIME_PENDING
+                )
+
+                syncFenceCompat.close()
+            }
+        }
+    }
+
+    // Helper method used in testing to initialize EGL and default
+    // EGLConfig to the ARGB8888 configuration
+    private fun EGLManager.initializeWithDefaultConfig() {
+        initialize()
+        val config = loadConfig(EGLConfigAttributes.RGBA_8888)
+        if (config == null) {
+            fail("Config 8888 should be supported")
+        }
+        createContext(config!!)
+    }
+
+    /**
+     * Helper method to ensure EglManager has the corresponding release calls
+     * made to it and verifies that no exceptions were thrown as part of the test.
+     */
+    private fun testEglManager(
+        eglSpec: EGLSpec = EGLSpec.V14,
+        block: EGLManager.() -> Unit = {}
+    ) {
+        with(EGLManager(eglSpec)) {
+            assertEquals(EGLVersion.Unknown, eglVersion)
+            assertEquals(EGL14.EGL_NO_CONTEXT, eglContext)
+            block()
+            release()
+            assertEquals(EGLVersion.Unknown, eglVersion)
+            assertEquals(EGL14.EGL_NO_CONTEXT, eglContext)
+        }
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceTest.kt
deleted file mode 100644
index 3c8f421..0000000
--- a/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceTest.kt
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.hardware
-
-import android.os.Build
-import androidx.graphics.surface.JniBindings
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
-import androidx.test.filters.SmallTest
-import org.junit.Assert
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Assert.fail
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
-@SmallTest
-class SyncFenceTest {
-
-    @Test
-    fun testDupSyncFenceFd() {
-        val fileDescriptor = 7
-        val syncFence = SyncFence(7)
-        // If the file descriptor is valid dup'ing it should return a different fd
-        Assert.assertNotEquals(fileDescriptor, JniBindings.nDupFenceFd(syncFence))
-    }
-
-    @Test
-    fun testWaitMethodLink() {
-        try {
-            SyncFence(8).await(1000)
-        } catch (linkError: UnsatisfiedLinkError) {
-            fail("Unable to resolve wait method")
-        } catch (exception: Exception) {
-            // Ignore other exceptions
-        }
-    }
-
-    @Test
-    fun testDupSyncFenceFdWhenInvalid() {
-        // If the fence is invalid there should be no attempt to dup the fd it and -1
-        // should be returned
-        Assert.assertEquals(-1, JniBindings.nDupFenceFd(SyncFence(-1)))
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testSignalTimeInvalid() {
-        // Something other than -1 even though this is not technically a valid file descriptor
-        // the internal APIs should not crash and instead return SIGNAL_TIME_INVALID
-        // Because not all devices support the ability to create a native file descriptor from
-        // an EGLSync, create a validity check to ensure we can get more presubmit test coverage
-        Assert.assertEquals(
-            SyncFence.SIGNAL_TIME_INVALID,
-            SyncFence(7).getSignalTime()
-        )
-        Assert.assertEquals(
-            SyncFence.SIGNAL_TIME_INVALID,
-            SyncFence(-1).getSignalTime()
-        )
-    }
-
-    @Test
-    fun testIsValid() {
-        assertFalse(SyncFence(-1).isValid())
-        assertTrue(SyncFence(42).isValid())
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testResolveSyncFileInfo() {
-        assertTrue(SyncFenceBindings.nResolveSyncFileInfo())
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-    @Test
-    fun testResolveSyncFileInfoFree() {
-        assertTrue(SyncFenceBindings.nResolveSyncFileInfoFree())
-    }
-}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceV19Test.kt b/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceV19Test.kt
new file mode 100644
index 0000000..7bedd9b
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceV19Test.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.hardware
+
+import android.os.Build
+import androidx.graphics.surface.JniBindings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import org.junit.Assert
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+@SmallTest
+class SyncFenceV19Test {
+
+    @Test
+    fun testDupSyncFenceFd() {
+        val fileDescriptor = 7
+        val syncFence = SyncFenceV19(7)
+        // If the file descriptor is valid dup'ing it should return a different fd
+        Assert.assertNotEquals(fileDescriptor, JniBindings.nDupFenceFd(syncFence))
+    }
+
+    @Test
+    fun testWaitMethodLink() {
+        try {
+            SyncFenceV19(8).await(1000)
+        } catch (linkError: UnsatisfiedLinkError) {
+            fail("Unable to resolve wait method")
+        } catch (exception: Exception) {
+            // Ignore other exceptions
+        }
+    }
+
+    @Test
+    fun testDupSyncFenceFdWhenInvalid() {
+        // If the fence is invalid there should be no attempt to dup the fd it and -1
+        // should be returned
+        Assert.assertEquals(-1, JniBindings.nDupFenceFd(SyncFenceV19(-1)))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testSignalTimeInvalid() {
+        // Something other than -1 even though this is not technically a valid file descriptor
+        // the internal APIs should not crash and instead return SIGNAL_TIME_INVALID
+        // Because not all devices support the ability to create a native file descriptor from
+        // an EGLSync, create a validity check to ensure we can get more presubmit test coverage
+        Assert.assertEquals(
+            SyncFenceCompat.SIGNAL_TIME_INVALID,
+            SyncFenceV19(7).getSignalTimeNanos()
+        )
+        Assert.assertEquals(
+            SyncFenceCompat.SIGNAL_TIME_INVALID,
+            SyncFenceV19(-1).getSignalTimeNanos()
+        )
+    }
+
+    @Test
+    fun testIsValid() {
+        assertFalse(SyncFenceV19(-1).isValid())
+        assertTrue(SyncFenceV19(42).isValid())
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testResolveSyncFileInfo() {
+        assertTrue(SyncFenceBindings.nResolveSyncFileInfo())
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testResolveSyncFileInfoFree() {
+        assertTrue(SyncFenceBindings.nResolveSyncFileInfoFree())
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/cpp/graphics-core.cpp b/graphics/graphics-core/src/main/cpp/graphics-core.cpp
index 564ea65..d7c44a7 100644
--- a/graphics/graphics-core/src/main/cpp/graphics-core.cpp
+++ b/graphics/graphics-core/src/main/cpp/graphics-core.cpp
@@ -266,7 +266,7 @@
 
 void setupSyncFenceClassInfo(JNIEnv *env) {
     if (!gSyncFenceClassInfo.CLASS_INFO_INITIALIZED) {
-        jclass syncFenceClazz = env->FindClass("androidx/hardware/SyncFence");
+        jclass syncFenceClazz = env->FindClass("androidx/hardware/SyncFenceV19");
         gSyncFenceClassInfo.clazz = static_cast<jclass>(env->NewGlobalRef(syncFenceClazz));
         gSyncFenceClassInfo.dupeFileDescriptor =
                 env->GetMethodID(gSyncFenceClassInfo.clazz, "dupeFileDescriptor", "()I");
@@ -496,12 +496,12 @@
         },
         {
                 "nDupFenceFd",
-                "(Landroidx/hardware/SyncFence;)I",
+                "(Landroidx/hardware/SyncFenceV19;)I",
                 (void *) JniBindings_nDupFenceFd
         },
         {
                 "nSetBuffer",
-                "(JJLandroid/hardware/HardwareBuffer;Landroidx/hardware/SyncFence;)V",
+                "(JJLandroid/hardware/HardwareBuffer;Landroidx/hardware/SyncFenceV19;)V",
                 (void *) JniBindings_nSetBuffer
         },
         {
diff --git a/graphics/graphics-core/src/main/cpp/sync_fence.cpp b/graphics/graphics-core/src/main/cpp/sync_fence.cpp
index b0ddb2a..68d18e3 100644
--- a/graphics/graphics-core/src/main/cpp/sync_fence.cpp
+++ b/graphics/graphics-core/src/main/cpp/sync_fence.cpp
@@ -239,7 +239,7 @@
 };
 
 jint loadSyncFenceMethods(JNIEnv* env) {
-    jclass syncFenceClass = env->FindClass("androidx/hardware/SyncFence");
+    jclass syncFenceClass = env->FindClass("androidx/hardware/SyncFenceV19");
     if (syncFenceClass == nullptr) {
         return JNI_ERR;
     }
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/BufferInfo.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/BufferInfo.kt
new file mode 100644
index 0000000..862799f
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/BufferInfo.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.graphics.lowlatency
+
+import android.opengl.GLES20
+
+/**
+ * Class that represents information about the current buffer that is target for rendered output
+ *
+ * @param width Current width of the buffer taking pre-rotation into account.
+ * @param height Current height of the buffer taking pre-rotation into account
+ * @param frameBufferId Frame buffer object identifier. This is useful for retargeting rendering
+ * operations to the original destination after rendering to intermediate scratch buffers.
+ */
+class BufferInfo internal constructor(
+    width: Int = 0,
+    height: Int = 0,
+    frameBufferId: Int = -1
+) {
+
+    /**
+     * Width of the buffer that is being rendered into. This can be different than the corresponding
+     * dimensions specified as pre-rotation can occasionally swap width and height parameters in
+     * order to avoid GPU composition to rotate content. This should be used as input to
+     * [GLES20.glViewport].
+     */
+    var width: Int = width
+        internal set
+
+    /**
+     * Height of the buffer that is being rendered into. This can be different than the
+     * corresponding dimensions specified as pre-rotation can occasionally swap width and height
+     * parameters in order to avoid GPU composition to rotate content. This should be used as input
+     * to [GLES20.glViewport].
+     */
+    var height: Int = height
+        internal set
+
+    /**
+     * Identifier of the destination frame buffer object that is being rendered into. This is
+     * useful for re-binding to the original target after rendering to intermediate frame buffer
+     * objects.
+     */
+    var frameBufferId: Int = frameBufferId
+        internal set
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBuffer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBuffer.kt
deleted file mode 100644
index 4f238d0..0000000
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBuffer.kt
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.graphics.lowlatency
-
-import android.hardware.HardwareBuffer
-import android.opengl.GLES20
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.graphics.opengl.egl.EGLSpec
-import androidx.opengl.EGLExt
-import androidx.opengl.EGLImageKHR
-
-/**
- * Object that enables rendering into a [HardwareBuffer] by
- * creating a frame buffer object from it by leveraging Android
- * specific EGL extensions to create an [EGLImageKHR] object
- * that is loaded as a texture.
- *
- * @param egl [EGLSpec] used to specify EGL version and call various EGL methods
- * @property hardwareBuffer the [HardwareBuffer] that this class wraps and used to generate a
- * [EGLImageKHR] object
- */
-@RequiresApi(Build.VERSION_CODES.O)
-class FrameBuffer(
-    private val egl: EGLSpec,
-    val hardwareBuffer: HardwareBuffer,
-) : AutoCloseable {
-
-    private var eglImage: EGLImageKHR?
-    private var texture: Int = -1
-    private var frameBuffer: Int = -1
-
-    /**
-     * Boolean that tells if the frame buffer is currently closed
-     */
-    var isClosed = false
-        private set
-
-    // Int array used for creation of fbos/textures
-    private val buffer = IntArray(1)
-
-    init {
-        val image: EGLImageKHR = egl.eglCreateImageFromHardwareBuffer(hardwareBuffer)
-            ?: throw IllegalArgumentException("Unable to create EGLImage from HardwareBuffer")
-        eglImage = image
-
-        GLES20.glGenTextures(1, buffer, 0)
-        texture = buffer[0]
-
-        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture)
-        EGLExt.glEGLImageTargetTexture2DOES(GLES20.GL_TEXTURE_2D, image)
-
-        GLES20.glGenFramebuffers(1, buffer, 0)
-        frameBuffer = buffer[0]
-    }
-
-    /**
-     * Binds this frame buffer to the read and draw framebuffer targets if it's not closed.
-     * If the frame buffer is already closed this method will do nothing.
-     */
-    fun makeCurrent() {
-        if (!isClosed) {
-            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffer)
-            GLES20.glFramebufferTexture2D(
-                GLES20.GL_FRAMEBUFFER,
-                GLES20.GL_COLOR_ATTACHMENT0,
-                GLES20.GL_TEXTURE_2D,
-                texture,
-                0
-            )
-        }
-    }
-
-    /**
-     * Closes out the frame buffer, freeing all resources within it. This should be done only
-     * when the frame buffer is no longer needed or being accessed.
-     */
-    override fun close() {
-        buffer[0] = frameBuffer
-        GLES20.glDeleteBuffers(1, buffer, 0)
-        frameBuffer = -1
-
-        buffer[0] = texture
-        GLES20.glDeleteTextures(1, buffer, 0)
-        texture = -1
-
-        eglImage?.let { egl.eglDestroyImageKHR(it) }
-        eglImage = null
-        hardwareBuffer.close()
-        isClosed = true
-    }
-}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferPool.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferPool.kt
deleted file mode 100644
index 29acb84e..0000000
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferPool.kt
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.graphics.lowlatency
-
-import android.hardware.HardwareBuffer
-import android.os.Build
-import android.util.Log
-import androidx.annotation.RequiresApi
-import androidx.graphics.opengl.egl.EGLSpec
-import java.util.concurrent.locks.ReentrantLock
-import kotlin.concurrent.withLock
-
-/**
- * Allocation pool used for the creation and reuse of [FrameBuffer] instances.
- * This class is thread safe.
- */
-@RequiresApi(Build.VERSION_CODES.O)
-internal class FrameBufferPool(
-    /**
-     * Width of the [HardwareBuffer] objects to allocate if none are available in the pool
-     */
-    private val width: Int,
-
-    /**
-     * Height of the [HardwareBuffer] objects to allocate if none are available in the pool
-     */
-    private val height: Int,
-
-    /**
-     * Format of the [HardwareBuffer] objects to allocate if none are available in the pool
-     */
-    private val format: Int,
-
-    /**
-     * Usage hint flag of the [HardwareBuffer] objects to allocate if none are available in the pool
-     */
-    private val usage: Long,
-
-    /**
-     * Maximum size that the pool before additional requests to allocate buffers blocks until
-     * another [FrameBuffer] is released. Must be greater than 0.
-     */
-    private val maxPoolSize: Int
-) {
-
-    private val mPool = ArrayList<FrameBuffer>()
-    private var mNumAllocated = 0
-    private val mLock = ReentrantLock()
-    private val mCondition = mLock.newCondition()
-
-    init {
-        if (maxPoolSize <= 0) {
-            throw IllegalArgumentException("Pool size must be at least 1")
-        }
-    }
-
-    /**
-     * Obtains a [FrameBuffer] instance. This will either return a [FrameBuffer] if one is
-     * available within the pool, or creates a new [FrameBuffer] instance if the number of
-     * outstanding [FrameBuffer] instances is less than [maxPoolSize]
-     */
-    fun obtain(eglSpec: EGLSpec): FrameBuffer {
-        mLock.withLock {
-            while (mPool.isEmpty() && mNumAllocated >= maxPoolSize) {
-                Log.w(
-                    TAG,
-                    "Waiting for FrameBuffer to become available, current allocation " +
-                        "count: $mNumAllocated"
-                )
-                mCondition.await()
-            }
-            return if (mPool.isNotEmpty()) {
-                val frameBuffer = mPool[mPool.size - 1]
-                mPool.removeAt(mPool.size - 1)
-                frameBuffer
-            } else {
-                mNumAllocated++
-                FrameBuffer(
-                    eglSpec,
-                    HardwareBuffer.create(
-                        width,
-                        height,
-                        format,
-                        1,
-                        usage
-                    )
-                )
-            }
-        }
-    }
-
-    /**
-     * Releases the given [FrameBuffer] back to the pool and signals all
-     * consumers that are currently waiting for a buffer to become available
-     * via [FrameBufferPool.obtain]
-     * This method is thread safe.
-     */
-    fun release(frameBuffer: FrameBuffer) {
-        mLock.withLock {
-            mPool.add(frameBuffer)
-            mCondition.signal()
-        }
-    }
-
-    /**
-     * Invokes [FrameBuffer.close] on all [FrameBuffer] instances currently available within
-     * the pool and clears the pool.
-     * This method is thread safe.
-     */
-    fun close() {
-        mLock.withLock {
-            for (frameBuffer in mPool) {
-                frameBuffer.close()
-            }
-            mPool.clear()
-            mNumAllocated = 0
-        }
-    }
-
-    private companion object {
-        private const val TAG = "FrameBufferPool"
-    }
-}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferRenderer.kt
deleted file mode 100644
index bf68dd4..0000000
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferRenderer.kt
+++ /dev/null
@@ -1,237 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.graphics.lowlatency
-
-import android.annotation.SuppressLint
-import android.hardware.HardwareBuffer
-import android.opengl.EGLConfig
-import android.opengl.EGLSurface
-import android.opengl.GLES20
-import android.os.Build
-import android.util.Log
-import android.view.Surface
-import androidx.annotation.RequiresApi
-import androidx.graphics.opengl.GLRenderer
-import androidx.graphics.opengl.egl.EGLManager
-import androidx.graphics.opengl.egl.EGLSpec
-import androidx.opengl.EGLExt
-import java.util.concurrent.atomic.AtomicBoolean
-import androidx.opengl.EGLExt.Companion.EGL_ANDROID_NATIVE_FENCE_SYNC
-import androidx.opengl.EGLExt.Companion.EGL_KHR_FENCE_SYNC
-
-/**
- * [GLRenderer.RenderCallback] implementation that renders content into a frame buffer object
- * backed by a [HardwareBuffer] object
- */
-@RequiresApi(Build.VERSION_CODES.O)
-class FrameBufferRenderer(
-    private val frameBufferRendererCallbacks: RenderCallback,
-    @SuppressLint("ListenerLast") private val syncStrategy: SyncStrategy = SyncStrategy.ALWAYS
-) : GLRenderer.RenderCallback {
-
-    private val mClear = AtomicBoolean(false)
-
-    override fun onSurfaceCreated(
-        spec: EGLSpec,
-        config: EGLConfig,
-        surface: Surface,
-        width: Int,
-        height: Int
-    ): EGLSurface? = null
-
-    fun clear() {
-        mClear.set(true)
-    }
-
-    override fun onDrawFrame(eglManager: EGLManager) {
-        val egl = eglManager.eglSpec
-        val buffer = frameBufferRendererCallbacks.obtainFrameBuffer(egl)
-        var syncFenceCompat: SyncFenceCompat? = null
-        try {
-            buffer.makeCurrent()
-            if (mClear.getAndSet(false)) {
-                GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
-                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
-            } else {
-                frameBufferRendererCallbacks.onDraw(eglManager)
-            }
-
-            syncFenceCompat = if (eglManager.supportsNativeAndroidFence()) {
-                syncStrategy.createSyncFence(egl)
-            } else if (eglManager.isExtensionSupported(EGL_KHR_FENCE_SYNC)) {
-                // In this case the device only supports EGL sync objects but not creation
-                // of native SyncFence objects from an EGLSync.
-                // This usually occurs in emulator/cuttlefish instances as well as ChromeOS devices
-                // running ARC++. In this case fallback onto creating a sync object and waiting
-                // on it instead.
-                // TODO b/256217036 block on another thread instead of waiting here
-                val syncKhr = egl.eglCreateSyncKHR(EGLExt.EGL_SYNC_FENCE_KHR, null)
-                if (syncKhr != null) {
-                    GLES20.glFlush()
-                    val status = egl.eglClientWaitSyncKHR(
-                        syncKhr,
-                        EGLExt.EGL_SYNC_FLUSH_COMMANDS_BIT_KHR,
-                        EGLExt.EGL_FOREVER_KHR
-                    )
-                    if (status != EGLExt.EGL_CONDITION_SATISFIED_KHR) {
-                        Log.w(TAG, "warning waiting on sync object: $status")
-                    }
-                } else {
-                    Log.w(TAG, "Unable to create EGLSync")
-                    GLES20.glFinish()
-                }
-                null
-            } else {
-                Log.w(TAG, "Device does not support creation of any fences")
-                GLES20.glFinish()
-                null
-            }
-        } catch (exception: Exception) {
-            Log.w(TAG, "Error attempting to render to frame buffer: ${exception.message}")
-        } finally {
-            // At this point the HardwareBuffer has the contents of the GL rendering
-            // Create a surface Control transaction to dispatch this request
-            frameBufferRendererCallbacks.onDrawComplete(buffer, syncFenceCompat)
-        }
-    }
-
-    private fun EGLManager.supportsNativeAndroidFence(): Boolean =
-        isExtensionSupported(EGL_KHR_FENCE_SYNC) &&
-            isExtensionSupported(EGL_ANDROID_NATIVE_FENCE_SYNC)
-
-    /**
-     * Callbacks invoked to render content leveraging a [FrameBufferRenderer]
-     */
-    interface RenderCallback {
-
-        /**
-         * Obtain a [FrameBuffer] to render content into. The [FrameBuffer] obtained here
-         * is expected to be managed by the consumer of [FrameBufferRenderer]. That is
-         * callers of this API are expected to be maintaining a reference to the returned
-         * [FrameBuffer] here and calling [FrameBuffer.close] where appropriate as the instance
-         * will not be released by [FrameBufferRenderer].
-         *
-         * @param egl EGLSpec that is utilized within creation of the [FrameBuffer] object
-         */
-        @SuppressLint("CallbackMethodName")
-        fun obtainFrameBuffer(egl: EGLSpec): FrameBuffer
-
-        /**
-         * Draw contents into the [HardwareBuffer]
-         */
-        fun onDraw(eglManager: EGLManager)
-
-        /**
-         * Callback when [onDraw] is complete and the contents of the draw
-         * are reflected in the corresponding [HardwareBuffer].
-         *
-         * @param frameBuffer [FrameBuffer] that content is rendered into. The frameBuffer
-         * should not be consumed unless the syncFenceCompat is signalled or the fence is null.
-         * @param syncFenceCompat [SyncFenceCompat] is used to determine when rendering
-         * is done in [onDraw] and reflected within the given frameBuffer.
-         */
-        fun onDrawComplete(frameBuffer: FrameBuffer, syncFenceCompat: SyncFenceCompat?)
-    }
-
-    private companion object {
-        private const val TAG = "FrameBufferRenderer"
-    }
-}
-
-/**
- * A strategy class for deciding how to utilize [SyncFenceCompat] within
- * [FrameBufferRenderer.RenderCallback]. SyncStrategy provides default strategies for
- * usage:
- *
- * [SyncStrategy.ALWAYS] will always create a [SyncFenceCompat] to pass into the render
- * callbacks for [FrameBufferRenderer]
- */
-interface SyncStrategy {
-    /**
-     * Conditionally generates a [SyncFenceCompat] based upon implementation.
-     *
-     * @param eglSpec an [EGLSpec] object to dictate the version of EGL and make EGL calls.
-     */
-    fun createSyncFence(eglSpec: EGLSpec): SyncFenceCompat?
-
-    companion object {
-        /**
-         * [SyncStrategy] that will always create a [SyncFenceCompat] object
-         */
-        @JvmField
-        val ALWAYS = object : SyncStrategy {
-            override fun createSyncFence(eglSpec: EGLSpec): SyncFenceCompat? {
-                return eglSpec.createNativeSyncFence()
-            }
-        }
-    }
-}
-
-/**
- * [SyncStrategy] implementation that optimizes for front buffered rendering use cases.
- * More specifically this attempts to avoid unnecessary synchronization overhead
- * wherever possible.
- *
- * This will always provide a fence if the corresponding layer transitions from
- * an invisible to a visible state. If the layer is already visible and front
- * buffer usage flags are support on the device, then no fence is provided. If this
- * flag is not supported, then a fence is created to ensure contents
- * are flushed to the single buffer.
- *
- * @param usageFlags usage flags that describe the [HardwareBuffer] that is used as the destination
- * for rendering content within [FrameBufferRenderer]. The usage flags can be obtained via
- * [HardwareBuffer.getUsage] or by passing in the same flags from [HardwareBuffer.create]
- */
-class FrontBufferSyncStrategy(
-    usageFlags: Long
-) : SyncStrategy {
-    private val supportsFrontBufferUsage = (usageFlags and HardwareBuffer.USAGE_FRONT_BUFFER) != 0L
-    private var mFrontBufferVisible: Boolean = false
-
-    /**
-     * Tells whether the corresponding front buffer layer is visible in its current state or not.
-     * Utilize this to dictate when a [SyncFenceCompat] will be created when using
-     * [createSyncFence].
-     */
-    var isVisible
-        get() = mFrontBufferVisible
-        set(visibility) {
-            mFrontBufferVisible = visibility
-        }
-
-    /**
-     * Creates a [SyncFenceCompat] based on various conditions.
-     * If the layer is changing from invisible to visible, a fence is provided.
-     * If the layer is already visible and front buffer usage flag is supported on the device, then
-     * no fence is provided.
-     * If front buffer usage is not supported, then a fence is created and destroyed to flush
-     * contents to screen.
-     */
-    @RequiresApi(Build.VERSION_CODES.KITKAT)
-    override fun createSyncFence(eglSpec: EGLSpec): SyncFenceCompat? {
-        return if (!isVisible) {
-            eglSpec.createNativeSyncFence()
-        } else if (supportsFrontBufferUsage) {
-            GLES20.glFlush()
-            return null
-        } else {
-            val fence = eglSpec.createNativeSyncFence()
-            fence.close()
-            return null
-        }
-    }
-}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrontBufferSyncStrategy.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrontBufferSyncStrategy.kt
new file mode 100644
index 0000000..a7579b7
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrontBufferSyncStrategy.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.graphics.lowlatency
+
+import android.hardware.HardwareBuffer
+import android.opengl.GLES20
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.graphics.opengl.FrameBufferRenderer
+import androidx.graphics.opengl.SyncStrategy
+import androidx.graphics.opengl.egl.EGLSpec
+import androidx.hardware.SyncFenceCompat
+
+/**
+ * [SyncStrategy] implementation that optimizes for front buffered rendering use cases.
+ * More specifically this attempts to avoid unnecessary synchronization overhead
+ * wherever possible.
+ *
+ * This will always provide a fence if the corresponding layer transitions from
+ * an invisible to a visible state. If the layer is already visible and front
+ * buffer usage flags are support on the device, then no fence is provided. If this
+ * flag is not supported, then a fence is created to ensure contents
+ * are flushed to the single buffer.
+ *
+ * @param usageFlags usage flags that describe the [HardwareBuffer] that is used as the destination
+ * for rendering content within [FrameBufferRenderer]. The usage flags can be obtained via
+ * [HardwareBuffer.getUsage] or by passing in the same flags from [HardwareBuffer.create]
+ */
+class FrontBufferSyncStrategy(
+    usageFlags: Long
+) : SyncStrategy {
+    private val supportsFrontBufferUsage = (usageFlags and HardwareBuffer.USAGE_FRONT_BUFFER) != 0L
+    private var mFrontBufferVisible: Boolean = false
+
+    /**
+     * Tells whether the corresponding front buffer layer is visible in its current state or not.
+     * Utilize this to dictate when a [SyncFenceCompat] will be created when using
+     * [createSyncFence].
+     */
+    var isVisible
+        get() = mFrontBufferVisible
+        set(visibility) {
+            mFrontBufferVisible = visibility
+        }
+
+    /**
+     * Creates a [SyncFenceCompat] based on various conditions.
+     * If the layer is changing from invisible to visible, a fence is provided.
+     * If the layer is already visible and front buffer usage flag is supported on the device, then
+     * no fence is provided.
+     * If front buffer usage is not supported, then a fence is created and destroyed to flush
+     * contents to screen.
+     */
+    @RequiresApi(Build.VERSION_CODES.KITKAT)
+    override fun createSyncFence(eglSpec: EGLSpec): SyncFenceCompat? {
+        return if (!isVisible) {
+            SyncFenceCompat.createNativeSyncFence()
+        } else if (supportsFrontBufferUsage) {
+            GLES20.glFlush()
+            return null
+        } else {
+            val fence = SyncFenceCompat.createNativeSyncFence()
+            fence.close()
+            return null
+        }
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
index 59f43ce..829e606 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
@@ -25,10 +25,14 @@
 import android.view.SurfaceView
 import androidx.annotation.RequiresApi
 import androidx.annotation.WorkerThread
+import androidx.graphics.opengl.FrameBuffer
+import androidx.graphics.opengl.FrameBufferPool
+import androidx.graphics.opengl.FrameBufferRenderer
 import androidx.graphics.opengl.GLRenderer
 import androidx.graphics.opengl.egl.EGLManager
 import androidx.graphics.opengl.egl.EGLSpec
 import androidx.graphics.surface.SurfaceControlCompat
+import androidx.hardware.SyncFenceCompat
 import androidx.opengl.EGLExt.Companion.EGL_ANDROID_NATIVE_FENCE_SYNC
 import androidx.opengl.EGLExt.Companion.EGL_KHR_FENCE_SYNC
 import java.util.concurrent.ConcurrentLinkedQueue
@@ -167,6 +171,19 @@
     }
 
     /**
+     * Runnable executed on the GLThread to update [FrontBufferSyncStrategy.isVisible] as well
+     * as hide the SurfaceControl associated with the front buffered layer
+     */
+    private val mCancelRunnable = Runnable {
+        mFrontBufferSyncStrategy.isVisible = false
+        mFrontBufferedLayerSurfaceControl?.let { frontBufferSurfaceControl ->
+            SurfaceControlCompat.Transaction()
+                .setVisibility(frontBufferSurfaceControl, false)
+                .commit()
+        }
+    }
+
+    /**
      * Queue of parameters to be consumed in [Callback.onDrawFrontBufferedLayer] with the parameter
      * provided in [renderFrontBufferedLayer]
      */
@@ -376,6 +393,41 @@
     }
 
     /**
+     * Requests to render to the double buffered layer. This schedules a call to
+     * [Callback.onDrawDoubleBufferedLayer] with the parameters provided. If the front buffered
+     * layer is visible, this will hide this layer after rendering to the double buffered layer
+     * is complete. This is equivalent to calling [GLFrontBufferedRenderer.renderFrontBufferedLayer]
+     * for each parameter provided in the collection followed by a single call to
+     * [GLFrontBufferedRenderer.commit]. This is useful for re-rendering the double buffered
+     * scene when the corresponding Activity is being resumed from the background in which the
+     * contents should be re-drawn. Additionally this allows for applications to decide to
+     * dynamically render to either front or double buffered layers.
+     *
+     * If this [GLFrontBufferedRenderer] has been released, that is [isValid] returns 'false',
+     * this call is ignored.
+     *
+     * @param params Parameters that to be consumed when rendering to the double buffered layer.
+     * These parameters will be provided in the corresponding call to
+     * [Callback.onDrawDoubleBufferedLayer]
+     */
+    fun renderDoubleBufferedLayer(params: Collection<T>) {
+        if (isValid()) {
+            val segment = if (params is MutableCollection<T>) {
+                params
+            } else {
+                ArrayList<T>().apply { addAll(params) }
+            }
+            mSegments.add(segment)
+            mDoubleBufferedLayerRenderTarget?.requestRender()
+        } else {
+            Log.w(
+                TAG, "Attempt to render to the double buffered layer when " +
+                    "GLFrontBufferedRenderer has been released"
+            )
+        }
+    }
+
+    /**
      * Clears the contents of both the front and double buffered layers. This triggers a call to
      * [Callback.onDoubleBufferedLayerRenderComplete] and hides the front buffered layer.
      */
@@ -387,8 +439,8 @@
 
     /**
      * Requests to render the entire scene to the double buffered layer and schedules a call to
-     * [Callback.onDoubleBufferedLayerRenderComplete]. The parameters provided to
-     * [Callback.onDoubleBufferedLayerRenderComplete] will include each argument provided to every
+     * [Callback.onDrawDoubleBufferedLayer]. The parameters provided to
+     * [Callback.onDrawDoubleBufferedLayer] will include each argument provided to every
      * [renderFrontBufferedLayer] call since the last call to [commit] has been made.
      *
      * If this [GLFrontBufferedRenderer] has been released, that is [isValid] returns `false`,
@@ -407,6 +459,39 @@
     }
 
     /**
+     * Requests to cancel rendering and hides the front buffered layer.
+     * Unlike [commit], this does not schedule a call to render into the double buffered layer.
+     *
+     * If this [GLFrontBufferedRenderer] has been released, that is [isValid] returns `false`,
+     * this call is ignored.
+     */
+    fun cancel() {
+        if (isValid()) {
+            mActiveSegment.clear()
+            mGLRenderer.execute(mCancelRunnable)
+            mFrontBufferedLayerRenderer?.clear()
+        } else {
+            Log.w(TAG, "Attempt to cancel rendering to front buffer after " +
+                "GLFrontBufferedRenderer has been released")
+        }
+    }
+
+    /**
+     * Queue a [Runnable] to be executed on the GL rendering thread. Note it is important
+     * this [Runnable] does not block otherwise it can stall the GL thread.
+     *
+     * @param runnable to be executed
+     */
+    fun execute(runnable: Runnable) {
+        if (isValid()) {
+            mGLRenderer.execute(runnable)
+        } else {
+            Log.w(TAG, "Attempt to execute runnable after GLFrontBufferedRenderer has " +
+                "been released")
+        }
+    }
+
+    /**
      * Helper method used to detach the front and multi buffered render targets as well as
      * release SurfaceControl instances
      */
@@ -490,6 +575,7 @@
         bufferHeight: Int,
         usageFlags: Long
     ): FrameBufferRenderer {
+        val bufferInfo = BufferInfo()
         return FrameBufferRenderer(
             object : FrameBufferRenderer.RenderCallback {
                 private fun createFrontBufferLayer(usageFlags: Long): HardwareBuffer {
@@ -512,6 +598,7 @@
                             createFrontBufferLayer(usageFlags)
                         ).also {
                             mFrontLayerBuffer = it
+                            bufferInfo.frameBufferId = it.frameBuffer
                         }
                     }
                     return buffer
@@ -519,11 +606,14 @@
 
                 @WorkerThread
                 override fun onDraw(eglManager: EGLManager) {
+                    bufferInfo.apply {
+                        this.width = mParentRenderLayer.getBufferWidth()
+                        this.height = mParentRenderLayer.getBufferHeight()
+                    }
                     mActiveSegment.next { param ->
                         mCallback.onDrawFrontBufferedLayer(
                             eglManager,
-                            mParentRenderLayer.getBufferWidth(),
-                            mParentRenderLayer.getBufferHeight(),
+                            bufferInfo,
                             mParentRenderLayer.getTransform(),
                             param
                         )
@@ -626,16 +716,14 @@
          * parameters.
          * @param eglManager [EGLManager] useful in configuring EGL objects to be used when issuing
          * OpenGL commands to render into the front buffered layer
-         * @param bufferWidth Width of the buffer that is being rendered into. This can be different
-         * than the corresponding dimensions of the [SurfaceView] provided to the
-         * [GLFrontBufferedRenderer] as pre-rotation can occasionally swap width and height
-         * parameters in order to avoid GPU composition to rotate content. This should be used
-         * as input to [GLES20.glViewport].
-         * @param bufferHeight Height of the buffer that is being rendered into. This can be different
-         * than the corresponding dimensions of the [SurfaceView] provided to the
-         * [GLFrontBufferedRenderer] as pre-rotation can occasionally swap width and height
-         * parameters in order to avoid GPU composition to rotate content. This should be used as
-         * input to [GLES20.glViewport].
+         * @param bufferInfo [BufferInfo] about the buffer that is being rendered into. This
+         * includes the width and height of the buffer which can be different than the corresponding
+         * dimensions of the [SurfaceView] provided to the [GLFrontBufferedRenderer] as pre-rotation
+         * can occasionally swap width and height parameters in order to avoid GPU composition to
+         * rotate content. This should be used as input to [GLES20.glViewport].
+         * Additionally this also contains a frame buffer identifier that can be used to retarget
+         * rendering operations to the original destination after rendering into intermediate
+         * scratch buffers.
          * @param transform Matrix that should be applied to the rendering in this callback.
          * This should be consumed as input to any vertex shader implementations. Buffers are
          * pre-rotated in advance in order to avoid unnecessary overhead of GPU composition to
@@ -650,9 +738,9 @@
          *      myMatrix, // matrix
          *      0, // offset starting index into myMatrix
          *      0f, // left
-         *      bufferWidth.toFloat(), // right
+         *      bufferInfo.bufferWidth.toFloat(), // right
          *      0f, // bottom
-         *      bufferHeight.toFloat(), // top
+         *      bufferInfo.bufferHeight.toFloat(), // top
          *      -1f, // near
          *      1f, // far
          * )
@@ -666,8 +754,7 @@
         @WorkerThread
         fun onDrawFrontBufferedLayer(
             eglManager: EGLManager,
-            bufferWidth: Int,
-            bufferHeight: Int,
+            bufferInfo: BufferInfo,
             transform: FloatArray,
             param: T
         )
@@ -677,16 +764,14 @@
          * parameters.
          * @param eglManager [EGLManager] useful in configuring EGL objects to be used when issuing
          * OpenGL commands to render into the double buffered layer
-         * @param bufferWidth Width of the buffer that is being rendered into. This can be different
-         * than the corresponding dimensions of the [SurfaceView] provided to the
-         * [GLFrontBufferedRenderer] as pre-rotation can occasionally swap width and height
-         * parameters in order to avoid GPU composition to rotate content. This should be used
-         * as input to [GLES20.glViewport].
-         * @param bufferHeight Height of the buffer that is being rendered into. This can be different
-         * than the corresponding dimensions of the [SurfaceView] provided to the
-         * [GLFrontBufferedRenderer] as pre-rotation can occasionally swap width and height
-         * parameters in order to avoid GPU composition to rotate content. This should be used as
-         * input to [GLES20.glViewport].
+         * @param bufferInfo [BufferInfo] about the buffer that is being rendered into. This
+         * includes the width and height of the buffer which can be different than the corresponding
+         * dimensions of the [SurfaceView] provided to the [GLFrontBufferedRenderer] as pre-rotation
+         * can occasionally swap width and height parameters in order to avoid GPU composition to
+         * rotate content. This should be used as input to [GLES20.glViewport].
+         * Additionally this also contains a frame buffer identifier that can be used to retarget
+         * rendering operations to the original destination after rendering into intermediate
+         * scratch buffers.
          * @param transform Matrix that should be applied to the rendering in this callback.
          * This should be consumed as input to any vertex shader implementations. Buffers are
          * pre-rotated in advance in order to avoid unnecessary overhead of GPU composition to
@@ -701,9 +786,9 @@
          *      myMatrix, // matrix
          *      0, // offset starting index into myMatrix
          *      0f, // left
-         *      bufferWidth.toFloat(), // right
+         *      bufferInfo.bufferWidth.toFloat(), // right
          *      0f, // bottom
-         *      bufferHeight.toFloat(), // top
+         *      bufferInfo.bufferHeight.toFloat(), // top
          *      -1f, // near
          *      1f, // far
          * )
@@ -742,8 +827,7 @@
         @WorkerThread
         fun onDrawDoubleBufferedLayer(
             eglManager: EGLManager,
-            bufferWidth: Int,
-            bufferHeight: Int,
+            bufferInfo: BufferInfo,
             transform: FloatArray,
             params: Collection<T>
         )
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
index fbdaf57..b702a35 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
@@ -16,6 +16,7 @@
 
 package androidx.graphics.lowlatency
 
+import androidx.graphics.opengl.FrameBufferPool
 import androidx.graphics.opengl.GLRenderer
 import androidx.graphics.surface.SurfaceControlCompat
 
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..f82da69 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
@@ -22,10 +22,13 @@
 import android.view.SurfaceHolder
 import android.view.SurfaceView
 import androidx.annotation.RequiresApi
+import androidx.graphics.opengl.FrameBuffer
+import androidx.graphics.opengl.FrameBufferRenderer
 import androidx.graphics.opengl.GLRenderer
 import androidx.graphics.opengl.egl.EGLManager
 import androidx.graphics.opengl.egl.EGLSpec
 import androidx.graphics.surface.SurfaceControlCompat
+import androidx.hardware.SyncFenceCompat
 import java.util.Collections
 
 /**
@@ -64,8 +67,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 +93,9 @@
     }
 
     override fun setParent(builder: SurfaceControlCompat.Builder) {
-        builder.setParent(surfaceView)
+        mParentSurfaceControl?.let { parentSurfaceControl ->
+            builder.setParent(parentSurfaceControl)
+        }
     }
 
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
@@ -99,18 +104,25 @@
         renderLayerCallback: GLFrontBufferedRenderer.Callback<T>
     ): GLRenderer.RenderTarget {
         var params: Collection<T>? = null
+        val bufferInfo = BufferInfo()
         val frameBufferRenderer = FrameBufferRenderer(
             object : FrameBufferRenderer.RenderCallback {
 
-                override fun obtainFrameBuffer(egl: EGLSpec): FrameBuffer =
-                    mLayerCallback?.getFrameBufferPool()?.obtain(egl)
+                override fun obtainFrameBuffer(egl: EGLSpec): FrameBuffer {
+                    val frameBuffer = mLayerCallback?.getFrameBufferPool()?.obtain(egl)
                         ?: throw IllegalArgumentException("No FrameBufferPool available")
+                    bufferInfo.frameBufferId = frameBuffer.frameBuffer
+                    return frameBuffer
+                }
 
                 override fun onDraw(eglManager: EGLManager) {
+                    bufferInfo.apply {
+                        this.width = mBufferTransform.glWidth
+                        this.height = mBufferTransform.glHeight
+                    }
                     renderLayerCallback.onDrawDoubleBufferedLayer(
                         eglManager,
-                        mBufferTransform.glWidth,
-                        mBufferTransform.glHeight,
+                        bufferInfo,
                         mBufferTransform.transform,
                         params ?: Collections.emptyList()
                     )
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceCompat.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceCompat.kt
deleted file mode 100644
index 6f3bac4..0000000
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceCompat.kt
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.graphics.lowlatency
-
-import android.opengl.EGL14
-import android.opengl.EGL15
-import android.opengl.GLES20
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.graphics.opengl.egl.EGLSpec
-import androidx.hardware.SyncFence
-import androidx.opengl.EGLExt
-import androidx.opengl.EGLSyncKHR
-import androidx.graphics.surface.SurfaceControlCompat
-
-/**
- * A synchronization primitive which signals when hardware units have completed work on a
- * particular resource. They initially start in an unsignaled state and make a one-time
- * transaction to either a signaled or error state.
- *
- * [SyncFenceCompat] is a presentation fence used in combination with
- * [SurfaceControlCompat.Transaction.setBuffer]. Note that depending on API level, this will
- * utilize either [android.hardware.SyncFence] or [SyncFence].
- */
-@RequiresApi(Build.VERSION_CODES.KITKAT)
-class SyncFenceCompat : AutoCloseable {
-    internal val mImpl: SyncFenceImpl
-
-    companion object {
-        /**
-         * Creates a native synchronization fence from an EGLSync object.
-         *
-         * @param egl an [EGLSpec] object to dictate the version of EGL and make EGL calls.
-         *
-         * @throws IllegalArgumentException if sync object creation fails.
-         */
-        @JvmStatic
-        fun createNativeSyncFence(egl: EGLSpec): SyncFenceCompat {
-            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-                SyncFenceCompatVerificationHelper.createSyncFenceCompatV33()
-            } else {
-                val eglSync: EGLSyncKHR =
-                    egl.eglCreateSyncKHR(EGLExt.EGL_SYNC_NATIVE_FENCE_ANDROID, null)
-                        ?: throw IllegalArgumentException("Unable to create sync object")
-                GLES20.glFlush()
-                val syncFenceCompat = SyncFenceCompat(egl.eglDupNativeFenceFDANDROID(eglSync))
-                egl.eglDestroySyncKHR(eglSync)
-
-                syncFenceCompat
-            }
-        }
-
-        /**
-         * An invalid signal time. Represents either the signal time for a SyncFence that isn't
-         * valid (that is, [isValid] is `false`), or if an error occurred while attempting to
-         * retrieve the signal time.
-         */
-        const val SIGNAL_TIME_INVALID: Long = -1L
-
-        /**
-         * A pending signal time. This is equivalent to the max value of a long, representing an
-         * infinitely far point in the future.
-         */
-        const val SIGNAL_TIME_PENDING: Long = Long.MAX_VALUE
-    }
-
-    internal constructor(syncFence: SyncFence) {
-        mImpl = SyncFenceV19(syncFence)
-    }
-
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    internal constructor(syncFence: android.hardware.SyncFence) {
-        mImpl = SyncFenceV33(syncFence)
-    }
-
-    /**
-     * Waits for a [SyncFenceCompat] to signal for up to the timeout duration
-     *
-     * @param timeoutNanos time in nanoseconds to wait for before timing out.
-     */
-    fun await(timeoutNanos: Long): Boolean =
-        mImpl.await(timeoutNanos)
-
-    /**
-     * Waits forever for a [SyncFenceImpl] to signal
-     */
-    fun awaitForever(): Boolean =
-        mImpl.awaitForever()
-
-    /**
-     * Close the [SyncFenceImpl]
-     */
-    override fun close() {
-        mImpl.close()
-    }
-
-    /**
-     * Returns the time that the fence signaled in the [CLOCK_MONOTONIC] time domain.
-     * This returns an instant, [SyncFence.SIGNAL_TIME_INVALID] if the SyncFence is invalid, and
-     * if the fence hasn't yet signaled, then [SyncFence.SIGNAL_TIME_PENDING] is returned.
-     */
-    @RequiresApi(Build.VERSION_CODES.O)
-    fun getSignalTimeNanos(): Long {
-        return mImpl.getSignalTimeNanos()
-    }
-
-    /**
-     * Checks if the SyncFence object is valid.
-     * @return `true` if it is valid, `false` otherwise
-     */
-    fun isValid() = mImpl.isValid()
-}
-
-/**
- * Creates a native synchronization fence from an EGLSync object.
- *
- * @throws IllegalArgumentException if sync object creation fails.
- */
-@RequiresApi(Build.VERSION_CODES.KITKAT)
-@JvmSynthetic
-fun EGLSpec.createNativeSyncFence(): SyncFenceCompat = SyncFenceCompat.createNativeSyncFence(this)
-
-/**
- * Helper class to avoid class verification failures
- */
-@RequiresApi(Build.VERSION_CODES.TIRAMISU)
-internal class SyncFenceCompatVerificationHelper private constructor() {
-    companion object {
-        private val mEmptyAttributes = longArrayOf(EGL14.EGL_NONE.toLong())
-
-        @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-        @androidx.annotation.DoNotInline
-        fun createSyncFenceCompatV33(): SyncFenceCompat {
-            val display = EGL15.eglGetPlatformDisplay(
-                EGL15.EGL_PLATFORM_ANDROID_KHR,
-                EGL14.EGL_DEFAULT_DISPLAY.toLong(),
-                mEmptyAttributes,
-                0
-            )
-            if (display == EGL15.EGL_NO_DISPLAY) {
-                throw RuntimeException("no EGL display")
-            }
-            val error = EGL14.eglGetError()
-            if (error != EGL14.EGL_SUCCESS) {
-                throw RuntimeException("eglGetPlatformDisplay failed")
-            }
-
-            val eglSync = EGL15.eglCreateSync(
-                display,
-                android.opengl.EGLExt.EGL_SYNC_NATIVE_FENCE_ANDROID,
-                mEmptyAttributes,
-                0
-            )
-            GLES20.glFlush()
-            val syncFenceCompat = SyncFenceCompat(
-                android.opengl.EGLExt.eglDupNativeFenceFDANDROID(
-                    display,
-                    eglSync
-                )
-            )
-            EGL15.eglDestroySync(display, eglSync)
-
-            return syncFenceCompat
-        }
-    }
-}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceImpl.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceImpl.kt
deleted file mode 100644
index 27c0c25..0000000
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceImpl.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.graphics.lowlatency
-
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.hardware.SyncFence
-
-internal interface SyncFenceImpl {
-    /**
-     * Waits for a [SyncFenceImpl] to signal for up to the timeout duration
-     *
-     * @param timeoutNanos time in nanoseconds to wait for before timing out.
-     */
-    fun await(timeoutNanos: Long): Boolean
-
-    /**
-     * Waits forever for a [SyncFenceImpl] to signal
-     */
-    fun awaitForever(): Boolean
-
-    /**
-     * Close the [SyncFenceImpl]
-     */
-    fun close()
-
-    /**
-     * Returns the time that the fence signaled in the [CLOCK_MONOTONIC] time domain.
-     * This returns an instant, [SyncFence.SIGNAL_TIME_INVALID] if the SyncFence is invalid, and
-     * if the fence hasn't yet signaled, then [SyncFence.SIGNAL_TIME_PENDING] is returned.
-     */
-    @RequiresApi(Build.VERSION_CODES.O)
-    fun getSignalTimeNanos(): Long
-
-    /**
-     * Checks if the SyncFence object is valid.
-     * @return `true` if it is valid, `false` otherwise
-     */
-    fun isValid(): Boolean
-}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceV19.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceV19.kt
deleted file mode 100644
index 869ad67..0000000
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceV19.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.graphics.lowlatency
-
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.hardware.SyncFence
-
-@RequiresApi(Build.VERSION_CODES.KITKAT)
-internal class SyncFenceV19(syncFence: SyncFence) : SyncFenceImpl {
-    internal val mSyncFence: SyncFence = syncFence
-
-    /**
-     * See [SyncFenceImpl.await]
-     */
-    override fun await(timeoutNanos: Long): Boolean {
-        return mSyncFence.await(timeoutNanos)
-    }
-
-    /**
-     * See [SyncFenceImpl.awaitForever]
-     */
-    override fun awaitForever(): Boolean {
-        return mSyncFence.awaitForever()
-    }
-
-    /**
-     * See [SyncFenceImpl.close]
-     */
-    override fun close() {
-        mSyncFence.close()
-    }
-
-    /**
-     * See [SyncFenceImpl.getSignalTimeNanos]
-     */
-    @RequiresApi(Build.VERSION_CODES.O)
-    override fun getSignalTimeNanos(): Long {
-        return mSyncFence.getSignalTime()
-    }
-
-    /**
-     * See [SyncFenceImpl.isValid]
-     */
-    override fun isValid(): Boolean {
-        return mSyncFence.isValid()
-    }
-}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceV33.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceV33.kt
deleted file mode 100644
index bab7753..0000000
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceV33.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.graphics.lowlatency
-
-import android.hardware.SyncFence
-import android.os.Build
-import androidx.annotation.RequiresApi
-import java.time.Duration
-
-@RequiresApi(Build.VERSION_CODES.TIRAMISU)
-internal class SyncFenceV33 internal constructor(syncFence: SyncFence) : SyncFenceImpl {
-    internal val mSyncFence: SyncFence = syncFence
-
-    /**
-     * See [SyncFenceImpl.await]
-     */
-    override fun await(timeoutNanos: Long): Boolean {
-        return mSyncFence.await(Duration.ofNanos(timeoutNanos))
-    }
-
-    /**
-     * See [SyncFenceImpl.awaitForever]
-     */
-    override fun awaitForever(): Boolean {
-        return mSyncFence.awaitForever()
-    }
-
-    /**
-     * See [SyncFenceImpl.close]
-     */
-    override fun close() {
-        mSyncFence.close()
-    }
-
-    /**
-     * See [SyncFenceImpl.getSignalTimeNanos]
-     */
-    override fun getSignalTimeNanos(): Long {
-        return mSyncFence.signalTime
-    }
-
-    /**
-     * Checks if the SyncFence object is valid.
-     * @return `true` if it is valid, `false` otherwise
-     */
-    override fun isValid(): Boolean {
-        return mSyncFence.isValid
-    }
-}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/WrapperFrameBufferRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/WrapperFrameBufferRenderer.kt
index 005b06d..a80712b 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/WrapperFrameBufferRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/WrapperFrameBufferRenderer.kt
@@ -21,6 +21,7 @@
 import android.os.Build
 import android.view.Surface
 import androidx.annotation.RequiresApi
+import androidx.graphics.opengl.FrameBufferRenderer
 import androidx.graphics.opengl.GLRenderer
 import androidx.graphics.opengl.egl.EGLManager
 import androidx.graphics.opengl.egl.EGLSpec
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/FrameBuffer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/FrameBuffer.kt
new file mode 100644
index 0000000..cee697f
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/FrameBuffer.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.graphics.opengl
+
+import android.hardware.HardwareBuffer
+import android.opengl.GLES20
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.graphics.opengl.egl.EGLSpec
+import androidx.opengl.EGLExt
+import androidx.opengl.EGLImageKHR
+
+/**
+ * Object that enables rendering into a [HardwareBuffer] by
+ * creating a frame buffer object from it by leveraging Android
+ * specific EGL extensions to create an [EGLImageKHR] object
+ * that is loaded as a texture.
+ *
+ * @param egl [EGLSpec] used to specify EGL version and call various EGL methods
+ * @param hardwareBuffer the [HardwareBuffer] that this class wraps and used to generate a
+ * [EGLImageKHR] object
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+class FrameBuffer(
+    private val egl: EGLSpec,
+    val hardwareBuffer: HardwareBuffer,
+) : AutoCloseable {
+
+    private var eglImage: EGLImageKHR?
+    private var texture: Int = -1
+
+    /**
+     * Return the corresponding FrameBuffer identifier.
+     */
+    internal var frameBuffer: Int = -1
+        private set
+
+    /**
+     * Boolean that tells if the frame buffer is currently closed
+     */
+    var isClosed = false
+        private set
+
+    // Int array used for creation of fbos/textures
+    private val buffer = IntArray(1)
+
+    init {
+        val image: EGLImageKHR = egl.eglCreateImageFromHardwareBuffer(hardwareBuffer)
+            ?: throw IllegalArgumentException("Unable to create EGLImage from HardwareBuffer")
+        eglImage = image
+
+        GLES20.glGenTextures(1, buffer, 0)
+        texture = buffer[0]
+
+        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture)
+        EGLExt.glEGLImageTargetTexture2DOES(GLES20.GL_TEXTURE_2D, image)
+
+        GLES20.glGenFramebuffers(1, buffer, 0)
+        frameBuffer = buffer[0]
+    }
+
+    /**
+     * Binds this frame buffer to the read and draw framebuffer targets if it's not closed.
+     * If the frame buffer is already closed this method will do nothing.
+     */
+    fun makeCurrent() {
+        if (!isClosed) {
+            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffer)
+            GLES20.glFramebufferTexture2D(
+                GLES20.GL_FRAMEBUFFER,
+                GLES20.GL_COLOR_ATTACHMENT0,
+                GLES20.GL_TEXTURE_2D,
+                texture,
+                0
+            )
+        }
+    }
+
+    /**
+     * Closes out the frame buffer, freeing all resources within it. This should be done only
+     * when the frame buffer is no longer needed or being accessed.
+     */
+    override fun close() {
+        buffer[0] = frameBuffer
+        GLES20.glDeleteBuffers(1, buffer, 0)
+        frameBuffer = -1
+
+        buffer[0] = texture
+        GLES20.glDeleteTextures(1, buffer, 0)
+        texture = -1
+
+        eglImage?.let { egl.eglDestroyImageKHR(it) }
+        eglImage = null
+        hardwareBuffer.close()
+        isClosed = true
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/FrameBufferPool.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/FrameBufferPool.kt
new file mode 100644
index 0000000..17da756
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/FrameBufferPool.kt
@@ -0,0 +1,137 @@
+/*
+ * 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.graphics.opengl
+
+import android.hardware.HardwareBuffer
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.graphics.opengl.egl.EGLSpec
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/**
+ * Allocation pool used for the creation and reuse of [FrameBuffer] instances.
+ * This class is thread safe.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+internal class FrameBufferPool(
+    /**
+     * Width of the [HardwareBuffer] objects to allocate if none are available in the pool
+     */
+    private val width: Int,
+
+    /**
+     * Height of the [HardwareBuffer] objects to allocate if none are available in the pool
+     */
+    private val height: Int,
+
+    /**
+     * Format of the [HardwareBuffer] objects to allocate if none are available in the pool
+     */
+    private val format: Int,
+
+    /**
+     * Usage hint flag of the [HardwareBuffer] objects to allocate if none are available in the pool
+     */
+    private val usage: Long,
+
+    /**
+     * Maximum size that the pool before additional requests to allocate buffers blocks until
+     * another [FrameBuffer] is released. Must be greater than 0.
+     */
+    private val maxPoolSize: Int
+) {
+
+    private val mPool = ArrayList<FrameBuffer>()
+    private var mNumAllocated = 0
+    private val mLock = ReentrantLock()
+    private val mCondition = mLock.newCondition()
+
+    init {
+        if (maxPoolSize <= 0) {
+            throw IllegalArgumentException("Pool size must be at least 1")
+        }
+    }
+
+    /**
+     * Obtains a [FrameBuffer] instance. This will either return a [FrameBuffer] if one is
+     * available within the pool, or creates a new [FrameBuffer] instance if the number of
+     * outstanding [FrameBuffer] instances is less than [maxPoolSize]
+     */
+    fun obtain(eglSpec: EGLSpec): FrameBuffer {
+        mLock.withLock {
+            while (mPool.isEmpty() && mNumAllocated >= maxPoolSize) {
+                Log.w(
+                    TAG,
+                    "Waiting for FrameBuffer to become available, current allocation " +
+                        "count: $mNumAllocated"
+                )
+                mCondition.await()
+            }
+            return if (mPool.isNotEmpty()) {
+                val frameBuffer = mPool[mPool.size - 1]
+                mPool.removeAt(mPool.size - 1)
+                frameBuffer
+            } else {
+                mNumAllocated++
+                FrameBuffer(
+                    eglSpec,
+                    HardwareBuffer.create(
+                        width,
+                        height,
+                        format,
+                        1,
+                        usage
+                    )
+                )
+            }
+        }
+    }
+
+    /**
+     * Releases the given [FrameBuffer] back to the pool and signals all
+     * consumers that are currently waiting for a buffer to become available
+     * via [FrameBufferPool.obtain]
+     * This method is thread safe.
+     */
+    fun release(frameBuffer: FrameBuffer) {
+        mLock.withLock {
+            mPool.add(frameBuffer)
+            mCondition.signal()
+        }
+    }
+
+    /**
+     * Invokes [FrameBuffer.close] on all [FrameBuffer] instances currently available within
+     * the pool and clears the pool.
+     * This method is thread safe.
+     */
+    fun close() {
+        mLock.withLock {
+            for (frameBuffer in mPool) {
+                frameBuffer.close()
+            }
+            mPool.clear()
+            mNumAllocated = 0
+        }
+    }
+
+    private companion object {
+        private const val TAG = "FrameBufferPool"
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/FrameBufferRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/FrameBufferRenderer.kt
new file mode 100644
index 0000000..19a3ed5
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/FrameBufferRenderer.kt
@@ -0,0 +1,229 @@
+/*
+ * 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.graphics.opengl
+
+import android.annotation.SuppressLint
+import android.hardware.HardwareBuffer
+import android.opengl.EGLConfig
+import android.opengl.EGLSurface
+import android.opengl.GLES20
+import android.os.Build
+import android.util.Log
+import android.view.Surface
+import androidx.annotation.RequiresApi
+import androidx.hardware.SyncFenceCompat
+import androidx.graphics.opengl.egl.EGLManager
+import androidx.graphics.opengl.egl.EGLSpec
+import androidx.opengl.EGLExt
+import java.util.concurrent.atomic.AtomicBoolean
+import androidx.opengl.EGLExt.Companion.EGL_ANDROID_NATIVE_FENCE_SYNC
+import androidx.opengl.EGLExt.Companion.EGL_KHR_FENCE_SYNC
+
+/**
+ * [GLRenderer.RenderCallback] implementation that renders content into a frame buffer object
+ * backed by a [HardwareBuffer] object
+ *
+ * @param frameBufferRendererCallbacks Callbacks to provide a [FrameBuffer] instance to render into,
+ * draw method to render into the [FrameBuffer] as well as a subsequent callback to consume the
+ * contents of the [FrameBuffer]
+ * @param syncStrategy [SyncStrategy] used to determine when a fence is to be created to gate on
+ * consumption of the [FrameBuffer] instance. This determines if a [SyncFenceCompat] instance is
+ * provided in the [RenderCallback.onDrawComplete] depending on the use case.
+ * For example for front buffered rendering scenarios, it is possible that no [SyncFenceCompat] is
+ * provided in order to reduce latency within the rendering pipeline.
+ *
+ * This API can be used to render content into a [HardwareBuffer] directly and convert that to a
+ * bitmap with the following code snippet:
+ *
+ * ```
+ * val glRenderer = GLRenderer().apply { start() }
+ * val callbacks = object : FrameBufferRenderer.RenderCallback {
+ *
+ *   override fun obtainFrameBuffer(egl: EGLSpec): FrameBuffer =
+ *      FrameBuffer(
+ *          egl,
+ *          HardwareBuffer.create(
+ *              width,
+ *              height,
+ *              HardwareBuffer.RGBA_8888,
+ *              1,
+ *              HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
+ *          )
+ *      )
+ *
+ *   override fun onDraw(eglManager: EGLManager) {
+ *       // GL code
+ *   }
+ *
+ *   override fun onDrawComplete(frameBuffer: FrameBuffer, syncFenceCompat: SyncFenceCompat?) {
+ *       syncFenceCompat?.awaitForever()
+ *       val bitmap = Bitmap.wrapHardwareBuffer(frameBuffer.hardwareBuffer,
+ *              ColorSpace.get(ColorSpace.Named.LINEAR_SRGB))
+ *       // bitmap operations
+ *   }
+ * }
+ *
+ * glRenderer.createRenderTarget(width,height, FrameBufferRenderer(callbacks)).requestRender()
+ * ```
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+class FrameBufferRenderer(
+    private val frameBufferRendererCallbacks: RenderCallback,
+    @SuppressLint("ListenerLast") private val syncStrategy: SyncStrategy = SyncStrategy.ALWAYS
+) : GLRenderer.RenderCallback {
+
+    private val mClear = AtomicBoolean(false)
+
+    override fun onSurfaceCreated(
+        spec: EGLSpec,
+        config: EGLConfig,
+        surface: Surface,
+        width: Int,
+        height: Int
+    ): EGLSurface? = null
+
+    fun clear() {
+        mClear.set(true)
+    }
+
+    override fun onDrawFrame(eglManager: EGLManager) {
+        val egl = eglManager.eglSpec
+        val buffer = frameBufferRendererCallbacks.obtainFrameBuffer(egl)
+        var syncFenceCompat: SyncFenceCompat? = null
+        try {
+            buffer.makeCurrent()
+            if (mClear.getAndSet(false)) {
+                GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
+                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+            } else {
+                frameBufferRendererCallbacks.onDraw(eglManager)
+            }
+
+            syncFenceCompat = if (eglManager.supportsNativeAndroidFence()) {
+                syncStrategy.createSyncFence(egl)
+            } else if (eglManager.isExtensionSupported(EGL_KHR_FENCE_SYNC)) {
+                // In this case the device only supports EGL sync objects but not creation
+                // of native SyncFence objects from an EGLSync.
+                // This usually occurs in emulator/cuttlefish instances as well as ChromeOS devices
+                // running ARC++. In this case fallback onto creating a sync object and waiting
+                // on it instead.
+                // TODO b/256217036 block on another thread instead of waiting here
+                val syncKhr = egl.eglCreateSyncKHR(EGLExt.EGL_SYNC_FENCE_KHR, null)
+                if (syncKhr != null) {
+                    GLES20.glFlush()
+                    val status = egl.eglClientWaitSyncKHR(
+                        syncKhr,
+                        EGLExt.EGL_SYNC_FLUSH_COMMANDS_BIT_KHR,
+                        EGLExt.EGL_FOREVER_KHR
+                    )
+                    if (status != EGLExt.EGL_CONDITION_SATISFIED_KHR) {
+                        Log.w(TAG, "warning waiting on sync object: $status")
+                    }
+                } else {
+                    Log.w(TAG, "Unable to create EGLSync")
+                    GLES20.glFinish()
+                }
+                null
+            } else {
+                Log.w(TAG, "Device does not support creation of any fences")
+                GLES20.glFinish()
+                null
+            }
+        } catch (exception: Exception) {
+            Log.w(TAG, "Error attempting to render to frame buffer: ${exception.message}")
+        } finally {
+            // At this point the HardwareBuffer has the contents of the GL rendering
+            // Create a surface Control transaction to dispatch this request
+            frameBufferRendererCallbacks.onDrawComplete(buffer, syncFenceCompat)
+        }
+    }
+
+    private fun EGLManager.supportsNativeAndroidFence(): Boolean =
+        isExtensionSupported(EGL_KHR_FENCE_SYNC) &&
+            isExtensionSupported(EGL_ANDROID_NATIVE_FENCE_SYNC)
+
+    /**
+     * Callbacks invoked to render content leveraging a [FrameBufferRenderer]
+     */
+    interface RenderCallback {
+
+        /**
+         * Obtain a [FrameBuffer] to render content into. The [FrameBuffer] obtained here
+         * is expected to be managed by the consumer of [FrameBufferRenderer]. That is
+         * implementations of this API are expected to be maintaining a reference to the returned
+         * [FrameBuffer] here and calling [FrameBuffer.close] where appropriate as the instance
+         * will not be released by [FrameBufferRenderer].
+         *
+         * @param egl EGLSpec that is utilized within creation of the [FrameBuffer] object
+         */
+        @SuppressLint("CallbackMethodName")
+        fun obtainFrameBuffer(egl: EGLSpec): FrameBuffer
+
+        /**
+         * Draw contents into the [HardwareBuffer]. Before this method is invoked the [FrameBuffer]
+         * instance returned in [obtainFrameBuffer] is made current
+         */
+        fun onDraw(eglManager: EGLManager)
+
+        /**
+         * Callback when [onDraw] is complete and the contents of the draw
+         * are reflected in the corresponding [HardwareBuffer].
+         *
+         * @param frameBuffer [FrameBuffer] that content is rendered into. The frameBuffer
+         * should not be consumed unless the syncFenceCompat is signalled or the fence is null.
+         * This is the same [FrameBuffer] instance returned in [obtainFrameBuffer]
+         * @param syncFenceCompat [SyncFenceCompat] is used to determine when rendering
+         * is done in [onDraw] and reflected within the given frameBuffer.
+         */
+        fun onDrawComplete(frameBuffer: FrameBuffer, syncFenceCompat: SyncFenceCompat?)
+    }
+
+    private companion object {
+        private const val TAG = "FrameBufferRenderer"
+    }
+}
+
+/**
+ * A strategy class for deciding how to utilize [SyncFenceCompat] within
+ * [FrameBufferRenderer.RenderCallback]. SyncStrategy provides default strategies for
+ * usage:
+ *
+ * [SyncStrategy.ALWAYS] will always create a [SyncFenceCompat] to pass into the render
+ * callbacks for [FrameBufferRenderer]
+ */
+interface SyncStrategy {
+    /**
+     * Conditionally generates a [SyncFenceCompat] based upon implementation.
+     *
+     * @param eglSpec an [EGLSpec] object to dictate the version of EGL and make EGL calls.
+     */
+    @RequiresApi(Build.VERSION_CODES.KITKAT)
+    fun createSyncFence(eglSpec: EGLSpec): SyncFenceCompat?
+
+    companion object {
+        /**
+         * [SyncStrategy] that will always create a [SyncFenceCompat] object
+         */
+        @JvmField
+        val ALWAYS = object : SyncStrategy {
+            @RequiresApi(Build.VERSION_CODES.KITKAT)
+            override fun createSyncFence(eglSpec: EGLSpec): SyncFenceCompat? {
+                return SyncFenceCompat.createNativeSyncFence()
+            }
+        }
+    }
+}
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
index ed18a5b..dbd53cf 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
@@ -211,6 +211,16 @@
     }
 
     /**
+     * Queue a [Runnable] to be executed on the GL rendering thread. Note it is important that this
+     * [Runnable] does not block otherwise it can stall the GL thread.
+     *
+     * @param runnable Runnable to be executed
+     */
+    fun execute(runnable: Runnable) {
+        mGLThread?.execute(runnable)
+    }
+
+    /**
      * Stop the corresponding GL thread. This destroys all EGLSurfaces as well
      * as any other EGL dependencies. All queued requests that have not been processed
      * yet are cancelled.
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLThread.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLThread.kt
index acde030..d39a39f 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLThread.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLThread.kt
@@ -134,6 +134,9 @@
         }
     }
 
+    @AnyThread
+    fun execute(runnable: Runnable) = withHandler { post(runnable) }
+
     /**
      * Removes the corresponding [android.view.Surface] from management of the GLThread.
      * This destroys the EGLSurface associated with this surface and subsequent requests
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt
index 7efd824..53c24d27 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt
@@ -24,7 +24,6 @@
 import android.os.Build
 import android.view.Surface
 import androidx.annotation.RequiresApi
-import androidx.hardware.SyncFence
 import androidx.opengl.EGLExt
 import androidx.opengl.EGLExt.Companion.EGLClientWaitResult
 import androidx.opengl.EGLExt.Companion.EGLSyncAttribute
@@ -315,27 +314,6 @@
     fun eglDestroySyncKHR(sync: EGLSyncKHR): Boolean
 
     /**
-     * Creates a native synchronization fence referenced through a file descriptor
-     * that is associated with an EGL fence sync object.
-     *
-     * See:
-     * https://www.khronos.org/registry/EGL/extensions/ANDROID/EGL_ANDROID_native_fence_sync.txt
-     *
-     * @param sync The [EGLSyncKHR] to fetch the [SyncFence] from
-     * @return A [SyncFence] representing the native fence.
-     *  If [sync] is not a valid sync object for display, an invalid [SyncFence]
-     *  instance is returned and an EGL_BAD_PARAMETER error is generated.
-     *  If the EGL_SYNC_NATIVE_FENCE_FD_ANDROID attribute of [sync] is
-     *  EGL_NO_NATIVE_FENCE_FD_ANDROID, an invalid [SyncFence] is
-     *  returned and an EGL_BAD_PARAMETER error is generated.
-     *  If the display does not match the display passed to [eglCreateSyncKHR]
-     *  when [sync] was created, the behavior is undefined.
-     */
-    @Suppress("AcronymName")
-    @RequiresApi(Build.VERSION_CODES.KITKAT)
-    fun eglDupNativeFenceFDANDROID(sync: EGLSyncKHR): SyncFence
-
-    /**
      * Blocks the calling thread until the specified sync object is signalled or until
      * [timeoutNanos] nanoseconds have passed.
      * More than one [eglClientWaitSyncKHR] may be outstanding on the same [sync] at any given
@@ -533,10 +511,6 @@
 
             override fun eglGetError(): Int = EGL14.eglGetError()
 
-            @RequiresApi(Build.VERSION_CODES.KITKAT)
-            override fun eglDupNativeFenceFDANDROID(sync: EGLSyncKHR): SyncFence =
-                EGLExt.eglDupNativeFenceFDANDROID(getDefaultDisplay(), sync)
-
             override fun eglClientWaitSyncKHR(
                 sync: EGLSyncKHR,
                 flags: Int,
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..5b51639 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
@@ -26,7 +26,7 @@
 import android.view.SurfaceView
 import androidx.annotation.IntDef
 import androidx.annotation.RequiresApi
-import androidx.graphics.lowlatency.SyncFenceCompat
+import androidx.hardware.SyncFenceCompat
 import java.util.concurrent.Executor
 
 /**
@@ -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..ec56717 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
@@ -25,8 +25,8 @@
 import android.view.SurfaceControl
 import android.view.SurfaceView
 import androidx.annotation.RequiresApi
-import androidx.graphics.lowlatency.SyncFenceCompat
-import androidx.graphics.lowlatency.SyncFenceImpl
+import androidx.hardware.SyncFenceCompat
+import androidx.hardware.SyncFenceImpl
 import androidx.graphics.surface.SurfaceControlCompat.TransactionCommittedListener
 import java.util.concurrent.Executor
 
@@ -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..010d20c 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
@@ -24,11 +24,10 @@
 import android.view.SurfaceView
 import androidx.annotation.RequiresApi
 import androidx.graphics.lowlatency.BufferTransformHintResolver.Companion.UNKNOWN_TRANSFORM
-import androidx.graphics.lowlatency.SyncFenceImpl
-import androidx.graphics.lowlatency.SyncFenceV19
+import androidx.hardware.SyncFenceImpl
 import androidx.graphics.surface.SurfaceControlCompat.Companion.BUFFER_TRANSFORM_ROTATE_270
 import androidx.graphics.surface.SurfaceControlCompat.Companion.BUFFER_TRANSFORM_ROTATE_90
-import androidx.hardware.SyncFence
+import androidx.hardware.SyncFenceV19
 import java.util.concurrent.Executor
 
 /**
@@ -68,6 +67,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,16 +378,9 @@
             )
         }
 
-        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 =
+        private fun SyncFenceImpl.asSyncFenceCompat(): SyncFenceV19 =
             if (this is SyncFenceV19) {
-                mSyncFence
+                this
             } else {
                 throw IllegalArgumentException(
                     "Expected SyncFenceCompat implementation " +
@@ -388,4 +388,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..69e3f4a0 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
@@ -25,8 +25,8 @@
 import android.view.SurfaceControl
 import android.view.SurfaceView
 import androidx.annotation.RequiresApi
-import androidx.graphics.lowlatency.SyncFenceImpl
-import androidx.graphics.lowlatency.SyncFenceV33
+import androidx.hardware.SyncFenceImpl
+import androidx.hardware.SyncFenceV33
 import java.util.concurrent.Executor
 
 /**
@@ -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..6b48262 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
@@ -23,7 +23,7 @@
 import android.view.Surface
 import android.view.SurfaceControl
 import androidx.annotation.RequiresApi
-import androidx.hardware.SyncFence
+import androidx.hardware.SyncFenceV19
 import java.util.concurrent.Executor
 
 internal class JniBindings {
@@ -62,7 +62,7 @@
 
         @JvmStatic
         external fun nDupFenceFd(
-            syncFence: SyncFence
+            syncFence: SyncFenceV19
         ): Int
 
         @JvmStatic
@@ -70,7 +70,7 @@
             surfaceTransaction: Long,
             surfaceControl: Long,
             hardwareBuffer: HardwareBuffer,
-            acquireFieldFd: SyncFence
+            acquireFieldFd: SyncFenceV19
         )
 
         @JvmStatic
@@ -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.
      */
@@ -264,7 +268,7 @@
 
         /**
          * Updates the [HardwareBuffer] displayed for the provided surfaceControl. Takes an
-         * optional [SyncFence] that is signalled when all pending work for the buffer
+         * optional [SyncFenceV19] that is signalled when all pending work for the buffer
          * is complete and the buffer can be safely read.
          *
          * The frameworks takes ownership of the syncFence passed and is responsible for closing
@@ -285,7 +289,7 @@
         fun setBuffer(
             surfaceControl: SurfaceControlWrapper,
             hardwareBuffer: HardwareBuffer,
-            syncFence: SyncFence = SyncFence(-1)
+            syncFence: SyncFenceV19 = SyncFenceV19(-1)
         ): Transaction {
             JniBindings.nSetBuffer(
                 mNativeSurfaceTransaction,
@@ -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/graphics/graphics-core/src/main/java/androidx/hardware/SyncFence.kt b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFence.kt
deleted file mode 100644
index 4eac9f5..0000000
--- a/graphics/graphics-core/src/main/java/androidx/hardware/SyncFence.kt
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.hardware
-
-import android.os.Build
-import androidx.annotation.RequiresApi
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.locks.ReentrantLock
-import kotlin.concurrent.withLock
-
-/**
- * A SyncFence represents a synchronization primitive which signals when hardware buffers have
- * completed work on a particular resource.
- *
- * For example, GPU rendering to a frame buffer may generate a synchronization fence which signals
- * when rendering has completed.
- *
- * When the fence signals, then the backing storage for the framebuffer may be safely read from,
- * such as for display or media encoding.
- */
-@RequiresApi(Build.VERSION_CODES.KITKAT)
-class SyncFence(private var fd: Int) : AutoCloseable {
-
-    private val fenceLock = ReentrantLock()
-
-    /**
-     * Checks if the SyncFence object is valid.
-     * @return `true` if it is valid, `false` otherwise
-     */
-    fun isValid(): Boolean = fenceLock.withLock {
-        fd != -1
-    }
-
-    /**
-     * Returns the time that the fence signaled in the [CLOCK_MONOTONIC] time domain.
-     * This returns [SyncFence.SIGNAL_TIME_INVALID] if the SyncFence is invalid.
-     */
-    // Relies on NDK APIs sync_file_info/sync_file_info_free which were introduced in API level 26
-    @RequiresApi(Build.VERSION_CODES.O)
-    fun getSignalTime(): Long = fenceLock.withLock {
-        if (isValid()) {
-            nGetSignalTime(fd)
-        } else {
-            SIGNAL_TIME_INVALID
-        }
-    }
-
-    // Accessed through JNI to obtain the dup'ed file descriptor in a thread safe manner
-    private fun dupeFileDescriptor(): Int = fenceLock.withLock {
-        return if (isValid()) {
-            nDup(fd)
-        } else {
-            -1
-        }
-    }
-
-    /**
-     * Waits for a SyncFence to signal for up to the [timeoutNanos] duration. An invalid SyncFence,
-     * that is if [isValid] is `false`, is treated equivalently to a SyncFence that has already
-     * signaled. That is, wait() will immediately return `true`.
-     *
-     * @param timeoutNanos Timeout duration in nanoseconds. Providing a negative value will wait
-     * indefinitely until the fence is signaled
-     * @return `true` if the fence signaled or is not valid, `false` otherwise
-     */
-    fun await(timeoutNanos: Long): Boolean {
-        fenceLock.withLock {
-            if (isValid()) {
-                val timeout: Int
-                if (timeoutNanos < 0) {
-                    timeout = -1
-                } else {
-                    timeout = TimeUnit.NANOSECONDS.toMillis(timeoutNanos).toInt()
-                }
-                return nWait(fd, timeout)
-            } else {
-                // invalid file descriptors will always return true
-                return true
-            }
-        }
-    }
-
-    /**
-     * Waits forever for a SyncFence to signal. An invalid SyncFence is treated equivalently to a
-     * SyncFence that has already signaled. That is, wait() will immediately return `true`.
-     *
-     * @return `true` if the fence signaled or isn't valid, `false` otherwise
-     */
-    fun awaitForever(): Boolean = await(-1)
-
-    /**
-     * Close the SyncFence instance. After this method is invoked the fence is invalid. That
-     * is subsequent calls to [isValid] will return `false`
-     */
-    override fun close() {
-        fenceLock.withLock {
-            if (isValid()) {
-                nClose(fd)
-                fd = -1
-            }
-        }
-    }
-
-    protected fun finalize() {
-        close()
-    }
-
-    // SyncFence in the framework implements timeoutNanos as a long but
-    // it is casted down to an int within native code and eventually calls into
-    // the poll API which consumes a timeout in nanoseconds as an int.
-    private external fun nWait(fd: Int, timeoutMillis: Int): Boolean
-    private external fun nGetSignalTime(fd: Int): Long
-    private external fun nClose(fd: Int)
-
-    /**
-     * Dup the provided file descriptor, this method requires the caller to acquire the corresponding
-     * [fenceLock] before invoking
-     */
-    private external fun nDup(fd: Int): Int
-
-    companion object {
-
-        /**
-         * An invalid signal time. Represents either the signal time for a SyncFence that isn't
-         * valid (that is, [isValid] is `false`), or if an error occurred while attempting to
-         * retrieve the signal time.
-         */
-        const val SIGNAL_TIME_INVALID: Long = -1L
-
-        /**
-         * A pending signal time. This is equivalent to the max value of a long, representing an
-         * infinitely far point in the future.
-         */
-        const val SIGNAL_TIME_PENDING: Long = Long.MAX_VALUE
-
-        init {
-            System.loadLibrary("graphics-core")
-        }
-    }
-}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceCompat.kt b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceCompat.kt
new file mode 100644
index 0000000..5465eb8
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceCompat.kt
@@ -0,0 +1,170 @@
+/*
+ * 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.hardware
+
+import android.opengl.EGL14
+import android.opengl.EGL15
+import android.opengl.GLES20
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.opengl.EGLExt
+import androidx.graphics.surface.SurfaceControlCompat
+import androidx.opengl.EGLSyncKHR
+
+/**
+ * A synchronization primitive which signals when hardware units have completed work on a
+ * particular resource. They initially start in an unsignaled state and make a one-time
+ * transaction to either a signaled or error state.
+ *
+ * [SyncFenceCompat] is a presentation fence used in combination with
+ * [SurfaceControlCompat.Transaction.setBuffer]. Note that depending on API level, this will
+ * utilize either [android.hardware.SyncFence] or a compatibility implementation.
+ */
+@RequiresApi(Build.VERSION_CODES.KITKAT)
+class SyncFenceCompat : AutoCloseable {
+    internal val mImpl: SyncFenceImpl
+
+    companion object {
+        /**
+         * Creates a native synchronization fence from an EGLSync object.
+         *
+         * @throws IllegalStateException if EGL dependencies cannot be resolved
+         */
+        @JvmStatic
+        fun createNativeSyncFence(): SyncFenceCompat {
+            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                SyncFenceCompatVerificationHelper.createSyncFenceCompatV33()
+            } else {
+                val display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
+                    ?: throw IllegalStateException("No EGL Display available")
+                val eglSync: EGLSyncKHR =
+                    EGLExt.eglCreateSyncKHR(display, EGLExt.EGL_SYNC_NATIVE_FENCE_ANDROID, null)
+                        ?: throw IllegalStateException("Unable to create sync object")
+                GLES20.glFlush()
+
+                val syncFenceCompat = EGLExt.eglDupNativeFenceFDANDROID(display, eglSync)
+                EGLExt.eglDestroySyncKHR(display, eglSync)
+
+                syncFenceCompat
+            }
+        }
+
+        /**
+         * An invalid signal time. Represents either the signal time for a SyncFence that isn't
+         * valid (that is, [isValid] is `false`), or if an error occurred while attempting to
+         * retrieve the signal time.
+         */
+        const val SIGNAL_TIME_INVALID: Long = -1L
+
+        /**
+         * A pending signal time. This is equivalent to the max value of a long, representing an
+         * infinitely far point in the future.
+         */
+        const val SIGNAL_TIME_PENDING: Long = Long.MAX_VALUE
+    }
+
+    internal constructor(syncFence: SyncFenceV19) {
+        mImpl = syncFence
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    internal constructor(syncFence: android.hardware.SyncFence) {
+        mImpl = SyncFenceV33(syncFence)
+    }
+
+    /**
+     * Waits for a [SyncFenceCompat] to signal for up to the timeout duration
+     *
+     * @param timeoutNanos time in nanoseconds to wait for before timing out.
+     */
+    fun await(timeoutNanos: Long): Boolean =
+        mImpl.await(timeoutNanos)
+
+    /**
+     * Waits forever for a [SyncFenceImpl] to signal
+     */
+    fun awaitForever(): Boolean =
+        mImpl.awaitForever()
+
+    /**
+     * Close the [SyncFenceImpl]
+     */
+    override fun close() {
+        mImpl.close()
+    }
+
+    /**
+     * Returns the time that the fence signaled in the [CLOCK_MONOTONIC] time domain.
+     * This returns an instant, [SyncFenceCompat.SIGNAL_TIME_INVALID] if the SyncFence is invalid, and
+     * if the fence hasn't yet signaled, then [SyncFenceCompat.SIGNAL_TIME_PENDING] is returned.
+     */
+    @RequiresApi(Build.VERSION_CODES.O)
+    fun getSignalTimeNanos(): Long {
+        return mImpl.getSignalTimeNanos()
+    }
+
+    /**
+     * Checks if the SyncFence object is valid.
+     * @return `true` if it is valid, `false` otherwise
+     */
+    fun isValid() = mImpl.isValid()
+}
+
+/**
+ * Helper class to avoid class verification failures
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+internal class SyncFenceCompatVerificationHelper private constructor() {
+    companion object {
+        private val mEmptyAttributes = longArrayOf(EGL14.EGL_NONE.toLong())
+
+        @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+        @androidx.annotation.DoNotInline
+        fun createSyncFenceCompatV33(): SyncFenceCompat {
+            val display = EGL15.eglGetPlatformDisplay(
+                EGL15.EGL_PLATFORM_ANDROID_KHR,
+                EGL14.EGL_DEFAULT_DISPLAY.toLong(),
+                mEmptyAttributes,
+                0
+            )
+            if (display == EGL15.EGL_NO_DISPLAY) {
+                throw RuntimeException("no EGL display")
+            }
+            val error = EGL14.eglGetError()
+            if (error != EGL14.EGL_SUCCESS) {
+                throw RuntimeException("eglGetPlatformDisplay failed")
+            }
+
+            val eglSync = EGL15.eglCreateSync(
+                display,
+                android.opengl.EGLExt.EGL_SYNC_NATIVE_FENCE_ANDROID,
+                mEmptyAttributes,
+                0
+            )
+            GLES20.glFlush()
+            val syncFenceCompat = SyncFenceCompat(
+                android.opengl.EGLExt.eglDupNativeFenceFDANDROID(
+                    display,
+                    eglSync
+                )
+            )
+            EGL15.eglDestroySync(display, eglSync)
+
+            return syncFenceCompat
+        }
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceImpl.kt b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceImpl.kt
new file mode 100644
index 0000000..a460188
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceImpl.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.hardware
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+
+internal interface SyncFenceImpl {
+    /**
+     * Waits for a [SyncFenceImpl] to signal for up to the timeout duration
+     *
+     * @param timeoutNanos time in nanoseconds to wait for before timing out.
+     */
+    fun await(timeoutNanos: Long): Boolean
+
+    /**
+     * Waits forever for a [SyncFenceImpl] to signal
+     */
+    fun awaitForever(): Boolean
+
+    /**
+     * Close the [SyncFenceImpl]
+     */
+    fun close()
+
+    /**
+     * Returns the time that the fence signaled in the [CLOCK_MONOTONIC] time domain.
+     * This returns an instant, [SyncFenceCompat.SIGNAL_TIME_INVALID] if the SyncFence is invalid,
+     * and if the fence hasn't yet signaled, then [SyncFenceCompat.SIGNAL_TIME_PENDING] is returned.
+     */
+    @RequiresApi(Build.VERSION_CODES.O)
+    fun getSignalTimeNanos(): Long
+
+    /**
+     * Checks if the SyncFence object is valid.
+     * @return `true` if it is valid, `false` otherwise
+     */
+    fun isValid(): Boolean
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceV19.kt b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceV19.kt
new file mode 100644
index 0000000..108bb14
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceV19.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.hardware
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/**
+ * A SyncFence represents a synchronization primitive which signals when hardware buffers have
+ * completed work on a particular resource.
+ *
+ * For example, GPU rendering to a frame buffer may generate a synchronization fence which signals
+ * when rendering has completed.
+ *
+ * When the fence signals, then the backing storage for the framebuffer may be safely read from,
+ * such as for display or media encoding.
+ */
+@RequiresApi(Build.VERSION_CODES.KITKAT)
+internal class SyncFenceV19(private var fd: Int) : AutoCloseable, SyncFenceImpl {
+
+    private val fenceLock = ReentrantLock()
+
+    /**
+     * Checks if the SyncFence object is valid.
+     * @return `true` if it is valid, `false` otherwise
+     */
+    override fun isValid(): Boolean = fenceLock.withLock {
+        fd != -1
+    }
+
+    /**
+     * Returns the time that the fence signaled in the [CLOCK_MONOTONIC] time domain.
+     * This returns [SyncFenceCompat.SIGNAL_TIME_INVALID] if the SyncFence is invalid.
+     */
+    // Relies on NDK APIs sync_file_info/sync_file_info_free which were introduced in API level 26
+    @RequiresApi(Build.VERSION_CODES.O)
+    override fun getSignalTimeNanos(): Long = fenceLock.withLock {
+        if (isValid()) {
+            nGetSignalTime(fd)
+        } else {
+            SyncFenceCompat.SIGNAL_TIME_INVALID
+        }
+    }
+
+    // Accessed through JNI to obtain the dup'ed file descriptor in a thread safe manner
+    private fun dupeFileDescriptor(): Int = fenceLock.withLock {
+        return if (isValid()) {
+            nDup(fd)
+        } else {
+            -1
+        }
+    }
+
+    /**
+     * Waits for a SyncFence to signal for up to the [timeoutNanos] duration. An invalid SyncFence,
+     * that is if [isValid] is `false`, is treated equivalently to a SyncFence that has already
+     * signaled. That is, wait() will immediately return `true`.
+     *
+     * @param timeoutNanos Timeout duration in nanoseconds. Providing a negative value will wait
+     * indefinitely until the fence is signaled
+     * @return `true` if the fence signaled or is not valid, `false` otherwise
+     */
+    override fun await(timeoutNanos: Long): Boolean {
+        fenceLock.withLock {
+            if (isValid()) {
+                val timeout: Int
+                if (timeoutNanos < 0) {
+                    timeout = -1
+                } else {
+                    timeout = TimeUnit.NANOSECONDS.toMillis(timeoutNanos).toInt()
+                }
+                return nWait(fd, timeout)
+            } else {
+                // invalid file descriptors will always return true
+                return true
+            }
+        }
+    }
+
+    /**
+     * Waits forever for a SyncFence to signal. An invalid SyncFence is treated equivalently to a
+     * SyncFence that has already signaled. That is, wait() will immediately return `true`.
+     *
+     * @return `true` if the fence signaled or isn't valid, `false` otherwise
+     */
+    override fun awaitForever(): Boolean = await(-1)
+
+    /**
+     * Close the SyncFence instance. After this method is invoked the fence is invalid. That
+     * is subsequent calls to [isValid] will return `false`
+     */
+    override fun close() {
+        fenceLock.withLock {
+            if (isValid()) {
+                nClose(fd)
+                fd = -1
+            }
+        }
+    }
+
+    protected fun finalize() {
+        close()
+    }
+
+    // SyncFence in the framework implements timeoutNanos as a long but
+    // it is casted down to an int within native code and eventually calls into
+    // the poll API which consumes a timeout in nanoseconds as an int.
+    private external fun nWait(fd: Int, timeoutMillis: Int): Boolean
+    private external fun nGetSignalTime(fd: Int): Long
+    private external fun nClose(fd: Int)
+
+    /**
+     * Dup the provided file descriptor, this method requires the caller to acquire the corresponding
+     * [fenceLock] before invoking
+     */
+    private external fun nDup(fd: Int): Int
+
+    companion object {
+
+        init {
+            System.loadLibrary("graphics-core")
+        }
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceV33.kt b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceV33.kt
new file mode 100644
index 0000000..86bc660f
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceV33.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.hardware
+
+import android.hardware.SyncFence
+import android.os.Build
+import androidx.annotation.RequiresApi
+import java.time.Duration
+
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+internal class SyncFenceV33 internal constructor(syncFence: SyncFence) : SyncFenceImpl {
+    internal val mSyncFence: SyncFence = syncFence
+
+    /**
+     * See [SyncFenceImpl.await]
+     */
+    override fun await(timeoutNanos: Long): Boolean {
+        return mSyncFence.await(Duration.ofNanos(timeoutNanos))
+    }
+
+    /**
+     * See [SyncFenceImpl.awaitForever]
+     */
+    override fun awaitForever(): Boolean {
+        return mSyncFence.awaitForever()
+    }
+
+    /**
+     * See [SyncFenceImpl.close]
+     */
+    override fun close() {
+        mSyncFence.close()
+    }
+
+    /**
+     * See [SyncFenceImpl.getSignalTimeNanos]
+     */
+    override fun getSignalTimeNanos(): Long {
+        return mSyncFence.signalTime
+    }
+
+    /**
+     * Checks if the SyncFence object is valid.
+     * @return `true` if it is valid, `false` otherwise
+     */
+    override fun isValid(): Boolean {
+        return mSyncFence.isValid
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt b/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt
index c124d19..3d2962a 100644
--- a/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt
+++ b/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt
@@ -21,7 +21,8 @@
 import androidx.annotation.IntDef
 import androidx.annotation.RequiresApi
 import androidx.graphics.opengl.egl.EGLConfigAttributes
-import androidx.hardware.SyncFence
+import androidx.hardware.SyncFenceCompat
+import androidx.hardware.SyncFenceV19
 import androidx.opengl.EGLExt.Companion.eglCreateSyncKHR
 
 /**
@@ -561,12 +562,12 @@
          * https://www.khronos.org/registry/EGL/extensions/ANDROID/EGL_ANDROID_native_fence_sync.txt
          *
          * @param display The EGLDisplay connection
-         * @param sync The EGLSyncKHR to fetch the [SyncFence] from
-         * @return A [SyncFence] representing the native fence.
-         *  If [sync] is not a valid sync object for [display], an invalid [SyncFence]
+         * @param sync The EGLSyncKHR to fetch the [SyncFenceCompat] from
+         * @return A [SyncFenceCompat] representing the native fence.
+         *  If [sync] is not a valid sync object for [display], an invalid [SyncFenceCompat]
          *  instance is returned and an EGL_BAD_PARAMETER error is generated.
          *  If the EGL_SYNC_NATIVE_FENCE_FD_ANDROID attribute of [sync] is
-         *  EGL_NO_NATIVE_FENCE_FD_ANDROID, an invalid [SyncFence] is
+         *  EGL_NO_NATIVE_FENCE_FD_ANDROID, an invalid [SyncFenceCompat] is
          *  returned and an EGL_BAD_PARAMETER error is generated.
          *  If [display] does not match the display passed to [eglCreateSyncKHR]
          *  when [sync] was created, the behavior is undefined.
@@ -574,15 +575,18 @@
         @JvmStatic
         @Suppress("AcronymName")
         @RequiresApi(Build.VERSION_CODES.KITKAT)
-        fun eglDupNativeFenceFDANDROID(display: EGLDisplay, sync: EGLSyncKHR): SyncFence {
+        internal fun eglDupNativeFenceFDANDROID(
+            display: EGLDisplay,
+            sync: EGLSyncKHR
+        ): SyncFenceCompat {
             val fd = EGLBindings.nDupNativeFenceFDANDROID(
                 display.obtainNativeHandle(),
                 sync.nativeHandle
             )
             return if (fd >= 0) {
-                SyncFence(fd)
+                SyncFenceCompat(SyncFenceV19(fd))
             } else {
-                SyncFence(-1)
+                SyncFenceCompat(SyncFenceV19(-1))
             }
         }
 
diff --git a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/PolygonTest.kt b/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/PolygonTest.kt
index 379f501..69601cf 100644
--- a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/PolygonTest.kt
+++ b/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/PolygonTest.kt
@@ -21,7 +21,10 @@
 import androidx.core.graphics.plus
 import androidx.core.graphics.times
 import androidx.test.filters.SmallTest
-import org.junit.Assert
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertTrue
 import org.junit.Test
 
 @SmallTest
@@ -73,7 +76,7 @@
     fun pathTest() {
         val shape = square.toCubicShape()
         val path = shape.toPath()
-        Assert.assertFalse(path.isEmpty)
+        assertFalse(path.isEmpty)
     }
 
     @Test
@@ -95,7 +98,7 @@
         val squareCopy = Polygon(square)
         val identity = Matrix()
         square.transform(identity)
-        Assert.assertEquals(square, squareCopy)
+        assertEquals(square, squareCopy)
 
         // Now create a matrix which translates points by (1, 2) and make sure
         // the shape is translated similarly by it
@@ -112,4 +115,48 @@
             assertPointsEqualish(squareCopyCubics[i].p3 + offset, squareCubics[i].p3)
         }
     }
+
+    @Test
+    fun featuresTest() {
+        val squareFeatures = square.features
+
+        // Verify that cubics of polygon == cubics of features of that polygon
+        assertTrue(square.toCubicShape().cubics == squareFeatures.flatMap { it.cubics })
+
+        // Same as above but with rounded corners
+        val roundedSquare = RoundedPolygon(4, rounding = CornerRounding(.1f))
+        val roundedFeatures = roundedSquare.features
+        assertTrue(roundedSquare.toCubicShape().cubics == roundedFeatures.flatMap { it.cubics })
+
+        // Same as the first polygon test, but with a copy of that polygon
+        val squareCopy = Polygon(square)
+        val squareCopyFeatures = squareCopy.features
+        assertTrue(squareCopy.toCubicShape().cubics == squareCopyFeatures.flatMap { it.cubics })
+
+        // Test other elements of Features
+        val copy = Polygon(square)
+        val matrix = Matrix()
+        matrix.setTranslate(1f, 2f)
+        val features = copy.features
+        val preTransformVertices = mutableListOf<PointF>()
+        val preTransformCenters = mutableListOf<PointF>()
+        for (feature in features) {
+            if (feature is Corner) {
+                // Copy into new Point objects since the ones in the feature should transform
+                preTransformVertices.add(PointF(feature.vertex.x, feature.vertex.y))
+                preTransformCenters.add(PointF(feature.roundedCenter.x, feature.roundedCenter.y))
+            }
+        }
+        copy.transform(matrix)
+        val postTransformVertices = mutableListOf<PointF>()
+        val postTransformCenters = mutableListOf<PointF>()
+        for (feature in features) {
+            if (feature is Corner) {
+                postTransformVertices.add(feature.vertex)
+                postTransformCenters.add(feature.roundedCenter)
+            }
+        }
+        assertNotEquals(preTransformVertices, postTransformVertices)
+        assertNotEquals(preTransformCenters, postTransformCenters)
+    }
 }
\ No newline at end of file
diff --git a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Polygon.kt b/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Polygon.kt
index 24194e4..5129044 100644
--- a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Polygon.kt
+++ b/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Polygon.kt
@@ -181,11 +181,12 @@
         val tempFeatures = mutableListOf<Feature>()
         for (feature in source.features) {
             if (feature is Edge) {
-                tempFeatures.add(Edge(feature))
+                tempFeatures.add(Edge(this, feature))
             } else {
-                tempFeatures.add(Corner(feature as Corner))
+                tempFeatures.add(Corner(this, feature as Corner))
             }
         }
+        features = tempFeatures
         center = PointF(source.center.x, source.center.y)
         cubicShape.updateCubics(newCubics)
     }
@@ -257,11 +258,15 @@
         // from above, along with new cubics representing the edges between those corners.
         val tempFeatures = mutableListOf<Feature>()
         for (i in 0 until n) {
-            cubics.addAll(corners[i])
+            val cornerIndices = mutableListOf<Int>()
+            for (cubic in corners[i]) {
+                cornerIndices.add(cubics.size)
+                cubics.add(cubic)
+            }
             // TODO: determine and pass convexity flag
-            tempFeatures.add(Corner(corners[i], roundedCorners[i].center, vertices[i]))
+            tempFeatures.add(Corner(this, cornerIndices, roundedCorners[i].center, vertices[i]))
+            tempFeatures.add(Edge(this, listOf(cubics.size)))
             cubics.add(Cubic.straightLine(corners[i].last().p3, corners[(i + 1) % n].first().p0))
-            tempFeatures.add(Edge(listOf(cubics[cubics.size - 1])))
         }
         features = tempFeatures
         cubicShape.updateCubics(cubics)
@@ -276,6 +281,9 @@
         matrix.mapPoints(point)
         center.x = point[0]
         center.y = point[1]
+        for (feature in features) {
+            feature.transform(matrix)
+        }
     }
 
     /**
@@ -415,15 +423,27 @@
  * of what the shape actually is, rather than simply manipulating the raw curves and lines
  * which describe it.
  */
-internal sealed class Feature(val cubics: List<Cubic>)
+internal sealed class Feature(private val polygon: Polygon, protected val cubicIndices: List<Int>) {
+    val cubics: MutableList<Cubic>
+        get() {
+            val featureCubics = mutableListOf<Cubic>()
+            val cubics = polygon.toCubicShape().cubics
+            for (index in cubicIndices) {
+                featureCubics.add(cubics[index])
+            }
+            return featureCubics
+        }
+
+    open fun transform(matrix: Matrix) {}
+}
 
 /**
  * Edges have only a list of the cubic curves which make up the edge. Edges lie between
  * corners and have no vertex or concavity; the curves are simply straight lines (represented
  * by Cubic curves).
  */
-internal class Edge(cubics: List<Cubic>) : Feature(cubics) {
-    constructor(source: Edge) : this(source.cubics)
+internal class Edge(polygon: Polygon, indices: List<Int>) : Feature(polygon, indices) {
+    constructor(polygon: Polygon, source: Edge) : this(polygon, source.cubicIndices)
 }
 
 /**
@@ -434,18 +454,27 @@
  * convex (outer) and concave (inner) corners.
  */
 internal class Corner(
-    cubics: List<Cubic>,
+    polygon: Polygon,
+    cubicIndices: List<Int>,
     // TODO: parameters here should be immutable
     val vertex: PointF,
     val roundedCenter: PointF,
     val convex: Boolean = true
-) : Feature(cubics) {
-    constructor(source: Corner) : this(
-        source.cubics,
+) : Feature(polygon, cubicIndices) {
+    constructor(polygon: Polygon, source: Corner) : this(
+        polygon,
+        source.cubicIndices,
         source.vertex,
         source.roundedCenter,
         source.convex
     )
+
+    override fun transform(matrix: Matrix) {
+        val tempPoints = floatArrayOf(vertex.x, vertex.y, roundedCenter.x, roundedCenter.y)
+        matrix.mapPoints(tempPoints)
+        vertex.set(tempPoints[0], tempPoints[1])
+        roundedCenter.set(tempPoints[2], tempPoints[3])
+    }
 }
 
 /**
diff --git a/health/connect/connect-client-proto/src/main/proto/data.proto b/health/connect/connect-client-proto/src/main/proto/data.proto
index 2ccddb3..b5e10a0 100644
--- a/health/connect/connect-client-proto/src/main/proto/data.proto
+++ b/health/connect/connect-client-proto/src/main/proto/data.proto
@@ -50,6 +50,13 @@
   optional int64 instant_time_millis = 2;
 }
 
+message SubTypeDataValue {
+  map<string, Value> values = 1;
+  optional int64 start_time_millis = 2;
+  optional int64 end_time_millis = 3;
+}
+
+// Next Id: 22
 message DataPoint {
   optional DataType data_type = 1;
   map<string, Value> values = 2;
@@ -82,6 +89,12 @@
 
   optional int32 start_zone_offset_seconds = 19;
   optional int32 end_zone_offset_seconds = 20;
+
+  optional SubTypeDataValue sub_type_data_values = 21;
+
+  message SubTypeDataList {
+    repeated SubTypeDataValue values = 1;
+  }
 }
 
 message AggregatedValue {
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index 5a3840e..b0febf5 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -9,12 +9,12 @@
     method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
     method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
-    method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+    method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
     method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
     method public androidx.health.connect.client.PermissionController getPermissionController();
     method public suspend Object? insertRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.InsertRecordsResponse>);
     method public default static boolean isApiSupported();
-    method public default static boolean isProviderAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+    method public default static boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
     method public default static boolean isProviderAvailable(android.content.Context context);
     method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecord(kotlin.reflect.KClass<T> recordType, String recordId, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordResponse<T>>);
     method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecords(androidx.health.connect.client.request.ReadRecordsRequest<T> request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordsResponse<T>>);
@@ -24,10 +24,10 @@
   }
 
   public static final class HealthConnectClient.Companion {
-    method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+    method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
     method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
     method public boolean isApiSupported();
-    method public boolean isProviderAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+    method public boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
     method public boolean isProviderAvailable(android.content.Context context);
   }
 
diff --git a/health/connect/connect-client/api/public_plus_experimental_current.txt b/health/connect/connect-client/api/public_plus_experimental_current.txt
index 5a3840e..b0febf5 100644
--- a/health/connect/connect-client/api/public_plus_experimental_current.txt
+++ b/health/connect/connect-client/api/public_plus_experimental_current.txt
@@ -9,12 +9,12 @@
     method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
     method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
-    method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+    method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
     method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
     method public androidx.health.connect.client.PermissionController getPermissionController();
     method public suspend Object? insertRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.InsertRecordsResponse>);
     method public default static boolean isApiSupported();
-    method public default static boolean isProviderAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+    method public default static boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
     method public default static boolean isProviderAvailable(android.content.Context context);
     method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecord(kotlin.reflect.KClass<T> recordType, String recordId, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordResponse<T>>);
     method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecords(androidx.health.connect.client.request.ReadRecordsRequest<T> request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordsResponse<T>>);
@@ -24,10 +24,10 @@
   }
 
   public static final class HealthConnectClient.Companion {
-    method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+    method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
     method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
     method public boolean isApiSupported();
-    method public boolean isProviderAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+    method public boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
     method public boolean isProviderAvailable(android.content.Context context);
   }
 
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 519dbf0..3df3d9d 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -9,12 +9,12 @@
     method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
     method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
-    method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+    method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
     method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
     method public androidx.health.connect.client.PermissionController getPermissionController();
     method public suspend Object? insertRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.InsertRecordsResponse>);
     method public default static boolean isApiSupported();
-    method public default static boolean isProviderAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+    method public default static boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
     method public default static boolean isProviderAvailable(android.content.Context context);
     method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecord(kotlin.reflect.KClass<T> recordType, String recordId, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordResponse<T>>);
     method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecords(androidx.health.connect.client.request.ReadRecordsRequest<T> request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordsResponse<T>>);
@@ -24,10 +24,10 @@
   }
 
   public static final class HealthConnectClient.Companion {
-    method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+    method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
     method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
     method public boolean isApiSupported();
-    method public boolean isProviderAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+    method public boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
     method public boolean isProviderAvailable(android.content.Context context);
   }
 
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
index b466452..09ad6fa 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
@@ -359,6 +359,13 @@
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         internal const val DEFAULT_PROVIDER_PACKAGE_NAME = "com.google.android.apps.healthdata"
 
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        internal const val DEFAULT_PROVIDER_MIN_VERSION_CODE = 35000
+
+        @RestrictTo(RestrictTo.Scope.LIBRARY) // To be released after testing
+        const val HEALTH_CONNECT_SETTING_INTENT_ACTION =
+            "androidx.health.ACTION_HEALTH_CONNECT_SETTINGS"
+
         /**
          * Determines whether the current Health Connect SDK is supported on this device. If it is
          * not supported, then installing any provider will not help - instead disable the
@@ -378,27 +385,29 @@
          *
          * @sample androidx.health.connect.client.samples.AvailabilityCheckSamples
          *
-         * @param providerPackageNames optional package provider to choose implementation from
+         * @param context the context
+         * @param providerPackageName optional package provider to choose for backend implementation
          * @return whether the api is available
          */
         @JvmOverloads
         @JvmStatic
         public fun isProviderAvailable(
             context: Context,
-            providerPackageNames: List<String> = listOf(DEFAULT_PROVIDER_PACKAGE_NAME),
+            providerPackageName: String = DEFAULT_PROVIDER_PACKAGE_NAME,
         ): Boolean {
             if (!isApiSupported()) {
                 return false
             }
-            return providerPackageNames.any { isPackageInstalled(context.packageManager, it) }
+            return isPackageInstalled(context.packageManager, providerPackageName)
         }
 
         /**
          * Retrieves an IPC-backed [HealthConnectClient] instance binding to an available
          * implementation.
          *
-         * @param providerPackageNames optional alternative package provider to choose
-         * implementation from
+         * @param context the context
+         * @param providerPackageName optional alternative package provider to choose for backend
+         * implementation
          * @return instance of [HealthConnectClient] ready for issuing requests
          * @throws UnsupportedOperationException if service not available due to SDK version too low
          * @throws IllegalStateException if service not available due to not installed
@@ -409,19 +418,16 @@
         @JvmStatic
         public fun getOrCreate(
             context: Context,
-            providerPackageNames: List<String> = listOf(DEFAULT_PROVIDER_PACKAGE_NAME),
+            providerPackageName: String = DEFAULT_PROVIDER_PACKAGE_NAME,
         ): HealthConnectClient {
             if (!isApiSupported()) {
                 throw UnsupportedOperationException("SDK version too low")
             }
-            if (!isProviderAvailable(context, providerPackageNames)) {
+            if (!isProviderAvailable(context, providerPackageName)) {
                 throw IllegalStateException("Service not available")
             }
-            val enabledPackage =
-                providerPackageNames.first { isPackageInstalled(context.packageManager, it) }
             return HealthConnectClientImpl(
-                enabledPackage,
-                HealthDataService.getClient(context, enabledPackage)
+                HealthDataService.getClient(context, providerPackageName)
             )
         }
 
@@ -440,7 +446,9 @@
                     return false
                 }
             return packageInfo.applicationInfo.enabled &&
-                PackageInfoCompat.getLongVersionCode(packageInfo) > 35000 &&
+                (packageName != DEFAULT_PROVIDER_PACKAGE_NAME ||
+                    PackageInfoCompat.getLongVersionCode(packageInfo) >=
+                    DEFAULT_PROVIDER_MIN_VERSION_CODE) &&
                 hasBindableService(packageManager, packageName)
         }
 
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
index 4994ea5..6006964 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
@@ -64,7 +64,6 @@
  */
 class HealthConnectClientImpl
 internal constructor(
-    private val providerPackageName: String,
     private val delegate: HealthDataAsyncClient,
     private val allPermissions: List<String> =
         HealthPermission.RECORD_TYPE_TO_PERMISSION.flatMap {
diff --git a/health/connect/connect-client/src/main/java/androidx/health/platform/client/SdkConfig.java b/health/connect/connect-client/src/main/java/androidx/health/platform/client/SdkConfig.java
index 92456f9..7629a30 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/platform/client/SdkConfig.java
+++ b/health/connect/connect-client/src/main/java/androidx/health/platform/client/SdkConfig.java
@@ -25,7 +25,7 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 public final class SdkConfig {
     // should be increased everytime a new SDK is released
-    public static final int SDK_VERSION = 9;
+    public static final int SDK_VERSION = 11;
 
     private SdkConfig() {}
 }
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/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
index 828d890..1ea349b 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
@@ -51,10 +51,10 @@
         val packageManager = context.packageManager
         Shadows.shadowOf(packageManager).removePackage(PROVIDER_PACKAGE_NAME)
         assertThat(HealthConnectClient.isApiSupported()).isTrue()
-        assertThat(HealthConnectClient.isProviderAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
+        assertThat(HealthConnectClient.isProviderAvailable(context, PROVIDER_PACKAGE_NAME))
             .isFalse()
         assertThrows(IllegalStateException::class.java) {
-            HealthConnectClient.getOrCreate(context, listOf(PROVIDER_PACKAGE_NAME))
+            HealthConnectClient.getOrCreate(context, PROVIDER_PACKAGE_NAME)
         }
     }
 
@@ -62,10 +62,10 @@
     @Config(sdk = [Build.VERSION_CODES.P])
     fun backingImplementation_notEnabled_unavailable() {
         installPackage(context, PROVIDER_PACKAGE_NAME, versionCode = 35001, enabled = false)
-        assertThat(HealthConnectClient.isProviderAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
+        assertThat(HealthConnectClient.isProviderAvailable(context, PROVIDER_PACKAGE_NAME))
             .isFalse()
         assertThrows(IllegalStateException::class.java) {
-            HealthConnectClient.getOrCreate(context, listOf(PROVIDER_PACKAGE_NAME))
+            HealthConnectClient.getOrCreate(context, PROVIDER_PACKAGE_NAME)
         }
     }
 
@@ -73,42 +73,51 @@
     @Config(sdk = [Build.VERSION_CODES.P])
     fun backingImplementation_enabledNoService_unavailable() {
         installPackage(context, PROVIDER_PACKAGE_NAME, versionCode = 35001, enabled = true)
-        assertThat(HealthConnectClient.isProviderAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
+        assertThat(HealthConnectClient.isProviderAvailable(context, PROVIDER_PACKAGE_NAME))
             .isFalse()
         assertThrows(IllegalStateException::class.java) {
-            HealthConnectClient.getOrCreate(context, listOf(PROVIDER_PACKAGE_NAME))
+            HealthConnectClient.getOrCreate(context, PROVIDER_PACKAGE_NAME)
         }
     }
 
     @Test
     @Config(sdk = [Build.VERSION_CODES.P])
     fun backingImplementation_enabledUnsupportedVersion_unavailable() {
-        installPackage(context, PROVIDER_PACKAGE_NAME, versionCode = 35000, enabled = true)
-        assertThat(HealthConnectClient.isProviderAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
-            .isFalse()
+        installPackage(
+            context,
+            HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME,
+            versionCode = HealthConnectClient.DEFAULT_PROVIDER_MIN_VERSION_CODE - 1,
+            enabled = true)
+        installService(context, HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME)
+
+        assertThat(HealthConnectClient.isProviderAvailable(context)).isFalse()
         assertThrows(IllegalStateException::class.java) {
-            HealthConnectClient.getOrCreate(context, listOf(PROVIDER_PACKAGE_NAME))
+            HealthConnectClient.getOrCreate(context)
         }
     }
 
     @Test
     @Config(sdk = [Build.VERSION_CODES.P])
     fun backingImplementation_enabledSupportedVersion_isAvailable() {
-        installPackage(context, PROVIDER_PACKAGE_NAME, versionCode = 35001, enabled = true)
-        installService(context, PROVIDER_PACKAGE_NAME)
-        assertThat(HealthConnectClient.isProviderAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
-            .isTrue()
-        HealthConnectClient.getOrCreate(context, listOf(PROVIDER_PACKAGE_NAME))
+        installPackage(
+            context,
+            HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME,
+            versionCode = HealthConnectClient.DEFAULT_PROVIDER_MIN_VERSION_CODE,
+            enabled = true)
+        installService(context, HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME)
+
+        assertThat(HealthConnectClient.isProviderAvailable(context)).isTrue()
+        HealthConnectClient.getOrCreate(context)
     }
 
     @Test
     @Config(sdk = [Build.VERSION_CODES.O_MR1])
     fun sdkVersionTooOld_unavailable() {
         assertThat(HealthConnectClient.isApiSupported()).isFalse()
-        assertThat(HealthConnectClient.isProviderAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
+        assertThat(HealthConnectClient.isProviderAvailable(context, PROVIDER_PACKAGE_NAME))
             .isFalse()
         assertThrows(UnsupportedOperationException::class.java) {
-            HealthConnectClient.getOrCreate(context, listOf(PROVIDER_PACKAGE_NAME))
+            HealthConnectClient.getOrCreate(context, PROVIDER_PACKAGE_NAME)
         }
     }
 
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
index 61f0915..2fc02d2 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
@@ -143,7 +143,6 @@
 
         healthConnectClient =
             HealthConnectClientImpl(
-                PROVIDER_PACKAGE_NAME,
                 ServiceBackedHealthDataClient(
                     ApplicationProvider.getApplicationContext(),
                     clientConfig,
diff --git a/hilt/hilt-navigation-compose/api/current.txt b/hilt/hilt-navigation-compose/api/current.txt
index ff89117..99cdd72 100644
--- a/hilt/hilt-navigation-compose/api/current.txt
+++ b/hilt/hilt-navigation-compose/api/current.txt
@@ -2,7 +2,7 @@
 package androidx.hilt.navigation.compose {
 
   public final class HiltViewModelKt {
-    method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM hiltViewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key);
+    method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM hiltViewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key);
   }
 
 }
diff --git a/hilt/hilt-navigation-compose/api/public_plus_experimental_current.txt b/hilt/hilt-navigation-compose/api/public_plus_experimental_current.txt
index ff89117..99cdd72 100644
--- a/hilt/hilt-navigation-compose/api/public_plus_experimental_current.txt
+++ b/hilt/hilt-navigation-compose/api/public_plus_experimental_current.txt
@@ -2,7 +2,7 @@
 package androidx.hilt.navigation.compose {
 
   public final class HiltViewModelKt {
-    method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM hiltViewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key);
+    method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM hiltViewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key);
   }
 
 }
diff --git a/hilt/hilt-navigation-compose/api/restricted_current.txt b/hilt/hilt-navigation-compose/api/restricted_current.txt
index 3bf1a26..40d2a82 100644
--- a/hilt/hilt-navigation-compose/api/restricted_current.txt
+++ b/hilt/hilt-navigation-compose/api/restricted_current.txt
@@ -3,7 +3,7 @@
 
   public final class HiltViewModelKt {
     method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.lifecycle.ViewModelProvider.Factory? createHiltViewModelFactory(androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner);
-    method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM hiltViewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key);
+    method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM hiltViewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key);
   }
 
 }
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsSupportFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsSupportFragmentTest.java
index b37673c..1db3f5e 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsSupportFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsSupportFragmentTest.java
@@ -429,6 +429,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void navigateBetweenRowsAndVideoUsingDPAD1() throws Throwable {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         navigateBetweenRowsAndVideoUsingDPADInternal(DetailsSupportFragmentWithVideo1.class);
     }
 
@@ -436,6 +440,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void navigateBetweenRowsAndVideoUsingDPAD2() throws Throwable {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         navigateBetweenRowsAndVideoUsingDPADInternal(DetailsSupportFragmentWithVideo2.class);
     }
 
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/wizard/GuidedStepAttributesTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/wizard/GuidedStepAttributesTest.java
index bfb79b2..66a6632 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/wizard/GuidedStepAttributesTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/wizard/GuidedStepAttributesTest.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
+import android.os.Build;
 import android.view.KeyEvent;
 
 import androidx.leanback.app.GuidedStepFragment;
@@ -387,6 +388,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void testCheckedActions() throws Throwable {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
 
         Intent intent = new Intent();
         Resources res = mContext.getResources();
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ControlBarTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ControlBarTest.java
index 514e31a..99ce2e6 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ControlBarTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ControlBarTest.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
+import android.os.Build;
 import android.view.View;
 import android.widget.Button;
 import android.widget.LinearLayout;
@@ -44,6 +45,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void defaultFocus() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         Context context = ApplicationProvider.getApplicationContext();
         final ControlBar bar = new ControlBar(context, null);
         final TextView v1 = new Button(context);
@@ -66,6 +71,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void persistFocus() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         Context context = ApplicationProvider.getApplicationContext();
         final LinearLayout rootView = new LinearLayout(context);
         final ControlBar bar = new ControlBar(context, null);
@@ -109,6 +118,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void getFocusables() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         Context context = ApplicationProvider.getApplicationContext();
         final LinearLayout rootView = new LinearLayout(context);
         final ControlBar bar = new ControlBar(context, null);
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java
index 470ddf4..77a5b38 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java
@@ -65,6 +65,7 @@
 import androidx.testutils.AnimationTest;
 
 import org.junit.After;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestName;
@@ -931,6 +932,7 @@
         assertEquals(29, mGridView.getSelectedPosition());
     }
 
+    @Ignore // b/266757643
     @Test
     public void testThreeColumnVerticalBasic() throws Throwable {
         Intent intent = new Intent();
@@ -2343,6 +2345,7 @@
         assertEquals(mGridView.getWidth() / 2, (view.getLeft() + view.getRight()) / 2);
     }
 
+    @Ignore // b/266757643
     @Test
     public void testItemAddRemoveHorizontal() throws Throwable {
 
@@ -2926,7 +2929,7 @@
         testRemoveVisibleItemsInSmoothScrollingBackward(/*focusOnGridView=*/ false);
     }
 
-    @FlakyTest(bugId = 186848347)
+    @Ignore // b/266757643
     @Test
     public void testPendingSmoothScrollAndRemove() throws Throwable {
         Intent intent = new Intent();
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/PlaybackTransportRowPresenterTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/PlaybackTransportRowPresenterTest.java
index 7da8bab..9cb7606 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/PlaybackTransportRowPresenterTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/PlaybackTransportRowPresenterTest.java
@@ -30,6 +30,7 @@
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.drawable.ColorDrawable;
+import android.os.Build;
 import android.view.ContextThemeWrapper;
 import android.view.KeyEvent;
 import android.view.View;
@@ -272,6 +273,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void navigateProgressUpToPrimary() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
             @Override
             public void run() {
@@ -291,6 +296,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void navigateProgressDownToSecondary() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
             @Override
             public void run() {
@@ -323,6 +332,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void seekAndConfirm() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         when(mImpl.isPrepared()).thenReturn(true);
         when(mImpl.getCurrentPosition()).thenReturn(0L);
         when(mImpl.getDuration()).thenReturn(20000L);
@@ -352,6 +365,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void playSeekToZero() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         when(mImpl.isPrepared()).thenReturn(true);
         when(mImpl.getCurrentPosition()).thenReturn(0L);
         when(mImpl.getDuration()).thenReturn(20000L);
@@ -395,6 +412,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void playSeekAndCancel() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         when(mImpl.isPrepared()).thenReturn(true);
         when(mImpl.getCurrentPosition()).thenReturn(0L);
         when(mImpl.getDuration()).thenReturn(20000L);
@@ -438,6 +459,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void seekHoldKeyDown() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         when(mImpl.isPrepared()).thenReturn(true);
         when(mImpl.getCurrentPosition()).thenReturn(4489L);
         when(mImpl.getDuration()).thenReturn(20000L);
@@ -465,6 +490,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void seekAndCancel() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         when(mImpl.isPrepared()).thenReturn(true);
         when(mImpl.getCurrentPosition()).thenReturn(0L);
         when(mImpl.getDuration()).thenReturn(20000L);
@@ -494,6 +523,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void seekUpBetweenTwoKeyPosition() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         PlaybackSeekProviderSample provider = Mockito.spy(
                 new PlaybackSeekProviderSample(10000L, 101));
         final long[] positions = provider.getSeekPositions();
@@ -519,6 +552,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void seekDownBetweenTwoKeyPosition() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         PlaybackSeekProviderSample provider = Mockito.spy(
                 new PlaybackSeekProviderSample(10000L, 101));
         final long[] positions = provider.getSeekPositions();
@@ -545,6 +582,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void seekDownOutOfKeyPositions() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         PlaybackSeekProviderSample provider = Mockito.spy(
                 new PlaybackSeekProviderSample(1000L, 10000L, 101));
         final long[] positions = provider.getSeekPositions();
@@ -575,6 +616,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void seekDownAheadOfKeyPositions() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         PlaybackSeekProviderSample provider = Mockito.spy(
                 new PlaybackSeekProviderSample(1000L, 10000L, 101));
         final long[] positions = provider.getSeekPositions();
@@ -603,6 +648,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void seekUpAheadOfKeyPositions() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         PlaybackSeekProviderSample provider = Mockito.spy(
                 new PlaybackSeekProviderSample(1000L, 10000L, 101));
         final long[] positions = provider.getSeekPositions();
@@ -631,6 +680,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void seekUpOutOfKeyPositions() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         PlaybackSeekProviderSample provider = Mockito.spy(
                 new PlaybackSeekProviderSample(10000L, 101));
         final long[] positions = provider.getSeekPositions();
@@ -665,6 +718,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void seekUpAfterKeyPositions() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         PlaybackSeekProviderSample provider = Mockito.spy(
                 new PlaybackSeekProviderSample(10000L, 101));
         final long[] positions = provider.getSeekPositions();
@@ -692,6 +749,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void seekDownAfterKeyPositions() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         PlaybackSeekProviderSample provider = Mockito.spy(
                 new PlaybackSeekProviderSample(10000L, 101));
         final long[] positions = provider.getSeekPositions();
@@ -719,6 +780,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void thumbLoadedInCallback() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         when(mImpl.isPrepared()).thenReturn(true);
         when(mImpl.getCurrentPosition()).thenReturn(0L);
         when(mImpl.getDuration()).thenReturn(20000L);
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/picker/TimePickerTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/picker/TimePickerTest.java
index 3670c3a..8cee2be 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/picker/TimePickerTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/picker/TimePickerTest.java
@@ -22,6 +22,7 @@
 
 import android.content.Context;
 import android.content.Intent;
+import android.os.Build;
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.ViewGroup;
@@ -440,6 +441,10 @@
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void testInitiallyActiveTimePicker()
             throws Throwable {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         Intent intent = new Intent();
         intent.putExtra(TimePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
                 R.layout.timepicker_alone);
diff --git a/libraryversions.toml b/libraryversions.toml
index 494e5f39..49c571e 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,35 +1,35 @@
 [versions]
-ACTIVITY = "1.7.0-alpha04"
+ACTIVITY = "1.7.0-alpha05"
 ADS_IDENTIFIER = "1.0.0-alpha05"
-ANNOTATION = "1.6.0-alpha02"
+ANNOTATION = "1.6.0-beta01"
 ANNOTATION_EXPERIMENTAL = "1.4.0-alpha01"
 APPACTIONS_INTERACTION = "1.0.0-alpha01"
 APPCOMPAT = "1.7.0-alpha02"
 APPSEARCH = "1.1.0-alpha03"
-ARCH_CORE = "2.2.0-alpha02"
+ARCH_CORE = "2.2.0-rc01"
 ASYNCLAYOUTINFLATER = "1.1.0-alpha02"
 AUTOFILL = "1.2.0-beta02"
 BENCHMARK = "1.2.0-alpha10"
 BIOMETRIC = "1.2.0-alpha06"
 BLUETOOTH = "1.0.0-alpha01"
-BROWSER = "1.5.0-beta02"
+BROWSER = "1.6.0-alpha01"
 BUILDSRC_TESTS = "1.0.0-alpha01"
-CAMERA = "1.3.0-alpha03"
+CAMERA = "1.3.0-alpha04"
 CAMERA_PIPE = "1.0.0-alpha01"
 CARDVIEW = "1.1.0-alpha01"
 CAR_APP = "1.4.0-alpha01"
 COLLECTION = "1.3.0-alpha03"
 COLLECTION_KMP = "1.3.0-dev01"
-COMPOSE = "1.4.0-alpha05"
-COMPOSE_COMPILER = "1.4.0-alpha02"
+COMPOSE = "1.4.0-alpha06"
+COMPOSE_COMPILER = "1.4.1"
 COMPOSE_MATERIAL3 = "1.1.0-alpha05"
 COMPOSE_RUNTIME_TRACING = "1.0.0-alpha02"
-CONSTRAINTLAYOUT = "2.2.0-alpha06"
-CONSTRAINTLAYOUT_COMPOSE = "1.1.0-alpha06"
-CONSTRAINTLAYOUT_CORE = "1.1.0-alpha06"
+CONSTRAINTLAYOUT = "2.2.0-alpha07"
+CONSTRAINTLAYOUT_COMPOSE = "1.1.0-alpha07"
+CONSTRAINTLAYOUT_CORE = "1.1.0-alpha07"
 CONTENTPAGER = "1.1.0-alpha01"
 COORDINATORLAYOUT = "1.3.0-alpha01"
-CORE = "1.10.0-alpha02"
+CORE = "1.10.0-alpha03"
 CORE_ANIMATION = "1.0.0-beta02"
 CORE_ANIMATION_TESTING = "1.0.0-beta01"
 CORE_APPDIGEST = "1.0.0-alpha01"
@@ -49,11 +49,12 @@
 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"
-EMOJI2 = "1.3.0-alpha02"
+EMOJI2 = "1.3.0-beta02"
+EMOJI2_QUARANTINE = "1.0.0-alpha01"
 ENTERPRISE = "1.1.0-rc01"
 EXIFINTERFACE = "1.4.0-alpha01"
 FRAGMENT = "1.6.0-alpha05"
@@ -63,7 +64,7 @@
 GRAPHICS = "1.0.0-alpha03"
 GRAPHICS_FILTERS = "1.0.0-alpha01"
 GRIDLAYOUT = "1.1.0-alpha01"
-HEALTH_CONNECT = "1.0.0-alpha10"
+HEALTH_CONNECT = "1.0.0-alpha11"
 HEALTH_SERVICES_CLIENT = "1.0.0-beta03"
 HEIFWRITER = "1.1.0-alpha02"
 HILT = "1.1.0-alpha02"
@@ -79,12 +80,12 @@
 LEANBACK_TAB = "1.1.0-beta01"
 LEGACY = "1.1.0-alpha01"
 LIBYUV = "0.1.0-dev01"
-LIFECYCLE = "2.6.0-alpha05"
+LIFECYCLE = "2.6.0-alpha06"
 LIFECYCLE_EXTENSIONS = "2.2.0"
 LOADER = "1.2.0-alpha01"
 MEDIA = "1.7.0-alpha01"
 MEDIA2 = "1.3.0-alpha01"
-MEDIAROUTER = "1.4.0-beta01"
+MEDIAROUTER = "1.4.0-beta02"
 METRICS = "1.0.0-alpha04"
 NAVIGATION = "2.6.0-alpha05"
 PAGING = "3.2.0-alpha04"
@@ -93,10 +94,11 @@
 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"
-PROFILEINSTALLER = "1.3.0-alpha04"
+PROFILEINSTALLER = "1.3.0-beta01"
 RECOMMENDATION = "1.1.0-alpha01"
 RECYCLERVIEW = "1.4.0-alpha01"
 RECYCLERVIEW_SELECTION = "1.2.0-alpha02"
@@ -134,18 +136,18 @@
 VERSIONED_PARCELABLE = "1.2.0-alpha01"
 VIEWPAGER = "1.1.0-alpha02"
 VIEWPAGER2 = "1.2.0-alpha01"
-WEAR = "1.3.0-alpha04"
-WEAR_COMPOSE = "1.2.0-alpha03"
+WEAR = "1.3.0-alpha05"
+WEAR_COMPOSE = "1.2.0-alpha04"
 WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha01"
 WEAR_INPUT = "1.2.0-alpha03"
 WEAR_INPUT_TESTING = "1.2.0-alpha03"
 WEAR_ONGOING = "1.1.0-alpha01"
 WEAR_PHONE_INTERACTIONS = "1.1.0-alpha04"
-WEAR_PROTOLAYOUT = "1.0.0-alpha02"
+WEAR_PROTOLAYOUT = "1.0.0-alpha03"
 WEAR_REMOTE_INTERACTIONS = "1.1.0-alpha01"
 WEAR_TILES = "1.2.0-alpha01"
-WEAR_WATCHFACE = "1.2.0-alpha06"
-WEBKIT = "1.7.0-alpha01"
+WEAR_WATCHFACE = "1.2.0-alpha07"
+WEBKIT = "1.7.0-alpha02"
 WINDOW = "1.1.0-alpha05"
 WINDOW_EXTENSIONS = "1.1.0-alpha02"
 WINDOW_EXTENSIONS_CORE = "1.0.0-alpha01"
@@ -166,7 +168,7 @@
 BIOMETRIC = { group = "androidx.biometric", atomicGroupVersion = "versions.BIOMETRIC" }
 BLUETOOTH = { group = "androidx.bluetooth", atomicGroupVersion = "versions.BLUETOOTH" }
 BROWSER = { group = "androidx.browser", atomicGroupVersion = "versions.BROWSER" }
-BUILDSRC_TESTS = { group = "androidx.buildSrc-tests", atomicGroupVersion = "versions.BUILDSRC_TESTS", overrideInclude = [ ":buildSrc-tests:max-dep-versions:buildSrc-tests-max-dep-versions-dep", ":buildSrc-tests:max-dep-versions:buildSrc-tests-max-dep-versions-main",] }
+BUILDSRC_TESTS = { group = "androidx.buildSrc-tests", atomicGroupVersion = "versions.BUILDSRC_TESTS", overrideInclude = [ ":buildSrc-tests:max-dep-versions:buildSrc-tests-max-dep-versions-dep", ":buildSrc-tests:max-dep-versions:buildSrc-tests-max-dep-versions-main"] }
 CAMERA = { group = "androidx.camera", atomicGroupVersion = "versions.CAMERA" }
 CARDVIEW = { group = "androidx.cardview", atomicGroupVersion = "versions.CARDVIEW" }
 CAR_APP = { group = "androidx.car.app", atomicGroupVersion = "versions.CAR_APP" }
@@ -209,7 +211,7 @@
 HILT = { group = "androidx.hilt" }
 INPUT = { group = "androidx.input" }
 INSPECTION = { group = "androidx.inspection", atomicGroupVersion = "versions.INSPECTION" }
-INSPECTION_EXTENSIONS = { group = "androidx.inspection.extensions", atomicGroupVersion = "versions.SQLITE_INSPECTOR", overrideInclude = [ ":sqlite:sqlite-inspection",] }
+INSPECTION_EXTENSIONS = { group = "androidx.inspection.extensions", atomicGroupVersion = "versions.SQLITE_INSPECTOR", overrideInclude = [ ":sqlite:sqlite-inspection"] }
 INTERPOLATOR = { group = "androidx.interpolator", atomicGroupVersion = "versions.INTERPOLATOR" }
 JAVASCRIPTENGINE = { group = "androidx.javascriptengine", atomicGroupVersion = "versions.JAVASCRIPTENGINE" }
 LEANBACK = { group = "androidx.leanback" }
@@ -227,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-common/api/current.txt b/lifecycle/lifecycle-common/api/current.txt
index a339f50..c802eec 100644
--- a/lifecycle/lifecycle-common/api/current.txt
+++ b/lifecycle/lifecycle-common/api/current.txt
@@ -64,6 +64,7 @@
 
   public interface LifecycleOwner {
     method public androidx.lifecycle.Lifecycle getLifecycle();
+    property public abstract androidx.lifecycle.Lifecycle lifecycle;
   }
 
   @Deprecated @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target(java.lang.annotation.ElementType.METHOD) public @interface OnLifecycleEvent {
diff --git a/lifecycle/lifecycle-common/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-common/api/public_plus_experimental_current.txt
index a339f50..c802eec 100644
--- a/lifecycle/lifecycle-common/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-common/api/public_plus_experimental_current.txt
@@ -64,6 +64,7 @@
 
   public interface LifecycleOwner {
     method public androidx.lifecycle.Lifecycle getLifecycle();
+    property public abstract androidx.lifecycle.Lifecycle lifecycle;
   }
 
   @Deprecated @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target(java.lang.annotation.ElementType.METHOD) public @interface OnLifecycleEvent {
diff --git a/lifecycle/lifecycle-common/api/restricted_current.txt b/lifecycle/lifecycle-common/api/restricted_current.txt
index 04b9c90..2c8958d 100644
--- a/lifecycle/lifecycle-common/api/restricted_current.txt
+++ b/lifecycle/lifecycle-common/api/restricted_current.txt
@@ -71,6 +71,7 @@
 
   public interface LifecycleOwner {
     method public androidx.lifecycle.Lifecycle getLifecycle();
+    property public abstract androidx.lifecycle.Lifecycle lifecycle;
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class Lifecycling {
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleOwner.java b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleOwner.java
deleted file mode 100644
index 02cc1eb..0000000
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleOwner.java
+++ /dev/null
@@ -1,37 +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 androidx.annotation.NonNull;
-
-/**
- * A class that has an Android lifecycle. These events can be used by custom components to
- * handle lifecycle changes without implementing any code inside the Activity or the Fragment.
- *
- * @see Lifecycle
- * @see ViewTreeLifecycleOwner
- */
-@SuppressWarnings({"WeakerAccess", "unused"})
-public interface LifecycleOwner {
-    /**
-     * Returns the Lifecycle of the provider.
-     *
-     * @return The lifecycle of the provider.
-     */
-    @NonNull
-    Lifecycle getLifecycle();
-}
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleOwner.kt b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleOwner.kt
new file mode 100644
index 0000000..cdc18d4
--- /dev/null
+++ b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleOwner.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.lifecycle
+
+/**
+ * A class that has an Android lifecycle. These events can be used by custom components to
+ * handle lifecycle changes without implementing any code inside the Activity or the Fragment.
+ *
+ * @see Lifecycle
+ * @see ViewTreeLifecycleOwner
+ */
+public interface LifecycleOwner {
+    /**
+     * Returns the Lifecycle of the provider.
+     *
+     * @return The lifecycle of the provider.
+     */
+    public val lifecycle: Lifecycle
+}
\ No newline at end of file
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.java
deleted file mode 100644
index 30afeedf..0000000
--- a/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.java
+++ /dev/null
@@ -1,32 +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;
-
-/**
- * A simple callback that can receive from {@link LiveData}.
- *
- * @param <T> The type of the parameter
- *
- * @see LiveData LiveData - for a usage description.
- */
-public interface Observer<T> {
-    /**
-     * Called when the data is changed.
-     * @param t  The new data
-     */
-    void onChanged(T t);
-}
diff --git a/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.kt b/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.kt
new file mode 100644
index 0000000..2df6295
--- /dev/null
+++ b/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.kt
@@ -0,0 +1,29 @@
+/*
+ * 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
+
+/**
+ * A simple callback that can receive from [LiveData].
+ *
+ * @see LiveData LiveData - for a usage description.
+*/
+fun interface Observer<T> {
+
+    /**
+     * Called when the data is changed is changed to [value].
+     */
+    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/api/current.txt b/lifecycle/lifecycle-process/api/current.txt
index 35d5ac4..891c9c6 100644
--- a/lifecycle/lifecycle-process/api/current.txt
+++ b/lifecycle/lifecycle-process/api/current.txt
@@ -10,6 +10,7 @@
   public final class ProcessLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
     method public static androidx.lifecycle.LifecycleOwner get();
     method public androidx.lifecycle.Lifecycle getLifecycle();
+    property public androidx.lifecycle.Lifecycle lifecycle;
     field public static final androidx.lifecycle.ProcessLifecycleOwner.Companion Companion;
   }
 
diff --git a/lifecycle/lifecycle-process/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-process/api/public_plus_experimental_current.txt
index 35d5ac4..891c9c6 100644
--- a/lifecycle/lifecycle-process/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-process/api/public_plus_experimental_current.txt
@@ -10,6 +10,7 @@
   public final class ProcessLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
     method public static androidx.lifecycle.LifecycleOwner get();
     method public androidx.lifecycle.Lifecycle getLifecycle();
+    property public androidx.lifecycle.Lifecycle lifecycle;
     field public static final androidx.lifecycle.ProcessLifecycleOwner.Companion Companion;
   }
 
diff --git a/lifecycle/lifecycle-process/api/restricted_current.txt b/lifecycle/lifecycle-process/api/restricted_current.txt
index 35d5ac4..891c9c6 100644
--- a/lifecycle/lifecycle-process/api/restricted_current.txt
+++ b/lifecycle/lifecycle-process/api/restricted_current.txt
@@ -10,6 +10,7 @@
   public final class ProcessLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
     method public static androidx.lifecycle.LifecycleOwner get();
     method public androidx.lifecycle.Lifecycle getLifecycle();
+    property public androidx.lifecycle.Lifecycle lifecycle;
     field public static final androidx.lifecycle.ProcessLifecycleOwner.Companion Companion;
   }
 
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..f29e9e4 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)
                 }
             }
 
@@ -184,9 +185,8 @@
         })
     }
 
-    override fun getLifecycle(): Lifecycle {
-        return registry
-    }
+    override val lifecycle: Lifecycle
+        get() = registry
 
     @RequiresApi(29)
     internal object Api29Impl {
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-ktx/src/androidTest/java/androidx/lifecycle/FakeLifecycleOwner.kt b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/FakeLifecycleOwner.kt
index bbf9774..10d9fd9 100644
--- a/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/FakeLifecycleOwner.kt
+++ b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/FakeLifecycleOwner.kt
@@ -29,7 +29,8 @@
         }
     }
 
-    override fun getLifecycle(): Lifecycle = registry
+    override val lifecycle: Lifecycle
+        get() = registry
 
     fun setState(state: Lifecycle.State) {
         registry.currentState = state
diff --git a/camera/camera-viewfinder/api/res-1.1.0-beta03.txt b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/ViewTreeLifecycleOwnerTest.kt
similarity index 100%
copy from camera/camera-viewfinder/api/res-1.1.0-beta03.txt
copy to lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/ViewTreeLifecycleOwnerTest.kt
diff --git a/lifecycle/lifecycle-runtime-testing/api/current.txt b/lifecycle/lifecycle-runtime-testing/api/current.txt
index dc2274c..47a819e 100644
--- a/lifecycle/lifecycle-runtime-testing/api/current.txt
+++ b/lifecycle/lifecycle-runtime-testing/api/current.txt
@@ -11,6 +11,7 @@
     method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event event);
     method public void setCurrentState(androidx.lifecycle.Lifecycle.State);
     property public final androidx.lifecycle.Lifecycle.State currentState;
+    property public androidx.lifecycle.LifecycleRegistry lifecycle;
     property public final int observerCount;
   }
 
diff --git a/lifecycle/lifecycle-runtime-testing/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-runtime-testing/api/public_plus_experimental_current.txt
index dc2274c..47a819e 100644
--- a/lifecycle/lifecycle-runtime-testing/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-runtime-testing/api/public_plus_experimental_current.txt
@@ -11,6 +11,7 @@
     method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event event);
     method public void setCurrentState(androidx.lifecycle.Lifecycle.State);
     property public final androidx.lifecycle.Lifecycle.State currentState;
+    property public androidx.lifecycle.LifecycleRegistry lifecycle;
     property public final int observerCount;
   }
 
diff --git a/lifecycle/lifecycle-runtime-testing/api/restricted_current.txt b/lifecycle/lifecycle-runtime-testing/api/restricted_current.txt
index dc2274c..47a819e 100644
--- a/lifecycle/lifecycle-runtime-testing/api/restricted_current.txt
+++ b/lifecycle/lifecycle-runtime-testing/api/restricted_current.txt
@@ -11,6 +11,7 @@
     method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event event);
     method public void setCurrentState(androidx.lifecycle.Lifecycle.State);
     property public final androidx.lifecycle.Lifecycle.State currentState;
+    property public androidx.lifecycle.LifecycleRegistry lifecycle;
     property public final int observerCount;
   }
 
diff --git a/lifecycle/lifecycle-runtime-testing/src/main/java/androidx/lifecycle/testing/TestLifecycleOwner.kt b/lifecycle/lifecycle-runtime-testing/src/main/java/androidx/lifecycle/testing/TestLifecycleOwner.kt
index ddb193b..7e0f455 100644
--- a/lifecycle/lifecycle-runtime-testing/src/main/java/androidx/lifecycle/testing/TestLifecycleOwner.kt
+++ b/lifecycle/lifecycle-runtime-testing/src/main/java/androidx/lifecycle/testing/TestLifecycleOwner.kt
@@ -45,7 +45,9 @@
     private val lifecycleRegistry = LifecycleRegistry.createUnsafe(this).apply {
         currentState = initialState
     }
-    override fun getLifecycle(): LifecycleRegistry = lifecycleRegistry
+
+    override val lifecycle: LifecycleRegistry
+        get() = lifecycleRegistry
 
     /**
      * Update the [currentState] by moving it to the state directly after the given [event].
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-service/api/current.txt b/lifecycle/lifecycle-service/api/current.txt
index a27283e..bebcd93 100644
--- a/lifecycle/lifecycle-service/api/current.txt
+++ b/lifecycle/lifecycle-service/api/current.txt
@@ -5,6 +5,7 @@
     ctor public LifecycleService();
     method public androidx.lifecycle.Lifecycle getLifecycle();
     method @CallSuper public android.os.IBinder? onBind(android.content.Intent intent);
+    property public androidx.lifecycle.Lifecycle lifecycle;
   }
 
   public class ServiceLifecycleDispatcher {
diff --git a/lifecycle/lifecycle-service/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-service/api/public_plus_experimental_current.txt
index a27283e..bebcd93 100644
--- a/lifecycle/lifecycle-service/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-service/api/public_plus_experimental_current.txt
@@ -5,6 +5,7 @@
     ctor public LifecycleService();
     method public androidx.lifecycle.Lifecycle getLifecycle();
     method @CallSuper public android.os.IBinder? onBind(android.content.Intent intent);
+    property public androidx.lifecycle.Lifecycle lifecycle;
   }
 
   public class ServiceLifecycleDispatcher {
diff --git a/lifecycle/lifecycle-service/api/restricted_current.txt b/lifecycle/lifecycle-service/api/restricted_current.txt
index a27283e..bebcd93 100644
--- a/lifecycle/lifecycle-service/api/restricted_current.txt
+++ b/lifecycle/lifecycle-service/api/restricted_current.txt
@@ -5,6 +5,7 @@
     ctor public LifecycleService();
     method public androidx.lifecycle.Lifecycle getLifecycle();
     method @CallSuper public android.os.IBinder? onBind(android.content.Intent intent);
+    property public androidx.lifecycle.Lifecycle lifecycle;
   }
 
   public class ServiceLifecycleDispatcher {
diff --git a/lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/LifecycleService.kt b/lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/LifecycleService.kt
index 7754cc7..17006dc 100644
--- a/lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/LifecycleService.kt
+++ b/lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/LifecycleService.kt
@@ -62,7 +62,6 @@
         super.onDestroy()
     }
 
-    override fun getLifecycle(): Lifecycle {
-        return dispatcher.lifecycle
-    }
+    override val lifecycle: Lifecycle
+        get() = dispatcher.lifecycle
 }
\ No newline at end of file
diff --git a/lifecycle/lifecycle-viewmodel-compose/api/current.txt b/lifecycle/lifecycle-viewmodel-compose/api/current.txt
index 12fad78..05b6910 100644
--- a/lifecycle/lifecycle-viewmodel-compose/api/current.txt
+++ b/lifecycle/lifecycle-viewmodel-compose/api/current.txt
@@ -13,10 +13,10 @@
 
   public final class ViewModelKt {
     method @androidx.compose.runtime.Composable public static <VM extends androidx.lifecycle.ViewModel> VM viewModel(Class<VM> modelClass, optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory, optional androidx.lifecycle.viewmodel.CreationExtras extras);
-    method @Deprecated @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
-    method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, optional androidx.lifecycle.ViewModelProvider.Factory? factory, optional androidx.lifecycle.viewmodel.CreationExtras extras);
-    method @Deprecated @androidx.compose.runtime.Composable public static <VM extends androidx.lifecycle.ViewModel> VM viewModel(Class<VM> modelClass, optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
-    method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends VM> initializer);
+    method @Deprecated @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
+    method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory, optional androidx.lifecycle.viewmodel.CreationExtras extras);
+    method @Deprecated @androidx.compose.runtime.Composable public static <VM extends androidx.lifecycle.ViewModel> VM viewModel(Class<VM> modelClass, optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
+    method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends VM> initializer);
   }
 
 }
diff --git a/lifecycle/lifecycle-viewmodel-compose/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-viewmodel-compose/api/public_plus_experimental_current.txt
index fdd4c83..188b922 100644
--- a/lifecycle/lifecycle-viewmodel-compose/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-viewmodel-compose/api/public_plus_experimental_current.txt
@@ -20,10 +20,10 @@
 
   public final class ViewModelKt {
     method @androidx.compose.runtime.Composable public static <VM extends androidx.lifecycle.ViewModel> VM viewModel(Class<VM> modelClass, optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory, optional androidx.lifecycle.viewmodel.CreationExtras extras);
-    method @Deprecated @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
-    method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, optional androidx.lifecycle.ViewModelProvider.Factory? factory, optional androidx.lifecycle.viewmodel.CreationExtras extras);
-    method @Deprecated @androidx.compose.runtime.Composable public static <VM extends androidx.lifecycle.ViewModel> VM viewModel(Class<VM> modelClass, optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
-    method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends VM> initializer);
+    method @Deprecated @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
+    method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory, optional androidx.lifecycle.viewmodel.CreationExtras extras);
+    method @Deprecated @androidx.compose.runtime.Composable public static <VM extends androidx.lifecycle.ViewModel> VM viewModel(Class<VM> modelClass, optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
+    method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends VM> initializer);
   }
 
 }
diff --git a/lifecycle/lifecycle-viewmodel-compose/api/restricted_current.txt b/lifecycle/lifecycle-viewmodel-compose/api/restricted_current.txt
index 12fad78..05b6910 100644
--- a/lifecycle/lifecycle-viewmodel-compose/api/restricted_current.txt
+++ b/lifecycle/lifecycle-viewmodel-compose/api/restricted_current.txt
@@ -13,10 +13,10 @@
 
   public final class ViewModelKt {
     method @androidx.compose.runtime.Composable public static <VM extends androidx.lifecycle.ViewModel> VM viewModel(Class<VM> modelClass, optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory, optional androidx.lifecycle.viewmodel.CreationExtras extras);
-    method @Deprecated @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
-    method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, optional androidx.lifecycle.ViewModelProvider.Factory? factory, optional androidx.lifecycle.viewmodel.CreationExtras extras);
-    method @Deprecated @androidx.compose.runtime.Composable public static <VM extends androidx.lifecycle.ViewModel> VM viewModel(Class<VM> modelClass, optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
-    method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends VM> initializer);
+    method @Deprecated @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
+    method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory, optional androidx.lifecycle.viewmodel.CreationExtras extras);
+    method @Deprecated @androidx.compose.runtime.Composable public static <VM extends androidx.lifecycle.ViewModel> VM viewModel(Class<VM> modelClass, optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
+    method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends VM> initializer);
   }
 
 }
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt
index bc46abe..3b02e84 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt
@@ -270,6 +270,6 @@
     val store = ViewModelStore()
     val factory = FakeViewModelProviderFactory()
 
-    override fun getViewModelStore(): ViewModelStore = store
-    override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory = factory
+    override val viewModelStore: ViewModelStore = store
+    override val defaultViewModelProviderFactory = factory
 }
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/api/current.ignore b/lifecycle/lifecycle-viewmodel-savedstate/api/current.ignore
new file mode 100644
index 0000000..3bd1b51
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-savedstate/api/current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+RemovedMethod: androidx.lifecycle.AbstractSavedStateViewModelFactory#create(Class<T>):
+    Removed method androidx.lifecycle.AbstractSavedStateViewModelFactory.create(Class<T>)
+RemovedMethod: androidx.lifecycle.AbstractSavedStateViewModelFactory#create(Class<T>, androidx.lifecycle.viewmodel.CreationExtras):
+    Removed method androidx.lifecycle.AbstractSavedStateViewModelFactory.create(Class<T>,androidx.lifecycle.viewmodel.CreationExtras)
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/api/current.txt b/lifecycle/lifecycle-viewmodel-savedstate/api/current.txt
index 35fdaaa..c030c8a 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/api/current.txt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/api/current.txt
@@ -3,10 +3,8 @@
 
   public abstract class AbstractSavedStateViewModelFactory implements androidx.lifecycle.ViewModelProvider.Factory {
     ctor public AbstractSavedStateViewModelFactory();
-    ctor public AbstractSavedStateViewModelFactory(androidx.savedstate.SavedStateRegistryOwner, android.os.Bundle?);
-    method public final <T extends androidx.lifecycle.ViewModel> T create(Class<T!>, androidx.lifecycle.viewmodel.CreationExtras);
-    method public final <T extends androidx.lifecycle.ViewModel> T create(Class<T!>);
-    method protected abstract <T extends androidx.lifecycle.ViewModel> T create(String, Class<T!>, androidx.lifecycle.SavedStateHandle);
+    ctor public AbstractSavedStateViewModelFactory(androidx.savedstate.SavedStateRegistryOwner owner, android.os.Bundle? defaultArgs);
+    method protected abstract <T extends androidx.lifecycle.ViewModel> T create(String key, Class<T> modelClass, androidx.lifecycle.SavedStateHandle handle);
   }
 
   public final class SavedStateHandle {
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-viewmodel-savedstate/api/public_plus_experimental_current.txt
index 35fdaaa..c030c8a 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/api/public_plus_experimental_current.txt
@@ -3,10 +3,8 @@
 
   public abstract class AbstractSavedStateViewModelFactory implements androidx.lifecycle.ViewModelProvider.Factory {
     ctor public AbstractSavedStateViewModelFactory();
-    ctor public AbstractSavedStateViewModelFactory(androidx.savedstate.SavedStateRegistryOwner, android.os.Bundle?);
-    method public final <T extends androidx.lifecycle.ViewModel> T create(Class<T!>, androidx.lifecycle.viewmodel.CreationExtras);
-    method public final <T extends androidx.lifecycle.ViewModel> T create(Class<T!>);
-    method protected abstract <T extends androidx.lifecycle.ViewModel> T create(String, Class<T!>, androidx.lifecycle.SavedStateHandle);
+    ctor public AbstractSavedStateViewModelFactory(androidx.savedstate.SavedStateRegistryOwner owner, android.os.Bundle? defaultArgs);
+    method protected abstract <T extends androidx.lifecycle.ViewModel> T create(String key, Class<T> modelClass, androidx.lifecycle.SavedStateHandle handle);
   }
 
   public final class SavedStateHandle {
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/api/restricted_current.ignore b/lifecycle/lifecycle-viewmodel-savedstate/api/restricted_current.ignore
new file mode 100644
index 0000000..3bd1b51
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-savedstate/api/restricted_current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+RemovedMethod: androidx.lifecycle.AbstractSavedStateViewModelFactory#create(Class<T>):
+    Removed method androidx.lifecycle.AbstractSavedStateViewModelFactory.create(Class<T>)
+RemovedMethod: androidx.lifecycle.AbstractSavedStateViewModelFactory#create(Class<T>, androidx.lifecycle.viewmodel.CreationExtras):
+    Removed method androidx.lifecycle.AbstractSavedStateViewModelFactory.create(Class<T>,androidx.lifecycle.viewmodel.CreationExtras)
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/api/restricted_current.txt b/lifecycle/lifecycle-viewmodel-savedstate/api/restricted_current.txt
index 35fdaaa..c030c8a 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/api/restricted_current.txt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/api/restricted_current.txt
@@ -3,10 +3,8 @@
 
   public abstract class AbstractSavedStateViewModelFactory implements androidx.lifecycle.ViewModelProvider.Factory {
     ctor public AbstractSavedStateViewModelFactory();
-    ctor public AbstractSavedStateViewModelFactory(androidx.savedstate.SavedStateRegistryOwner, android.os.Bundle?);
-    method public final <T extends androidx.lifecycle.ViewModel> T create(Class<T!>, androidx.lifecycle.viewmodel.CreationExtras);
-    method public final <T extends androidx.lifecycle.ViewModel> T create(Class<T!>);
-    method protected abstract <T extends androidx.lifecycle.ViewModel> T create(String, Class<T!>, androidx.lifecycle.SavedStateHandle);
+    ctor public AbstractSavedStateViewModelFactory(androidx.savedstate.SavedStateRegistryOwner owner, android.os.Bundle? defaultArgs);
+    method protected abstract <T extends androidx.lifecycle.ViewModel> T create(String key, Class<T> modelClass, androidx.lifecycle.SavedStateHandle handle);
   }
 
   public final class SavedStateHandle {
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
index 3ceb0b3..a88aecc 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
@@ -52,10 +52,6 @@
     constraints {
         implementation(project(":lifecycle:lifecycle-livedata-core"))
         implementation(project(":lifecycle:lifecycle-viewmodel"))
-        // this syntax is a temporary workout to allow project dependency on cross-project-set
-        // i.e. COMPOSE + MAIN project sets
-        // update syntax when b/239979823 is fixed
-        implementation("androidx.lifecycle:lifecycle-viewmodel-compose:${androidx.LibraryVersions.LIFECYCLE}")
     }
 }
 
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/TestComponent.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/TestComponent.kt
index 1f7b190..a0c00510 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/TestComponent.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/TestComponent.kt
@@ -45,11 +45,12 @@
         savedStateController.performRestore(bundle)
     }
 
-    override fun getLifecycle(): Lifecycle = lifecycleRegistry
+    override val lifecycle: Lifecycle
+        get() = lifecycleRegistry
     override val savedStateRegistry: SavedStateRegistry
         get() = savedStateController.savedStateRegistry
 
-    override fun getViewModelStore(): ViewModelStore = vmStore
+    override val viewModelStore: ViewModelStore = vmStore
 
     fun resume() {
         lifecycleRegistry.currentState = Lifecycle.State.RESUMED
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/ViewModelsWithStateTest.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/ViewModelsWithStateTest.kt
index c9d5d4e..c139d47 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/ViewModelsWithStateTest.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/ViewModelsWithStateTest.kt
@@ -251,14 +251,16 @@
     SavedStateRegistryOwner by ssrOwner,
     HasDefaultViewModelProviderFactory {
 
-    override fun getDefaultViewModelProviderFactory(): Factory {
-        throw UnsupportedOperationException()
-    }
+    override val defaultViewModelProviderFactory: Factory
+        get() {
+            throw UnsupportedOperationException()
+        }
 
-    override fun getDefaultViewModelCreationExtras(): CreationExtras {
-        val extras = MutableCreationExtras()
-        extras[SAVED_STATE_REGISTRY_OWNER_KEY] = this
-        extras[VIEW_MODEL_STORE_OWNER_KEY] = this
-        return extras
-    }
+    override val defaultViewModelCreationExtras: CreationExtras
+        get() {
+            val extras = MutableCreationExtras()
+            extras[SAVED_STATE_REGISTRY_OWNER_KEY] = this
+            extras[VIEW_MODEL_STORE_OWNER_KEY] = this
+            return extras
+        }
 }
\ No newline at end of file
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.java b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.java
deleted file mode 100644
index dad9f6f..0000000
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.lifecycle;
-
-import static androidx.lifecycle.LegacySavedStateHandleController.attachHandleIfNeeded;
-import static androidx.lifecycle.ViewModelProvider.NewInstanceFactory.VIEW_MODEL_KEY;
-
-import android.annotation.SuppressLint;
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.lifecycle.viewmodel.CreationExtras;
-import androidx.savedstate.SavedStateRegistry;
-import androidx.savedstate.SavedStateRegistryOwner;
-
-/**
- * Skeleton of androidx.lifecycle.ViewModelProvider.KeyedFactory
- * that creates {@link SavedStateHandle} for every requested {@link androidx.lifecycle.ViewModel}.
- * The subclasses implement {@link #create(String, Class, SavedStateHandle)} to actually instantiate
- * {@code androidx.lifecycle.ViewModel}s.
- */
-public abstract class AbstractSavedStateViewModelFactory extends ViewModelProvider.OnRequeryFactory
-        implements ViewModelProvider.Factory {
-    static final String TAG_SAVED_STATE_HANDLE_CONTROLLER = "androidx.lifecycle.savedstate.vm.tag";
-
-    private SavedStateRegistry mSavedStateRegistry;
-    private Lifecycle mLifecycle;
-    private Bundle mDefaultArgs;
-
-    /**
-     * Constructs this factory.
-     * <p>
-     * When a factory is constructed this way, a component for which {@link SavedStateHandle} is
-     * scoped must have called
-     * {@link SavedStateHandleSupport#enableSavedStateHandles(SavedStateRegistryOwner)}.
-     * See {@link SavedStateHandleSupport#createSavedStateHandle(CreationExtras)} docs for more
-     * details.
-     */
-    public AbstractSavedStateViewModelFactory() {
-    }
-
-    /**
-     * Constructs this factory.
-     *
-     * @param owner {@link SavedStateRegistryOwner} that will provide restored state for created
-     * {@link androidx.lifecycle.ViewModel ViewModels}
-     * @param defaultArgs values from this {@code Bundle} will be used as defaults by
-     *                    {@link SavedStateHandle} passed in {@link ViewModel ViewModels}
-     *                    if there is no previously saved state
-     *                    or previously saved state misses a value by such key
-     */
-    @SuppressLint("LambdaLast")
-    public AbstractSavedStateViewModelFactory(@NonNull SavedStateRegistryOwner owner,
-            @Nullable Bundle defaultArgs) {
-        mSavedStateRegistry = owner.getSavedStateRegistry();
-        mLifecycle = owner.getLifecycle();
-        mDefaultArgs = defaultArgs;
-    }
-
-    @NonNull
-    @Override
-    public final <T extends ViewModel> T create(@NonNull Class<T> modelClass,
-            @NonNull CreationExtras extras) {
-        String key = extras.get(VIEW_MODEL_KEY);
-        if (key == null) {
-            throw new IllegalStateException(
-                    "VIEW_MODEL_KEY must always be provided by ViewModelProvider");
-        }
-        // if a factory constructed in the old way use the old infra to create SavedStateHandle
-        if (mSavedStateRegistry != null) {
-            return create(key, modelClass);
-        } else {
-            return create(key, modelClass, SavedStateHandleSupport.createSavedStateHandle(extras));
-        }
-    }
-
-    @NonNull
-    private <T extends ViewModel> T create(@NonNull String key, @NonNull Class<T> modelClass) {
-        SavedStateHandleController controller = LegacySavedStateHandleController
-                .create(mSavedStateRegistry, mLifecycle, key, mDefaultArgs);
-        T viewmodel = create(key, modelClass, controller.getHandle());
-        viewmodel.setTagIfAbsent(TAG_SAVED_STATE_HANDLE_CONTROLLER, controller);
-        return viewmodel;
-    }
-
-    @NonNull
-    @Override
-    public final <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
-        // ViewModelProvider calls correct create that support same modelClass with different keys
-        // If a developer manually calls this method, there is no "key" in picture, so factory
-        // simply uses classname internally as as key.
-        String canonicalName = modelClass.getCanonicalName();
-        if (canonicalName == null) {
-            throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
-        }
-        if (mLifecycle == null) {
-            throw new UnsupportedOperationException(
-                    "AbstractSavedStateViewModelFactory constructed "
-                            + "with empty constructor supports only calls to "
-                            +   "create(modelClass: Class<T>, extras: CreationExtras)."
-            );
-        }
-        return create(canonicalName, modelClass);
-    }
-
-    /**
-     * Creates a new instance of the given {@code Class}.
-     * <p>
-     *
-     * @param key a key associated with the requested ViewModel
-     * @param modelClass a {@code Class} whose instance is requested
-     * @param handle a handle to saved state associated with the requested ViewModel
-     * @param <T> The type parameter for the ViewModel.
-     * @return a newly created ViewModels
-     */
-    @NonNull
-    protected abstract <T extends ViewModel> T create(@NonNull String key,
-            @NonNull Class<T> modelClass, @NonNull SavedStateHandle handle);
-
-    /**
-     * @hide
-     */
-    @Override
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public void onRequery(@NonNull ViewModel viewModel) {
-        // is need only for legacy path
-        if (mSavedStateRegistry != null) {
-            attachHandleIfNeeded(viewModel, mSavedStateRegistry, mLifecycle);
-        }
-    }
-}
-
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
new file mode 100644
index 0000000..3e09793
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.lifecycle
+
+import android.os.Bundle
+import androidx.annotation.RestrictTo
+import androidx.lifecycle.LegacySavedStateHandleController.attachHandleIfNeeded
+import androidx.lifecycle.viewmodel.CreationExtras
+import androidx.savedstate.SavedStateRegistry
+import androidx.savedstate.SavedStateRegistryOwner
+
+/**
+ * Skeleton of androidx.lifecycle.ViewModelProvider.KeyedFactory
+ * that creates [SavedStateHandle] for every requested [ViewModel].
+ * The subclasses implement [create] to actually instantiate
+ * `androidx.lifecycle.ViewModel`s.
+ */
+public abstract class AbstractSavedStateViewModelFactory :
+    ViewModelProvider.OnRequeryFactory,
+    ViewModelProvider.Factory {
+
+    private var savedStateRegistry: SavedStateRegistry? = null
+    private var lifecycle: Lifecycle? = null
+    private var defaultArgs: Bundle? = null
+
+    /**
+     * Constructs this factory.
+     *
+     * When a factory is constructed this way, a component for which [SavedStateHandle] is
+     * scoped must have called
+     * [SavedStateHandleSupport.enableSavedStateHandles].
+     * See [CreationExtras.createSavedStateHandle] docs for more
+     * details.
+     */
+    constructor() {}
+
+    /**
+     * Constructs this factory.
+     *
+     * @param owner [SavedStateRegistryOwner] that will provide restored state for created
+     * [ViewModels][ViewModel]
+     * @param defaultArgs values from this `Bundle` will be used as defaults by
+     * [SavedStateHandle] passed in [ViewModels][ViewModel] if there is no
+     * previously saved state or previously saved state misses a value by such key
+     */
+    constructor(
+        owner: SavedStateRegistryOwner,
+        defaultArgs: Bundle?
+    ) {
+        savedStateRegistry = owner.savedStateRegistry
+        lifecycle = owner.lifecycle
+        this.defaultArgs = defaultArgs
+    }
+
+    /**
+     * Creates a new instance of the given `Class`.
+     *
+     * @param modelClass a `Class` whose instance is requested
+     * @param extras an additional information for this creation request
+     *
+     * @return a newly created ViewModel
+     *
+     * @throws IllegalStateException if no VIEW_MODEL_KEY provided by ViewModelProvider
+     */
+    public override fun <T : ViewModel> create(
+        modelClass: Class<T>,
+        extras: CreationExtras
+    ): T {
+        val key = extras[ViewModelProvider.NewInstanceFactory.VIEW_MODEL_KEY]
+            ?: throw IllegalStateException(
+                "VIEW_MODEL_KEY must always be provided by ViewModelProvider"
+            )
+        // if a factory constructed in the old way use the old infra to create SavedStateHandle
+        return if (savedStateRegistry != null) {
+            create(key, modelClass)
+        } else {
+            create(key, modelClass, extras.createSavedStateHandle())
+        }
+    }
+
+    private fun <T : ViewModel> create(key: String, modelClass: Class<T>): T {
+        val controller = LegacySavedStateHandleController
+            .create(savedStateRegistry!!, lifecycle!!, key, defaultArgs)
+        val viewModel = create(key, modelClass, controller.handle)
+        viewModel.setTagIfAbsent(TAG_SAVED_STATE_HANDLE_CONTROLLER, controller)
+        return viewModel
+    }
+
+    /**
+     * Creates a new instance of the given `Class`.
+     *
+     * @param modelClass a `Class` whose instance is requested
+     *
+     * @return a newly created ViewModel
+     *
+     * @throws IllegalArgumentException if the given [modelClass] is local or anonymous class.
+     * @throws UnsupportedOperationException if AbstractSavedStateViewModelFactory constructed
+     * with empty constructor, therefore no [SavedStateRegistryOwner] available for lifecycle
+     */
+    public override fun <T : ViewModel> create(modelClass: Class<T>): T {
+        // ViewModelProvider calls correct create that support same modelClass with different keys
+        // If a developer manually calls this method, there is no "key" in picture, so factory
+        // simply uses classname internally as as key.
+        val canonicalName = modelClass.canonicalName
+            ?: throw IllegalArgumentException("Local and anonymous classes can not be ViewModels")
+        if (lifecycle == null) {
+            throw UnsupportedOperationException(
+                "AbstractSavedStateViewModelFactory constructed " +
+                    "with empty constructor supports only calls to " +
+                    "create(modelClass: Class<T>, extras: CreationExtras)."
+            )
+        }
+        return create(canonicalName, modelClass)
+    }
+
+    /**
+     * Creates a new instance of the given `Class`.
+     *
+     * @param key a key associated with the requested ViewModel
+     * @param modelClass a `Class` whose instance is requested
+     * @param handle a handle to saved state associated with the requested ViewModel
+     *
+     * @return the newly created ViewModel
+    </T> */
+    protected abstract fun <T : ViewModel> create(
+        key: String,
+        modelClass: Class<T>,
+        handle: SavedStateHandle
+    ): T
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    override fun onRequery(viewModel: ViewModel) {
+        // is need only for legacy path
+        if (savedStateRegistry != null) {
+            attachHandleIfNeeded(viewModel, savedStateRegistry!!, lifecycle!!)
+        }
+    }
+
+    internal companion object {
+        internal const val TAG_SAVED_STATE_HANDLE_CONTROLLER =
+            "androidx.lifecycle.savedstate.vm.tag"
+    }
+}
\ No newline at end of file
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/SavedStateHandleController.java b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleController.java
deleted file mode 100644
index 33797c7..0000000
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleController.java
+++ /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.lifecycle;
-
-import androidx.annotation.NonNull;
-import androidx.savedstate.SavedStateRegistry;
-
-final class SavedStateHandleController implements LifecycleEventObserver {
-    private final String mKey;
-    private boolean mIsAttached = false;
-    private final SavedStateHandle mHandle;
-
-    SavedStateHandleController(String key, SavedStateHandle handle) {
-        mKey = key;
-        mHandle = handle;
-    }
-
-    boolean isAttached() {
-        return mIsAttached;
-    }
-
-    void attachToLifecycle(SavedStateRegistry registry, Lifecycle lifecycle) {
-        if (mIsAttached) {
-            throw new IllegalStateException("Already attached to lifecycleOwner");
-        }
-        mIsAttached = true;
-        lifecycle.addObserver(this);
-        registry.registerSavedStateProvider(mKey, mHandle.savedStateProvider());
-    }
-
-    @Override
-    public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
-        if (event == Lifecycle.Event.ON_DESTROY) {
-            mIsAttached = false;
-            source.getLifecycle().removeObserver(this);
-        }
-    }
-
-    SavedStateHandle getHandle() {
-        return mHandle;
-    }
-}
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleController.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleController.kt
new file mode 100644
index 0000000..d50f2c7
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleController.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.lifecycle
+
+import androidx.savedstate.SavedStateRegistry
+
+internal class SavedStateHandleController(
+    private val key: String,
+    val handle: SavedStateHandle
+) : LifecycleEventObserver {
+
+    var isAttached = false
+        private set
+
+    fun attachToLifecycle(registry: SavedStateRegistry, lifecycle: Lifecycle) {
+        check(!isAttached) { "Already attached to lifecycleOwner" }
+        isAttached = true
+        lifecycle.addObserver(this)
+        registry.registerSavedStateProvider(key, handle.savedStateProvider())
+    }
+
+    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+        if (event === Lifecycle.Event.ON_DESTROY) {
+            isAttached = false
+            source.lifecycle.removeObserver(this)
+        }
+    }
+}
\ 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 e4f76f95..f8457f6 100644
--- a/lifecycle/lifecycle-viewmodel/api/current.txt
+++ b/lifecycle/lifecycle-viewmodel/api/current.txt
@@ -9,6 +9,8 @@
   public interface HasDefaultViewModelProviderFactory {
     method public default androidx.lifecycle.viewmodel.CreationExtras getDefaultViewModelCreationExtras();
     method public androidx.lifecycle.ViewModelProvider.Factory getDefaultViewModelProviderFactory();
+    property public default androidx.lifecycle.viewmodel.CreationExtras defaultViewModelCreationExtras;
+    property public abstract androidx.lifecycle.ViewModelProvider.Factory defaultViewModelProviderFactory;
   }
 
   public abstract class ViewModel {
@@ -78,15 +80,16 @@
 
   public interface ViewModelStoreOwner {
     method public androidx.lifecycle.ViewModelStore getViewModelStore();
+    property public abstract androidx.lifecycle.ViewModelStore viewModelStore;
   }
 
   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 e4f76f95..f8457f6 100644
--- a/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt
@@ -9,6 +9,8 @@
   public interface HasDefaultViewModelProviderFactory {
     method public default androidx.lifecycle.viewmodel.CreationExtras getDefaultViewModelCreationExtras();
     method public androidx.lifecycle.ViewModelProvider.Factory getDefaultViewModelProviderFactory();
+    property public default androidx.lifecycle.viewmodel.CreationExtras defaultViewModelCreationExtras;
+    property public abstract androidx.lifecycle.ViewModelProvider.Factory defaultViewModelProviderFactory;
   }
 
   public abstract class ViewModel {
@@ -78,15 +80,16 @@
 
   public interface ViewModelStoreOwner {
     method public androidx.lifecycle.ViewModelStore getViewModelStore();
+    property public abstract androidx.lifecycle.ViewModelStore viewModelStore;
   }
 
   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 e4f76f95..f8457f6 100644
--- a/lifecycle/lifecycle-viewmodel/api/restricted_current.txt
+++ b/lifecycle/lifecycle-viewmodel/api/restricted_current.txt
@@ -9,6 +9,8 @@
   public interface HasDefaultViewModelProviderFactory {
     method public default androidx.lifecycle.viewmodel.CreationExtras getDefaultViewModelCreationExtras();
     method public androidx.lifecycle.ViewModelProvider.Factory getDefaultViewModelProviderFactory();
+    property public default androidx.lifecycle.viewmodel.CreationExtras defaultViewModelCreationExtras;
+    property public abstract androidx.lifecycle.ViewModelProvider.Factory defaultViewModelProviderFactory;
   }
 
   public abstract class ViewModel {
@@ -78,15 +80,16 @@
 
   public interface ViewModelStoreOwner {
     method public androidx.lifecycle.ViewModelStore getViewModelStore();
+    property public abstract androidx.lifecycle.ViewModelStore viewModelStore;
   }
 
   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..6424d49 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,29 +112,30 @@
         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)
     }
 
     internal class FakeViewModelStoreOwner : ViewModelStoreOwner {
-        override fun getViewModelStore(): ViewModelStore {
-            throw UnsupportedOperationException("not a real ViewModelStoreOwner")
-        }
+        override val viewModelStore: ViewModelStore
+            get() {
+                throw UnsupportedOperationException("not a real ViewModelStoreOwner")
+            }
     }
 }
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/HasDefaultViewModelProviderFactory.java b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/HasDefaultViewModelProviderFactory.java
deleted file mode 100644
index ff7539a..0000000
--- a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/HasDefaultViewModelProviderFactory.java
+++ /dev/null
@@ -1,48 +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.lifecycle;
-
-import androidx.annotation.NonNull;
-import androidx.lifecycle.viewmodel.CreationExtras;
-
-/**
- * Interface that marks a {@link ViewModelStoreOwner} as having a default
- * {@link androidx.lifecycle.ViewModelProvider.Factory} for use with
- * {@link androidx.lifecycle.ViewModelProvider#ViewModelProvider(ViewModelStoreOwner)}.
- */
-public interface HasDefaultViewModelProviderFactory {
-    /**
-     * Returns the default {@link androidx.lifecycle.ViewModelProvider.Factory} that should be
-     * used when no custom {@code Factory} is provided to the
-     * {@link androidx.lifecycle.ViewModelProvider} constructors.
-     *
-     * @return a {@code ViewModelProvider.Factory}
-     */
-    @NonNull
-    ViewModelProvider.Factory getDefaultViewModelProviderFactory();
-
-    /**
-     * Returns the default {@link CreationExtras} that should be passed into the
-     * {@link ViewModelProvider.Factory#create(Class, CreationExtras)} when no overriding
-     * {@link CreationExtras} were passed to the
-     * {@link androidx.lifecycle.ViewModelProvider} constructors.
-     */
-    @NonNull
-    default CreationExtras getDefaultViewModelCreationExtras() {
-        return CreationExtras.Empty.INSTANCE;
-    }
-}
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/HasDefaultViewModelProviderFactory.kt b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/HasDefaultViewModelProviderFactory.kt
new file mode 100644
index 0000000..3a896b1
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/HasDefaultViewModelProviderFactory.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.lifecycle
+
+import androidx.lifecycle.viewmodel.CreationExtras
+
+/**
+ * Interface that marks a [ViewModelStoreOwner] as having a default
+ * [ViewModelProvider.Factory] for use with [ViewModelProvider].
+ */
+interface HasDefaultViewModelProviderFactory {
+    /**
+     * Returns the default [ViewModelProvider.Factory] that should be
+     * used when no custom `Factory` is provided to the
+     * [ViewModelProvider] constructors.
+     */
+    val defaultViewModelProviderFactory: ViewModelProvider.Factory
+
+    /**
+     * Returns the default [CreationExtras] that should be passed into
+     * [ViewModelProvider.Factory.create] when no overriding
+     * [CreationExtras] were passed to the [ViewModelProvider] constructors.
+     */
+    val defaultViewModelCreationExtras: CreationExtras
+        get() = CreationExtras.Empty
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelProvider.kt b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelProvider.kt
index 06c9025..ba62b11 100644
--- a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelProvider.kt
+++ b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelProvider.kt
@@ -109,7 +109,7 @@
      *
      *
      * This method will use the
-     * [default factory][HasDefaultViewModelProviderFactory.getDefaultViewModelProviderFactory]
+     * [default factory][HasDefaultViewModelProviderFactory.defaultViewModelProviderFactory]
      * if the owner implements [HasDefaultViewModelProviderFactory]. Otherwise, a
      * [NewInstanceFactory] will be used.
      */
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelStoreOwner.java b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelStoreOwner.java
deleted file mode 100644
index 19152be..0000000
--- a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelStoreOwner.java
+++ /dev/null
@@ -1,39 +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 androidx.annotation.NonNull;
-
-/**
- * A scope that owns {@link ViewModelStore}.
- * <p>
- * A responsibility of an implementation of this interface is to retain owned ViewModelStore
- * during the configuration changes and call {@link ViewModelStore#clear()}, when this scope is
- * going to be destroyed.
- *
- * @see ViewTreeViewModelStoreOwner
- */
-@SuppressWarnings("WeakerAccess")
-public interface ViewModelStoreOwner {
-    /**
-     * Returns owned {@link ViewModelStore}
-     *
-     * @return a {@code ViewModelStore}
-     */
-    @NonNull
-    ViewModelStore getViewModelStore();
-}
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelStoreOwner.kt b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelStoreOwner.kt
new file mode 100644
index 0000000..5296800
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelStoreOwner.kt
@@ -0,0 +1,33 @@
+/*
+ * 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
+
+/**
+ * A scope that owns [ViewModelStore].
+ *
+ * A responsibility of an implementation of this interface is to retain owned ViewModelStore
+ * during the configuration changes and call [ViewModelStore.clear], when this scope is
+ * going to be destroyed.
+ *
+ * @see ViewTreeViewModelStoreOwner
+ */
+interface ViewModelStoreOwner {
+
+    /**
+     * The owned [ViewModelStore]
+     */
+    val viewModelStore: ViewModelStore
+}
\ No newline at end of file
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/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderTest.kt b/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderTest.kt
index f4423a9..934878d 100644
--- a/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderTest.kt
+++ b/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderTest.kt
@@ -62,8 +62,7 @@
 
     @Test
     fun testOwnedBy() {
-        val store = ViewModelStore()
-        val owner = ViewModelStoreOwner { store }
+        val owner = FakeViewModelStoreOwner()
         val provider = ViewModelProvider(owner, ViewModelProvider.NewInstanceFactory())
         val viewModel = provider[ViewModel1::class.java]
         assertThat(viewModel).isSameInstanceAs(provider[ViewModel1::class.java])
@@ -82,8 +81,7 @@
 
     @Test
     fun testKeyedFactory() {
-        val store = ViewModelStore()
-        val owner = ViewModelStoreOwner { store }
+        val owner = FakeViewModelStoreOwner()
         val explicitlyKeyed: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
             override fun <T : ViewModel> create(
                 modelClass: Class<T>,
@@ -137,9 +135,7 @@
         assertThat(wasCalled[0]).isTrue()
         wasCalled[0] = false
         ViewModelProvider(object : ViewModelStoreOwnerWithCreationExtras() {
-            override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
-                return testFactory
-            }
+            override val defaultViewModelProviderFactory = testFactory
         })["customKey", ViewModel1::class.java]
         assertThat(wasCalled[0]).isTrue()
     }
@@ -165,9 +161,7 @@
         assertThat(wasCalled[0]).isTrue()
         wasCalled[0] = false
         ViewModelProvider(object : ViewModelStoreOwnerWithCreationExtras() {
-            override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
-                return testFactory
-            }
+            override val defaultViewModelProviderFactory = testFactory
         })["customKey", ViewModel1::class.java]
         assertThat(wasCalled[0]).isTrue()
     }
@@ -176,13 +170,14 @@
         private val mStore: ViewModelStore,
         private val mFactory: ViewModelProvider.Factory
     ) : ViewModelStoreOwner, HasDefaultViewModelProviderFactory {
-        override fun getViewModelStore(): ViewModelStore {
-            return mStore
-        }
+        override val viewModelStore: ViewModelStore = mStore
+        override val defaultViewModelProviderFactory = mFactory
+    }
 
-        override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
-            return mFactory
-        }
+    class FakeViewModelStoreOwner internal constructor(
+        store: ViewModelStore = ViewModelStore()
+    ) : ViewModelStoreOwner {
+        override val viewModelStore: ViewModelStore = store
     }
 
     open class ViewModel1 : ViewModel() {
@@ -203,20 +198,18 @@
 
     internal open class ViewModelStoreOwnerWithCreationExtras : ViewModelStoreOwner,
         HasDefaultViewModelProviderFactory {
-        private val viewModelStore = ViewModelStore()
-        override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
-            throw UnsupportedOperationException()
-        }
+        private val _viewModelStore = ViewModelStore()
+        override val defaultViewModelProviderFactory: ViewModelProvider.Factory
+            get() = throw UnsupportedOperationException()
 
-        override fun getDefaultViewModelCreationExtras(): CreationExtras {
-            val extras = MutableCreationExtras()
-            extras[TEST_KEY] = TEST_VALUE
-            return extras
-        }
+        override val defaultViewModelCreationExtras: CreationExtras
+            get() {
+                val extras = MutableCreationExtras()
+                extras[TEST_KEY] = TEST_VALUE
+                return extras
+            }
 
-        override fun getViewModelStore(): ViewModelStore {
-            return viewModelStore
-        }
+        override val viewModelStore: ViewModelStore = _viewModelStore
     }
 }
 
diff --git a/lint-checks/integration-tests/src/main/java/sample/annotation/provider/ExperimentalSampleAnnotationJava.java b/lint-checks/integration-tests/src/main/java/sample/annotation/provider/ExperimentalSampleAnnotationJava.java
index d04506a..5cf74d9 100644
--- a/lint-checks/integration-tests/src/main/java/sample/annotation/provider/ExperimentalSampleAnnotationJava.java
+++ b/lint-checks/integration-tests/src/main/java/sample/annotation/provider/ExperimentalSampleAnnotationJava.java
@@ -22,8 +22,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 
-@SuppressWarnings("deprecation")
-@kotlin.Experimental
+@kotlin.RequiresOptIn
 @Retention(CLASS)
 @Target({ElementType.TYPE, ElementType.METHOD})
 public @interface ExperimentalSampleAnnotationJava {}
diff --git a/lint-checks/src/main/java/androidx/build/lint/CameraXQuirksClassDetector.kt b/lint-checks/src/main/java/androidx/build/lint/CameraXQuirksClassDetector.kt
index 842f9c3..7326529 100644
--- a/lint-checks/src/main/java/androidx/build/lint/CameraXQuirksClassDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/CameraXQuirksClassDetector.kt
@@ -20,6 +20,7 @@
 import com.android.tools.lint.detector.api.Category
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Incident
 import com.android.tools.lint.detector.api.Issue
 import com.android.tools.lint.detector.api.JavaContext
 import com.android.tools.lint.detector.api.Scope
@@ -38,7 +39,7 @@
     override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
 
         override fun visitClass(node: UClass) {
-            val isQuirk = node.implementsList?.referenceElements?.find { it ->
+            val isQuirk = node.implementsList?.referenceElements?.find {
                 it.referenceName!!.endsWith("Quirk")
             } != null
 
@@ -59,12 +60,13 @@
                          *     Device(s):
                         """.trimIndent()
 
-                    context.report(
-                        CameraXQuirksClassDetector.ISSUE, node,
-                        context.getNameLocation(node),
-                        "CameraX quirks should include this template in the javadoc:" +
-                            "\n\n$implForInsertion\n\n"
-                    )
+                    val incident = Incident(context)
+                        .issue(ISSUE)
+                        .message("CameraX quirks should include this template in the javadoc:" +
+                            "\n\n$implForInsertion\n\n")
+                        .location(context.getNameLocation(node))
+                        .scope(node)
+                    context.report(incident)
                 }
             }
         }
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/media/media/api/api_lint.ignore b/media/media/api/api_lint.ignore
index d098ab4..2a61822 100644
--- a/media/media/api/api_lint.ignore
+++ b/media/media/api/api_lint.ignore
@@ -73,6 +73,24 @@
     Intent action constant name must be ACTION_FOO: BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST
 IntentName: androidx.media.utils.MediaConstants#DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST:
     Intent action constant name must be ACTION_FOO: DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST
+IntentName: androidx.media.utils.MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_EXTRAS:
+    Intent action constant name must be ACTION_FOO: EXTRAS_KEY_CUSTOM_BROWSER_ACTION_EXTRAS
+IntentName: androidx.media.utils.MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI:
+    Intent action constant name must be ACTION_FOO: EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI
+IntentName: androidx.media.utils.MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID:
+    Intent action constant name must be ACTION_FOO: EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID
+IntentName: androidx.media.utils.MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL:
+    Intent action constant name must be ACTION_FOO: EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL
+IntentName: androidx.media.utils.MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID:
+    Intent action constant name must be ACTION_FOO: EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID
+IntentName: androidx.media.utils.MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE:
+    Intent action constant name must be ACTION_FOO: EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE
+IntentName: androidx.media.utils.MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE:
+    Intent action constant name must be ACTION_FOO: EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE
+IntentName: androidx.media.utils.MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM:
+    Intent action constant name must be ACTION_FOO: EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM
+IntentName: androidx.media.utils.MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM:
+    Intent action constant name must be ACTION_FOO: EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM
 
 
 InterfaceConstant: androidx.media.MediaBrowserServiceCompat#SERVICE_INTERFACE:
diff --git a/media/media/api/current.txt b/media/media/api/current.txt
index 0f1bb9d..3e65d18 100644
--- a/media/media/api/current.txt
+++ b/media/media/api/current.txt
@@ -712,12 +712,12 @@
 package androidx.media.utils {
 
   public final class MediaConstants {
-    field public static final String BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT = "androidx.media.MediaBrowserCompat.CUSTOM_BROWSER_ACTION_LIMIT";
+    field public static final String BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT = "androidx.media.utils.MediaBrowserCompat.extras.CUSTOM_BROWSER_ACTION_LIMIT";
     field public static final String BROWSER_ROOT_HINTS_KEY_MEDIA_ART_SIZE_PIXELS = "android.media.extras.MEDIA_ART_SIZE_HINT_PIXELS";
     field public static final String BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT = "androidx.media.MediaBrowserCompat.Extras.KEY_ROOT_CHILDREN_LIMIT";
     field public static final String BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS = "androidx.media.MediaBrowserCompat.Extras.KEY_ROOT_CHILDREN_SUPPORTED_FLAGS";
     field public static final String BROWSER_SERVICE_EXTRAS_KEY_APPLICATION_PREFERENCES_USING_CAR_APP_LIBRARY_INTENT = "androidx.media.BrowserRoot.Extras.APPLICATION_PREFERENCES_USING_CAR_APP_LIBRARY_INTENT";
-    field public static final String BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST = "android.media.extra.CUSTOM_BROWSER_ACTION_ROOT_LIST";
+    field public static final String BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST = "androidx.media.utils.extras.CUSTOM_BROWSER_ACTION_ROOT_LIST";
     field public static final String BROWSER_SERVICE_EXTRAS_KEY_FAVORITES_MEDIA_ITEM = "androidx.media.BrowserRoot.Extras.FAVORITES_MEDIA_ITEM";
     field public static final String BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED = "android.media.browse.SEARCH_SUPPORTED";
     field public static final String DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE = "androidx.media.MediaItem.Extras.COMPLETION_PERCENTAGE";
@@ -726,7 +726,7 @@
     field public static final String DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE = "android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT";
     field public static final String DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE = "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT";
     field public static final String DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM = "android.media.browse.CONTENT_STYLE_SINGLE_ITEM_HINT";
-    field public static final String DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST = "android.media.extra.CUSTOM_BROWSER_ACTION_ID_LIST";
+    field public static final String DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST = "androidx.media.utils.extras.CUSTOM_BROWSER_ACTION_ID_LIST";
     field public static final int DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED = 2; // 0x2
     field public static final int DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED = 0; // 0x0
     field public static final int DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED = 1; // 0x1
@@ -734,15 +734,15 @@
     field public static final int DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM = 3; // 0x3
     field public static final int DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM = 2; // 0x2
     field public static final int DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM = 1; // 0x1
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_EXTRAS = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_EXTRAS";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_ICON_URI = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_ICON_URI";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_ID = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_ID";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_LABEL = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_LABEL";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_EXTRAS = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_EXTRAS";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_ICON_URI";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_ID";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_LABEL";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM";
     field public static final String METADATA_KEY_CONTENT_ID = "androidx.media.MediaMetadatCompat.METADATA_KEY_CONTENT_ID";
     field public static final String METADATA_KEY_IS_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT";
     field public static final String METADATA_KEY_IS_EXPLICIT = "android.media.IS_EXPLICIT";
diff --git a/media/media/api/public_plus_experimental_current.txt b/media/media/api/public_plus_experimental_current.txt
index 0f1bb9d..3e65d18 100644
--- a/media/media/api/public_plus_experimental_current.txt
+++ b/media/media/api/public_plus_experimental_current.txt
@@ -712,12 +712,12 @@
 package androidx.media.utils {
 
   public final class MediaConstants {
-    field public static final String BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT = "androidx.media.MediaBrowserCompat.CUSTOM_BROWSER_ACTION_LIMIT";
+    field public static final String BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT = "androidx.media.utils.MediaBrowserCompat.extras.CUSTOM_BROWSER_ACTION_LIMIT";
     field public static final String BROWSER_ROOT_HINTS_KEY_MEDIA_ART_SIZE_PIXELS = "android.media.extras.MEDIA_ART_SIZE_HINT_PIXELS";
     field public static final String BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT = "androidx.media.MediaBrowserCompat.Extras.KEY_ROOT_CHILDREN_LIMIT";
     field public static final String BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS = "androidx.media.MediaBrowserCompat.Extras.KEY_ROOT_CHILDREN_SUPPORTED_FLAGS";
     field public static final String BROWSER_SERVICE_EXTRAS_KEY_APPLICATION_PREFERENCES_USING_CAR_APP_LIBRARY_INTENT = "androidx.media.BrowserRoot.Extras.APPLICATION_PREFERENCES_USING_CAR_APP_LIBRARY_INTENT";
-    field public static final String BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST = "android.media.extra.CUSTOM_BROWSER_ACTION_ROOT_LIST";
+    field public static final String BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST = "androidx.media.utils.extras.CUSTOM_BROWSER_ACTION_ROOT_LIST";
     field public static final String BROWSER_SERVICE_EXTRAS_KEY_FAVORITES_MEDIA_ITEM = "androidx.media.BrowserRoot.Extras.FAVORITES_MEDIA_ITEM";
     field public static final String BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED = "android.media.browse.SEARCH_SUPPORTED";
     field public static final String DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE = "androidx.media.MediaItem.Extras.COMPLETION_PERCENTAGE";
@@ -726,7 +726,7 @@
     field public static final String DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE = "android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT";
     field public static final String DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE = "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT";
     field public static final String DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM = "android.media.browse.CONTENT_STYLE_SINGLE_ITEM_HINT";
-    field public static final String DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST = "android.media.extra.CUSTOM_BROWSER_ACTION_ID_LIST";
+    field public static final String DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST = "androidx.media.utils.extras.CUSTOM_BROWSER_ACTION_ID_LIST";
     field public static final int DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED = 2; // 0x2
     field public static final int DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED = 0; // 0x0
     field public static final int DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED = 1; // 0x1
@@ -734,15 +734,15 @@
     field public static final int DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM = 3; // 0x3
     field public static final int DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM = 2; // 0x2
     field public static final int DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM = 1; // 0x1
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_EXTRAS = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_EXTRAS";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_ICON_URI = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_ICON_URI";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_ID = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_ID";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_LABEL = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_LABEL";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_EXTRAS = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_EXTRAS";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_ICON_URI";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_ID";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_LABEL";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM";
     field public static final String METADATA_KEY_CONTENT_ID = "androidx.media.MediaMetadatCompat.METADATA_KEY_CONTENT_ID";
     field public static final String METADATA_KEY_IS_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT";
     field public static final String METADATA_KEY_IS_EXPLICIT = "android.media.IS_EXPLICIT";
diff --git a/media/media/api/restricted_current.txt b/media/media/api/restricted_current.txt
index fe59f23..35c24b8 100644
--- a/media/media/api/restricted_current.txt
+++ b/media/media/api/restricted_current.txt
@@ -750,12 +750,12 @@
 package androidx.media.utils {
 
   public final class MediaConstants {
-    field public static final String BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT = "androidx.media.MediaBrowserCompat.CUSTOM_BROWSER_ACTION_LIMIT";
+    field public static final String BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT = "androidx.media.utils.MediaBrowserCompat.extras.CUSTOM_BROWSER_ACTION_LIMIT";
     field public static final String BROWSER_ROOT_HINTS_KEY_MEDIA_ART_SIZE_PIXELS = "android.media.extras.MEDIA_ART_SIZE_HINT_PIXELS";
     field public static final String BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT = "androidx.media.MediaBrowserCompat.Extras.KEY_ROOT_CHILDREN_LIMIT";
     field public static final String BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS = "androidx.media.MediaBrowserCompat.Extras.KEY_ROOT_CHILDREN_SUPPORTED_FLAGS";
     field public static final String BROWSER_SERVICE_EXTRAS_KEY_APPLICATION_PREFERENCES_USING_CAR_APP_LIBRARY_INTENT = "androidx.media.BrowserRoot.Extras.APPLICATION_PREFERENCES_USING_CAR_APP_LIBRARY_INTENT";
-    field public static final String BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST = "android.media.extra.CUSTOM_BROWSER_ACTION_ROOT_LIST";
+    field public static final String BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST = "androidx.media.utils.extras.CUSTOM_BROWSER_ACTION_ROOT_LIST";
     field public static final String BROWSER_SERVICE_EXTRAS_KEY_FAVORITES_MEDIA_ITEM = "androidx.media.BrowserRoot.Extras.FAVORITES_MEDIA_ITEM";
     field public static final String BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED = "android.media.browse.SEARCH_SUPPORTED";
     field public static final String DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE = "androidx.media.MediaItem.Extras.COMPLETION_PERCENTAGE";
@@ -764,7 +764,7 @@
     field public static final String DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE = "android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT";
     field public static final String DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE = "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT";
     field public static final String DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM = "android.media.browse.CONTENT_STYLE_SINGLE_ITEM_HINT";
-    field public static final String DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST = "android.media.extra.CUSTOM_BROWSER_ACTION_ID_LIST";
+    field public static final String DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST = "androidx.media.utils.extras.CUSTOM_BROWSER_ACTION_ID_LIST";
     field public static final int DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED = 2; // 0x2
     field public static final int DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED = 0; // 0x0
     field public static final int DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED = 1; // 0x1
@@ -772,15 +772,15 @@
     field public static final int DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM = 3; // 0x3
     field public static final int DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM = 2; // 0x2
     field public static final int DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM = 1; // 0x1
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_EXTRAS = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_EXTRAS";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_ICON_URI = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_ICON_URI";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_ID = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_ID";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_LABEL = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_LABEL";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM";
-    field public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM = "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_EXTRAS = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_EXTRAS";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_ICON_URI";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_ID";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_LABEL";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM";
+    field public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM = "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM";
     field public static final String METADATA_KEY_CONTENT_ID = "androidx.media.MediaMetadatCompat.METADATA_KEY_CONTENT_ID";
     field public static final String METADATA_KEY_IS_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT";
     field public static final String METADATA_KEY_IS_EXPLICIT = "android.media.IS_EXPLICIT";
diff --git a/media/media/src/main/java/androidx/media/utils/MediaConstants.java b/media/media/src/main/java/androidx/media/utils/MediaConstants.java
index ebbc9e4..123be21 100644
--- a/media/media/src/main/java/androidx/media/utils/MediaConstants.java
+++ b/media/media/src/main/java/androidx/media/utils/MediaConstants.java
@@ -552,42 +552,42 @@
      * custom action.
      *
      * <p>A custom browser action is defined by an
-     * {@linkplain MediaConstants#EXTRA_KEY_CUSTOM_BROWSER_ACTION_ID action ID}, an
-     * {@linkplain MediaConstants#EXTRA_KEY_CUSTOM_BROWSER_ACTION_LABEL action label},
-     * an {@linkplain MediaConstants#EXTRA_KEY_CUSTOM_BROWSER_ACTION_ICON_URI action icon
+     * {@linkplain MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID action ID}, an
+     * {@linkplain MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL action label},
+     * an {@linkplain MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI action icon
      * URI}, and optionally an
-     * {@linkplain MediaConstants#EXTRA_KEY_CUSTOM_BROWSER_ACTION_EXTRAS action extras
+     * {@linkplain MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_EXTRAS action extras
      * bundle}.
      *
      * <p>Custom browser action example:
      * <ul>
      *   <li>Action ID: "com.example.audioapp.download"
      *     <ul>
-     *       <li>Key: {@link MediaConstants#EXTRA_KEY_CUSTOM_BROWSER_ACTION_ID}
+     *       <li>Key: {@link MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID}
      *     </ul>
      *   </li>
      *     <li>Action label: "Download Song"
      *     <ul>
-     *       <li>Key: {@link MediaConstants#EXTRA_KEY_CUSTOM_BROWSER_ACTION_LABEL}
+     *       <li>Key: {@link MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL}
      *       <li>Localized String label for action
      *     </ul>
      *   </li>
      *     <li>Action Icon URI: "content://com.example.public/download"
      *     <ul>
-     *       <li>Key: {@link MediaConstants#EXTRA_KEY_CUSTOM_BROWSER_ACTION_ICON_URI}
+     *       <li>Key: {@link MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI}
      *       <li>Tintable vector drawable
      *     </ul>
      *   </li>
      *     <li>Action extras: {bundle}
      *     <ul>
-     *       <li>Key: {@link MediaConstants#EXTRA_KEY_CUSTOM_BROWSER_ACTION_EXTRAS}
+     *       <li>Key: {@link MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_EXTRAS}
      *       <li>Bundle extras
      *     </ul>
      *   </li>
      * </ul>
      */
     public static final String BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST =
-            "android.media.extra.CUSTOM_BROWSER_ACTION_ROOT_LIST";
+            "androidx.media.utils.extras.CUSTOM_BROWSER_ACTION_ROOT_LIST";
 
     /**
      * {@link Bundle} key used to define a string list of custom browser actions for a
@@ -616,7 +616,7 @@
      * @see MediaConstants#BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST
      */
     public static final String DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST =
-            "android.media.extra.CUSTOM_BROWSER_ACTION_ID_LIST";
+            "androidx.media.utils.extras.CUSTOM_BROWSER_ACTION_ID_LIST";
 
     /**
      * {@link Bundle} key used to define the ID for a custom browser action.
@@ -626,8 +626,8 @@
      * @see MediaConstants#BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST
      * @see MediaConstants#DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST
      */
-    public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_ID =
-            "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_ID";
+    public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID =
+            "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_ID";
 
     /**
      * {@link Bundle} key used to define the label for a custom browser action. Label is a localized
@@ -638,8 +638,8 @@
      * @see MediaConstants#BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST
      * @see MediaConstants#DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST
      */
-    public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_LABEL =
-            "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_LABEL";
+    public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL =
+            "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_LABEL";
     /**
      * {@link Bundle} key used to define the icon URI for a custom browser action.
      *
@@ -648,8 +648,8 @@
      * @see MediaConstants#BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST
      * @see MediaConstants#DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST
      */
-    public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_ICON_URI =
-            "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_ICON_URI";
+    public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI =
+            "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_ICON_URI";
     /**
      * {@link Bundle} key used to define an extras bundle for a custom browser action.
      *
@@ -661,14 +661,14 @@
      * @see MediaConstants#BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST
      * @see MediaConstants#DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST
      */
-    public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_EXTRAS =
-            "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_EXTRAS";
+    public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_EXTRAS =
+            "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_EXTRAS";
     /**
      * {@link Bundle} key used to define the total number of actions allowed per item. Passed to
      * {@link MediaBrowserServiceCompat} using
      * {@link MediaBrowserServiceCompat#onGetRoot(String, int, Bundle)} in root hints bundle.
      *
-     * <p>Presence and non-zero value of this key in the root hints indicates that custom browse
+     * <p>Presence of this key and positive value in the root hints indicates that custom browse
      * actions feature is supported. Actions beyond this limit will be truncated.
      *
      * <p>TYPE: int, number of actions each item is limited to.
@@ -678,7 +678,7 @@
      * @see MediaConstants#DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST
      */
     public static final String BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT =
-            "androidx.media.MediaBrowserCompat.CUSTOM_BROWSER_ACTION_LIMIT";
+            "androidx.media.utils.MediaBrowserCompat.extras.CUSTOM_BROWSER_ACTION_LIMIT";
 
     /**
      * {@link Bundle} key used to define the ID of the {@link MediaBrowserCompat.MediaItem}
@@ -686,7 +686,8 @@
      *
      * <p>A {@link MediaBrowserCompat} that supports custom browser actions can set this key
      * in the parameter extra bundle when using
-     * {@link MediaBrowserCompat#sendCustomAction(String, Bundle, MediaBrowserCompat.CustomActionCallback)}.
+     * {@link MediaBrowserCompat#sendCustomAction(String, Bundle,
+     * MediaBrowserCompat.CustomActionCallback)}.
      *
      * <p>A {@link MediaBrowserServiceCompat} that supports custom browser actions should override
      * {@link MediaBrowserServiceCompat#onCustomAction(
@@ -701,22 +702,24 @@
      * @see
      * MediaBrowserCompat#sendCustomAction(String, Bundle, MediaBrowserCompat.CustomActionCallback)
      */
-    public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID =
-            "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID";
+    public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID =
+            "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID";
     /**
-     * {@link Bundle} key set in custom browser action extra bundle or
-     * {@link MediaBrowserServiceCompat.Result} to indicate which browse node should be displayed
-     * next.
+     * {@link Bundle} key set in {@link MediaBrowserServiceCompat.Result} to indicate which
+     * browse node should be displayed next.
      *
      * <p>A {@link MediaBrowserServiceCompat} that supports custom browser actions can set this key
      * in the {@link MediaBrowserServiceCompat.Result} passed in
-     * {@link MediaBrowserServiceCompat#onCustomAction(String, Bundle, MediaBrowserServiceCompat.Result)}.
-     *
-     * If this key is present in a {@link MediaBrowserCompat.CustomActionCallback} data
-     * {@link Bundle} the {@link MediaBrowserCompat} will update the current browse root when
-     * {@link MediaBrowserCompat.CustomActionCallback#onProgressUpdate(String, Bundle, Bundle)} or
+     * {@link MediaBrowserServiceCompat#onCustomAction(String, Bundle,
+     * MediaBrowserServiceCompat.Result)}.
+     * <p>If this key is present in a {@link MediaBrowserCompat.CustomActionCallback} data
+     * {@link Bundle} the {@link MediaBrowserCompat} will update the current browse node when
      * {@link MediaBrowserCompat.CustomActionCallback#onResult(String, Bundle, Bundle)} is called by
-     * the {@link MediaBrowserServiceCompat}.
+     * the {@link MediaBrowserServiceCompat}. The new browse node will be fetched by
+     * {@link MediaBrowserCompat#getItem(String, MediaBrowserCompat.ItemCallback)}.
+     * <p>A {@link MediaBrowserServiceCompat} that supports custom browser actions must implement
+     * {@link MediaBrowserServiceCompat#onLoadItem(String, MediaBrowserServiceCompat.Result)} to
+     * use this feature.
      *
      * <p>TYPE: string, string {@link MediaBrowserCompat.MediaItem} ID to set as new browse node.
      *
@@ -726,55 +729,51 @@
      * @see
      * MediaBrowserServiceCompat#onCustomAction(String, Bundle, MediaBrowserServiceCompat.Result)
      */
-    public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE =
-            "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE";
+    public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE =
+            "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE";
 
     /**
-     * {@link Bundle} key set in custom browser action extra bundle or
-     * {@link MediaBrowserServiceCompat.Result} to show the playing item.
+     * {@link Bundle} key set in {@link MediaBrowserServiceCompat.Result} to show the
+     * currently playing item.
      *
      * <p>A {@link MediaBrowserServiceCompat} that supports custom browser actions can set this key
-     * in the
-     * {@link MediaBrowserServiceCompat.Result} passed in
-     * {@link MediaBrowserServiceCompat#onCustomAction(String, Bundle, MediaBrowserServiceCompat.Result)}.
-     *
-     * <p>If this key is present in {@link MediaBrowserCompat.CustomActionCallback}
-     * {@link MediaBrowserServiceCompat.Result}, the currently playing item will be shown
-     * when result is handled by {@link MediaBrowserCompat}.
-     *
-     * <P>TYPE: string, string {@link MediaBrowserCompat.MediaItem} ID of item to show, or if no
-     * value is set then show currently playing
-     * {@link MediaBrowserCompat.MediaItem}
-     *
+     * in the {@link MediaBrowserServiceCompat.Result} passed in
+     * {@link MediaBrowserServiceCompat#onCustomAction(String, Bundle,
+     * MediaBrowserServiceCompat.Result)}.
+     * <p>If this key is present and the value is true in
+     * {@link MediaBrowserCompat.CustomActionCallback}
+     * {@link MediaBrowserServiceCompat.Result}, the currently playing item will be shown when
+     * {@link MediaBrowserCompat.CustomActionCallback#onResult(String, Bundle, Bundle)} is called by
+     * the {@link MediaBrowserServiceCompat}.
+     * <p>TYPE: boolean, boolean value of true will show currently playing item.
      * @see
      * MediaBrowserCompat#sendCustomAction(String, Bundle, MediaBrowserCompat.CustomActionCallback)
      * @see MediaBrowserCompat.CustomActionCallback
      * @see
      * MediaBrowserServiceCompat#onCustomAction(String, Bundle, MediaBrowserServiceCompat.Result)
      */
-    public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM =
-            "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM";
+    public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM =
+            "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM";
 
     /**
-     * {@link Bundle} key set in custom browser action extra bundle or
-     * {@link MediaBrowserServiceCompat.Result} to refresh a
+     * {@link Bundle} key set in {@link MediaBrowserServiceCompat.Result} to refresh a
      * {@link MediaBrowserCompat.MediaItem} in the browse tree.
      *
      * <p>A {@link MediaBrowserServiceCompat} that supports custom browser actions can set this key
      * in the {@link MediaBrowserServiceCompat.Result} passed in
-     * {@link MediaBrowserServiceCompat#onCustomAction(String, Bundle, MediaBrowserServiceCompat.Result)}.
+     * {@link MediaBrowserServiceCompat#onCustomAction(String, Bundle,
+     * MediaBrowserServiceCompat.Result)}.
      *
-     * <p>A {@link MediaBrowserCompat} that supports custom browser actions will refresh
-     * the items custom browser action IDs by using
-     * {@link MediaBrowserCompat#getItem(String, MediaBrowserCompat.ItemCallback)}.
-     * A {@link MediaBrowserServiceCompat} that supports custom browser actions  must implement
+     * <p>If this key is present in {@link MediaBrowserCompat.CustomActionCallback}
+     * {@link MediaBrowserServiceCompat.Result}, the item will be refreshed with
+     * {@link MediaBrowserCompat#getItem(String, MediaBrowserCompat.ItemCallback)} when
+     * {@link MediaBrowserCompat.CustomActionCallback#onProgressUpdate(String, Bundle, Bundle)} or
+     * {@link MediaBrowserCompat.CustomActionCallback#onResult(String, Bundle, Bundle)} is called by
+     * the {@link MediaBrowserServiceCompat}.
+     * <p>A {@link MediaBrowserServiceCompat} that supports custom browser actions must
+     * implement
      * {@link MediaBrowserServiceCompat#onLoadItem(String, MediaBrowserServiceCompat.Result)} in
-     * order to refresh actions in an item.
-     *
-     * The key in the action result bundle will trigger the item refresh
-     * when the {@link MediaBrowserCompat.CustomActionCallback} is called, which is passed
-     * to the {@link MediaBrowserServiceCompat} using
-     * {@link MediaBrowserCompat#sendCustomAction(String, Bundle, MediaBrowserCompat.CustomActionCallback)}
+     * order to update the state of the item.
      *
      * <p>TYPE: string, string {@link MediaBrowserCompat.MediaItem} ID to refresh.
      *
@@ -784,22 +783,24 @@
      * @see
      * MediaBrowserServiceCompat#onCustomAction(String, Bundle, MediaBrowserServiceCompat.Result)
      */
-    public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM =
-            "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM";
+    public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM =
+            "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM";
     /**
-     * {@link Bundle} key set in custom browser action extra bundle or
-     * {@link MediaBrowserServiceCompat.Result} to set a message for user.
+     * {@link Bundle} key set in {@link MediaBrowserServiceCompat.Result} to set a message for
+     * the user.
      *
      * <p>A {@link MediaBrowserServiceCompat} that supports custom browser actions can set this key
      * in the {@link MediaBrowserServiceCompat.Result} passed in
-     * {@link MediaBrowserServiceCompat#onCustomAction(String, Bundle, MediaBrowserServiceCompat.Result)}.
+     * {@link MediaBrowserServiceCompat#onCustomAction(String, Bundle,
+     * MediaBrowserServiceCompat.Result)}.
      *
-     * The key in the action result bundle will trigger the message
-     * handling when the {@link MediaBrowserCompat.CustomActionCallback} is called, which is passed
-     * to the {@link MediaBrowserServiceCompat} using
-     * {@link MediaBrowserCompat#sendCustomAction(String, Bundle, MediaBrowserCompat.CustomActionCallback)}
+     * <p>If this key is present in {@link MediaBrowserCompat.CustomActionCallback}
+     * {@link MediaBrowserServiceCompat.Result}, the message will be shown to the user when
+     * {@link MediaBrowserCompat.CustomActionCallback#onProgressUpdate(String, Bundle, Bundle)} or
+     * {@link MediaBrowserCompat.CustomActionCallback#onResult(String, Bundle, Bundle)} is called by
+     * the {@link MediaBrowserServiceCompat}.
      *
-     * <p>TYPE: string, localized message string to show user.
+     * <p>TYPE: string, localized message string to show the user.
      *
      * @see
      * MediaBrowserCompat#sendCustomAction(String, Bundle, MediaBrowserCompat.CustomActionCallback)
@@ -807,8 +808,8 @@
      * @see
      * MediaBrowserServiceCompat#onCustomAction(String, Bundle, MediaBrowserServiceCompat.Result)
      */
-    public static final String EXTRA_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE =
-            "androidx.media.utils.extra.KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE";
+    public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE =
+            "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE";
 
     /**
      * Bundle key used for the media ID in {@link PlaybackStateCompat playback state} extras. It's
diff --git a/media2/media2-widget/src/main/res/values-ky/strings.xml b/media2/media2-widget/src/main/res/values-ky/strings.xml
index 2452d322..c53b918 100644
--- a/media2/media2-widget/src/main/res/values-ky/strings.xml
+++ b/media2/media2-widget/src/main/res/values-ky/strings.xml
@@ -35,7 +35,7 @@
     <string name="mcv2_overflow_left_button_desc" msgid="2749567167276435888">"Мурунку баскыч тизмесине кайтуу"</string>
     <string name="mcv2_overflow_right_button_desc" msgid="7388732945289831383">"Дагы баскычтарды көрүү"</string>
     <string name="mcv2_seek_bar_desc" msgid="24915699029009384">"Ойнотуу көрсөткүчү"</string>
-    <string name="mcv2_settings_button_desc" msgid="811917224044739656">"Жөндөөлөр"</string>
+    <string name="mcv2_settings_button_desc" msgid="811917224044739656">"Параметрлер"</string>
     <string name="mcv2_cc_is_on" msgid="5427119422911561783">"Коштомо жазуу күйүк. Жашыруу үчүн чыкылдатыңыз."</string>
     <string name="mcv2_cc_is_off" msgid="2380791179816122456">"Коштомо жазуу өчүк. Аны көрсөтүү үчүн чыкылдатыңыз."</string>
     <string name="mcv2_replay_button_desc" msgid="3128622733570179596">"Кайрадан ойнотуу"</string>
diff --git a/mediarouter/mediarouter-testing/api/1.4.0-beta02.txt b/mediarouter/mediarouter-testing/api/1.4.0-beta02.txt
new file mode 100644
index 0000000..14e1df6
--- /dev/null
+++ b/mediarouter/mediarouter-testing/api/1.4.0-beta02.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.mediarouter.testing {
+
+  public class MediaRouterTestHelper {
+    method @MainThread public static void resetMediaRouter();
+  }
+
+}
+
diff --git a/mediarouter/mediarouter-testing/api/public_plus_experimental_1.4.0-beta02.txt b/mediarouter/mediarouter-testing/api/public_plus_experimental_1.4.0-beta02.txt
new file mode 100644
index 0000000..14e1df6
--- /dev/null
+++ b/mediarouter/mediarouter-testing/api/public_plus_experimental_1.4.0-beta02.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.mediarouter.testing {
+
+  public class MediaRouterTestHelper {
+    method @MainThread public static void resetMediaRouter();
+  }
+
+}
+
diff --git a/camera/camera-viewfinder/api/res-1.2.0-beta02.txt b/mediarouter/mediarouter-testing/api/res-1.4.0-beta02.txt
similarity index 100%
copy from camera/camera-viewfinder/api/res-1.2.0-beta02.txt
copy to mediarouter/mediarouter-testing/api/res-1.4.0-beta02.txt
diff --git a/mediarouter/mediarouter-testing/api/restricted_1.4.0-beta02.txt b/mediarouter/mediarouter-testing/api/restricted_1.4.0-beta02.txt
new file mode 100644
index 0000000..14e1df6
--- /dev/null
+++ b/mediarouter/mediarouter-testing/api/restricted_1.4.0-beta02.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.mediarouter.testing {
+
+  public class MediaRouterTestHelper {
+    method @MainThread public static void resetMediaRouter();
+  }
+
+}
+
diff --git a/mediarouter/mediarouter/api/1.4.0-beta02.txt b/mediarouter/mediarouter/api/1.4.0-beta02.txt
new file mode 100644
index 0000000..b1cf094
--- /dev/null
+++ b/mediarouter/mediarouter/api/1.4.0-beta02.txt
@@ -0,0 +1,558 @@
+// Signature format: 4.0
+package androidx.mediarouter.app {
+
+  public class MediaRouteActionProvider extends androidx.core.view.ActionProvider {
+    ctor public MediaRouteActionProvider(android.content.Context);
+    method @Deprecated public void enableDynamicGroup();
+    method public androidx.mediarouter.app.MediaRouteDialogFactory getDialogFactory();
+    method public androidx.mediarouter.app.MediaRouteButton? getMediaRouteButton();
+    method public androidx.mediarouter.media.MediaRouteSelector getRouteSelector();
+    method public android.view.View onCreateActionView();
+    method public androidx.mediarouter.app.MediaRouteButton onCreateMediaRouteButton();
+    method public void setAlwaysVisible(boolean);
+    method public void setDialogFactory(androidx.mediarouter.app.MediaRouteDialogFactory);
+    method public void setRouteSelector(androidx.mediarouter.media.MediaRouteSelector);
+  }
+
+  public class MediaRouteButton extends android.view.View {
+    ctor public MediaRouteButton(android.content.Context);
+    ctor public MediaRouteButton(android.content.Context, android.util.AttributeSet?);
+    ctor public MediaRouteButton(android.content.Context, android.util.AttributeSet?, int);
+    method @Deprecated public void enableDynamicGroup();
+    method public androidx.mediarouter.app.MediaRouteDialogFactory getDialogFactory();
+    method public androidx.mediarouter.media.MediaRouteSelector getRouteSelector();
+    method public void onAttachedToWindow();
+    method public void onDetachedFromWindow();
+    method public void setAlwaysVisible(boolean);
+    method public void setDialogFactory(androidx.mediarouter.app.MediaRouteDialogFactory);
+    method public void setRemoteIndicatorDrawable(android.graphics.drawable.Drawable?);
+    method public void setRouteSelector(androidx.mediarouter.media.MediaRouteSelector);
+    method public boolean showDialog();
+  }
+
+  public class MediaRouteChooserDialog extends androidx.appcompat.app.AppCompatDialog {
+    ctor public MediaRouteChooserDialog(android.content.Context);
+    ctor public MediaRouteChooserDialog(android.content.Context, int);
+    method public androidx.mediarouter.media.MediaRouteSelector getRouteSelector();
+    method public boolean onFilterRoute(androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void onFilterRoutes(java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!>);
+    method public void refreshRoutes();
+    method public void setRouteSelector(androidx.mediarouter.media.MediaRouteSelector);
+  }
+
+  public class MediaRouteChooserDialogFragment extends androidx.fragment.app.DialogFragment {
+    ctor public MediaRouteChooserDialogFragment();
+    method public androidx.mediarouter.media.MediaRouteSelector getRouteSelector();
+    method public androidx.mediarouter.app.MediaRouteChooserDialog onCreateChooserDialog(android.content.Context, android.os.Bundle?);
+    method public void setRouteSelector(androidx.mediarouter.media.MediaRouteSelector);
+  }
+
+  public class MediaRouteControllerDialog extends androidx.appcompat.app.AlertDialog {
+    ctor public MediaRouteControllerDialog(android.content.Context);
+    ctor public MediaRouteControllerDialog(android.content.Context, int);
+    method public android.view.View? getMediaControlView();
+    method public android.support.v4.media.session.MediaSessionCompat.Token? getMediaSession();
+    method public androidx.mediarouter.media.MediaRouter.RouteInfo getRoute();
+    method public boolean isVolumeControlEnabled();
+    method public android.view.View? onCreateMediaControlView(android.os.Bundle?);
+    method public void setVolumeControlEnabled(boolean);
+  }
+
+  public class MediaRouteControllerDialogFragment extends androidx.fragment.app.DialogFragment {
+    ctor public MediaRouteControllerDialogFragment();
+    method public androidx.mediarouter.app.MediaRouteControllerDialog onCreateControllerDialog(android.content.Context, android.os.Bundle?);
+  }
+
+  public class MediaRouteDialogFactory {
+    ctor public MediaRouteDialogFactory();
+    method public static androidx.mediarouter.app.MediaRouteDialogFactory getDefault();
+    method public androidx.mediarouter.app.MediaRouteChooserDialogFragment onCreateChooserDialogFragment();
+    method public androidx.mediarouter.app.MediaRouteControllerDialogFragment onCreateControllerDialogFragment();
+  }
+
+  public class MediaRouteDiscoveryFragment extends androidx.fragment.app.Fragment {
+    ctor public MediaRouteDiscoveryFragment();
+    method public androidx.mediarouter.media.MediaRouter getMediaRouter();
+    method public androidx.mediarouter.media.MediaRouteSelector getRouteSelector();
+    method public androidx.mediarouter.media.MediaRouter.Callback? onCreateCallback();
+    method public int onPrepareCallbackFlags();
+    method public void setRouteSelector(androidx.mediarouter.media.MediaRouteSelector);
+  }
+
+  public final class SystemOutputSwitcherDialogController {
+    method public static boolean showDialog(android.content.Context);
+  }
+
+}
+
+package androidx.mediarouter.media {
+
+  public final class MediaControlIntent {
+    field public static final String ACTION_END_SESSION = "android.media.intent.action.END_SESSION";
+    field public static final String ACTION_ENQUEUE = "android.media.intent.action.ENQUEUE";
+    field public static final String ACTION_GET_SESSION_STATUS = "android.media.intent.action.GET_SESSION_STATUS";
+    field public static final String ACTION_GET_STATUS = "android.media.intent.action.GET_STATUS";
+    field public static final String ACTION_PAUSE = "android.media.intent.action.PAUSE";
+    field public static final String ACTION_PLAY = "android.media.intent.action.PLAY";
+    field public static final String ACTION_REMOVE = "android.media.intent.action.REMOVE";
+    field public static final String ACTION_RESUME = "android.media.intent.action.RESUME";
+    field public static final String ACTION_SEEK = "android.media.intent.action.SEEK";
+    field public static final String ACTION_SEND_MESSAGE = "android.media.intent.action.SEND_MESSAGE";
+    field public static final String ACTION_START_SESSION = "android.media.intent.action.START_SESSION";
+    field public static final String ACTION_STOP = "android.media.intent.action.STOP";
+    field public static final String CATEGORY_LIVE_AUDIO = "android.media.intent.category.LIVE_AUDIO";
+    field public static final String CATEGORY_LIVE_VIDEO = "android.media.intent.category.LIVE_VIDEO";
+    field public static final String CATEGORY_REMOTE_PLAYBACK = "android.media.intent.category.REMOTE_PLAYBACK";
+    field public static final int ERROR_INVALID_ITEM_ID = 3; // 0x3
+    field public static final int ERROR_INVALID_SESSION_ID = 2; // 0x2
+    field public static final int ERROR_UNKNOWN = 0; // 0x0
+    field public static final int ERROR_UNSUPPORTED_OPERATION = 1; // 0x1
+    field public static final String EXTRA_ERROR_CODE = "android.media.intent.extra.ERROR_CODE";
+    field public static final String EXTRA_ITEM_CONTENT_POSITION = "android.media.intent.extra.ITEM_POSITION";
+    field public static final String EXTRA_ITEM_HTTP_HEADERS = "android.media.intent.extra.HTTP_HEADERS";
+    field public static final String EXTRA_ITEM_ID = "android.media.intent.extra.ITEM_ID";
+    field public static final String EXTRA_ITEM_METADATA = "android.media.intent.extra.ITEM_METADATA";
+    field public static final String EXTRA_ITEM_STATUS = "android.media.intent.extra.ITEM_STATUS";
+    field public static final String EXTRA_ITEM_STATUS_UPDATE_RECEIVER = "android.media.intent.extra.ITEM_STATUS_UPDATE_RECEIVER";
+    field public static final String EXTRA_MESSAGE = "android.media.intent.extra.MESSAGE";
+    field public static final String EXTRA_MESSAGE_RECEIVER = "android.media.intent.extra.MESSAGE_RECEIVER";
+    field public static final String EXTRA_SESSION_ID = "android.media.intent.extra.SESSION_ID";
+    field public static final String EXTRA_SESSION_STATUS = "android.media.intent.extra.SESSION_STATUS";
+    field public static final String EXTRA_SESSION_STATUS_UPDATE_RECEIVER = "android.media.intent.extra.SESSION_STATUS_UPDATE_RECEIVER";
+  }
+
+  public final class MediaItemMetadata {
+    field public static final String KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
+    field public static final String KEY_ALBUM_TITLE = "android.media.metadata.ALBUM_TITLE";
+    field public static final String KEY_ARTIST = "android.media.metadata.ARTIST";
+    field public static final String KEY_ARTWORK_URI = "android.media.metadata.ARTWORK_URI";
+    field public static final String KEY_AUTHOR = "android.media.metadata.AUTHOR";
+    field public static final String KEY_COMPOSER = "android.media.metadata.COMPOSER";
+    field public static final String KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
+    field public static final String KEY_DURATION = "android.media.metadata.DURATION";
+    field public static final String KEY_TITLE = "android.media.metadata.TITLE";
+    field public static final String KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
+    field public static final String KEY_YEAR = "android.media.metadata.YEAR";
+  }
+
+  public final class MediaItemStatus {
+    method public android.os.Bundle asBundle();
+    method public static androidx.mediarouter.media.MediaItemStatus? fromBundle(android.os.Bundle?);
+    method public long getContentDuration();
+    method public long getContentPosition();
+    method public android.os.Bundle? getExtras();
+    method public int getPlaybackState();
+    method public long getTimestamp();
+    field public static final String EXTRA_HTTP_RESPONSE_HEADERS = "android.media.status.extra.HTTP_RESPONSE_HEADERS";
+    field public static final String EXTRA_HTTP_STATUS_CODE = "android.media.status.extra.HTTP_STATUS_CODE";
+    field public static final int PLAYBACK_STATE_BUFFERING = 3; // 0x3
+    field public static final int PLAYBACK_STATE_CANCELED = 5; // 0x5
+    field public static final int PLAYBACK_STATE_ERROR = 7; // 0x7
+    field public static final int PLAYBACK_STATE_FINISHED = 4; // 0x4
+    field public static final int PLAYBACK_STATE_INVALIDATED = 6; // 0x6
+    field public static final int PLAYBACK_STATE_PAUSED = 2; // 0x2
+    field public static final int PLAYBACK_STATE_PENDING = 0; // 0x0
+    field public static final int PLAYBACK_STATE_PLAYING = 1; // 0x1
+  }
+
+  public static final class MediaItemStatus.Builder {
+    ctor public MediaItemStatus.Builder(int);
+    ctor public MediaItemStatus.Builder(androidx.mediarouter.media.MediaItemStatus);
+    method public androidx.mediarouter.media.MediaItemStatus build();
+    method public androidx.mediarouter.media.MediaItemStatus.Builder setContentDuration(long);
+    method public androidx.mediarouter.media.MediaItemStatus.Builder setContentPosition(long);
+    method public androidx.mediarouter.media.MediaItemStatus.Builder setExtras(android.os.Bundle?);
+    method public androidx.mediarouter.media.MediaItemStatus.Builder setPlaybackState(int);
+    method public androidx.mediarouter.media.MediaItemStatus.Builder setTimestamp(long);
+  }
+
+  public final class MediaRouteDescriptor {
+    method public android.os.Bundle asBundle();
+    method public boolean canDisconnectAndKeepPlaying();
+    method public static androidx.mediarouter.media.MediaRouteDescriptor? fromBundle(android.os.Bundle?);
+    method public int getConnectionState();
+    method public java.util.List<android.content.IntentFilter!> getControlFilters();
+    method public String? getDescription();
+    method public int getDeviceType();
+    method public android.os.Bundle? getExtras();
+    method public android.net.Uri? getIconUri();
+    method public String getId();
+    method public String getName();
+    method public int getPlaybackStream();
+    method public int getPlaybackType();
+    method public int getPresentationDisplayId();
+    method public android.content.IntentSender? getSettingsActivity();
+    method public int getVolume();
+    method public int getVolumeHandling();
+    method public int getVolumeMax();
+    method @Deprecated public boolean isConnecting();
+    method public boolean isDynamicGroupRoute();
+    method public boolean isEnabled();
+    method public boolean isValid();
+  }
+
+  public static final class MediaRouteDescriptor.Builder {
+    ctor public MediaRouteDescriptor.Builder(String, String);
+    ctor public MediaRouteDescriptor.Builder(androidx.mediarouter.media.MediaRouteDescriptor);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder addControlFilter(android.content.IntentFilter);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder addControlFilters(java.util.Collection<android.content.IntentFilter!>);
+    method public androidx.mediarouter.media.MediaRouteDescriptor build();
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder clearControlFilters();
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setCanDisconnect(boolean);
+    method @Deprecated public androidx.mediarouter.media.MediaRouteDescriptor.Builder setConnecting(boolean);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setConnectionState(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDescription(String?);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDeviceType(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setEnabled(boolean);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setExtras(android.os.Bundle?);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setIconUri(android.net.Uri);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setId(String);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setIsDynamicGroupRoute(boolean);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setName(String);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setPlaybackStream(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setPlaybackType(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setPresentationDisplayId(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setSettingsActivity(android.content.IntentSender?);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVolume(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVolumeHandling(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVolumeMax(int);
+  }
+
+  public final class MediaRouteDiscoveryRequest {
+    ctor public MediaRouteDiscoveryRequest(androidx.mediarouter.media.MediaRouteSelector, boolean);
+    method public android.os.Bundle asBundle();
+    method public static androidx.mediarouter.media.MediaRouteDiscoveryRequest? fromBundle(android.os.Bundle?);
+    method public androidx.mediarouter.media.MediaRouteSelector getSelector();
+    method public boolean isActiveScan();
+    method public boolean isValid();
+  }
+
+  public abstract class MediaRouteProvider {
+    ctor public MediaRouteProvider(android.content.Context);
+    method public final android.content.Context getContext();
+    method public final androidx.mediarouter.media.MediaRouteProviderDescriptor? getDescriptor();
+    method public final androidx.mediarouter.media.MediaRouteDiscoveryRequest? getDiscoveryRequest();
+    method public final android.os.Handler getHandler();
+    method public final androidx.mediarouter.media.MediaRouteProvider.ProviderMetadata getMetadata();
+    method public androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController? onCreateDynamicGroupRouteController(String);
+    method public androidx.mediarouter.media.MediaRouteProvider.RouteController? onCreateRouteController(String);
+    method public void onDiscoveryRequestChanged(androidx.mediarouter.media.MediaRouteDiscoveryRequest?);
+    method public final void setCallback(androidx.mediarouter.media.MediaRouteProvider.Callback?);
+    method public final void setDescriptor(androidx.mediarouter.media.MediaRouteProviderDescriptor?);
+    method public final void setDiscoveryRequest(androidx.mediarouter.media.MediaRouteDiscoveryRequest?);
+  }
+
+  public abstract static class MediaRouteProvider.Callback {
+    ctor public MediaRouteProvider.Callback();
+    method public void onDescriptorChanged(androidx.mediarouter.media.MediaRouteProvider, androidx.mediarouter.media.MediaRouteProviderDescriptor?);
+  }
+
+  public abstract static class MediaRouteProvider.DynamicGroupRouteController extends androidx.mediarouter.media.MediaRouteProvider.RouteController {
+    ctor public MediaRouteProvider.DynamicGroupRouteController();
+    method public String? getGroupableSelectionTitle();
+    method public String? getTransferableSectionTitle();
+    method @Deprecated public final void notifyDynamicRoutesChanged(java.util.Collection<androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor!>);
+    method public final void notifyDynamicRoutesChanged(androidx.mediarouter.media.MediaRouteDescriptor, java.util.Collection<androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor!>);
+    method public abstract void onAddMemberRoute(String);
+    method public abstract void onRemoveMemberRoute(String);
+    method public abstract void onUpdateMemberRoutes(java.util.List<java.lang.String!>?);
+  }
+
+  public static final class MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor {
+    method public androidx.mediarouter.media.MediaRouteDescriptor getRouteDescriptor();
+    method public int getSelectionState();
+    method public boolean isGroupable();
+    method public boolean isTransferable();
+    method public boolean isUnselectable();
+    field public static final int SELECTED = 3; // 0x3
+    field public static final int SELECTING = 2; // 0x2
+    field public static final int UNSELECTED = 1; // 0x1
+    field public static final int UNSELECTING = 0; // 0x0
+  }
+
+  public static final class MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder {
+    ctor public MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder(androidx.mediarouter.media.MediaRouteDescriptor);
+    ctor public MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder(androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor);
+    method public androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor build();
+    method public androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder setIsGroupable(boolean);
+    method public androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder setIsTransferable(boolean);
+    method public androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder setIsUnselectable(boolean);
+    method public androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder setSelectionState(int);
+  }
+
+  public static final class MediaRouteProvider.ProviderMetadata {
+    method public android.content.ComponentName getComponentName();
+    method public String getPackageName();
+  }
+
+  public abstract static class MediaRouteProvider.RouteController {
+    ctor public MediaRouteProvider.RouteController();
+    method public boolean onControlRequest(android.content.Intent, androidx.mediarouter.media.MediaRouter.ControlRequestCallback?);
+    method public void onRelease();
+    method public void onSelect();
+    method public void onSetVolume(int);
+    method @Deprecated public void onUnselect();
+    method public void onUnselect(int);
+    method public void onUpdateVolume(int);
+  }
+
+  public final class MediaRouteProviderDescriptor {
+    method public android.os.Bundle asBundle();
+    method public static androidx.mediarouter.media.MediaRouteProviderDescriptor? fromBundle(android.os.Bundle?);
+    method public java.util.List<androidx.mediarouter.media.MediaRouteDescriptor!> getRoutes();
+    method public boolean isValid();
+    method public boolean supportsDynamicGroupRoute();
+  }
+
+  public static final class MediaRouteProviderDescriptor.Builder {
+    ctor public MediaRouteProviderDescriptor.Builder();
+    ctor public MediaRouteProviderDescriptor.Builder(androidx.mediarouter.media.MediaRouteProviderDescriptor);
+    method public androidx.mediarouter.media.MediaRouteProviderDescriptor.Builder addRoute(androidx.mediarouter.media.MediaRouteDescriptor);
+    method public androidx.mediarouter.media.MediaRouteProviderDescriptor.Builder addRoutes(java.util.Collection<androidx.mediarouter.media.MediaRouteDescriptor!>);
+    method public androidx.mediarouter.media.MediaRouteProviderDescriptor build();
+    method public androidx.mediarouter.media.MediaRouteProviderDescriptor.Builder setSupportsDynamicGroupRoute(boolean);
+  }
+
+  public abstract class MediaRouteProviderService extends android.app.Service {
+    ctor public MediaRouteProviderService();
+    method public androidx.mediarouter.media.MediaRouteProvider? getMediaRouteProvider();
+    method public android.os.IBinder? onBind(android.content.Intent);
+    method public abstract androidx.mediarouter.media.MediaRouteProvider? onCreateMediaRouteProvider();
+    field public static final String SERVICE_INTERFACE = "android.media.MediaRouteProviderService";
+  }
+
+  public final class MediaRouteSelector {
+    method public android.os.Bundle asBundle();
+    method public boolean contains(androidx.mediarouter.media.MediaRouteSelector);
+    method public static androidx.mediarouter.media.MediaRouteSelector? fromBundle(android.os.Bundle?);
+    method public java.util.List<java.lang.String!> getControlCategories();
+    method public boolean hasControlCategory(String?);
+    method public boolean isEmpty();
+    method public boolean isValid();
+    method public boolean matchesControlFilters(java.util.List<android.content.IntentFilter!>?);
+    field public static final androidx.mediarouter.media.MediaRouteSelector! EMPTY;
+  }
+
+  public static final class MediaRouteSelector.Builder {
+    ctor public MediaRouteSelector.Builder();
+    ctor public MediaRouteSelector.Builder(androidx.mediarouter.media.MediaRouteSelector);
+    method public androidx.mediarouter.media.MediaRouteSelector.Builder addControlCategories(java.util.Collection<java.lang.String!>);
+    method public androidx.mediarouter.media.MediaRouteSelector.Builder addControlCategory(String);
+    method public androidx.mediarouter.media.MediaRouteSelector.Builder addSelector(androidx.mediarouter.media.MediaRouteSelector);
+    method public androidx.mediarouter.media.MediaRouteSelector build();
+  }
+
+  public final class MediaRouter {
+    method @MainThread public void addCallback(androidx.mediarouter.media.MediaRouteSelector, androidx.mediarouter.media.MediaRouter.Callback);
+    method @MainThread public void addCallback(androidx.mediarouter.media.MediaRouteSelector, androidx.mediarouter.media.MediaRouter.Callback, int);
+    method @MainThread public void addProvider(androidx.mediarouter.media.MediaRouteProvider);
+    method @MainThread public void addRemoteControlClient(Object);
+    method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo? getBluetoothRoute();
+    method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo getDefaultRoute();
+    method @MainThread public static androidx.mediarouter.media.MediaRouter getInstance(android.content.Context);
+    method public android.support.v4.media.session.MediaSessionCompat.Token? getMediaSessionToken();
+    method @MainThread public java.util.List<androidx.mediarouter.media.MediaRouter.ProviderInfo!> getProviders();
+    method @MainThread public androidx.mediarouter.media.MediaRouterParams? getRouterParams();
+    method @MainThread public java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!> getRoutes();
+    method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo getSelectedRoute();
+    method @MainThread public boolean isRouteAvailable(androidx.mediarouter.media.MediaRouteSelector, int);
+    method @MainThread public void removeCallback(androidx.mediarouter.media.MediaRouter.Callback);
+    method @MainThread public void removeProvider(androidx.mediarouter.media.MediaRouteProvider);
+    method @MainThread public void removeRemoteControlClient(Object);
+    method @MainThread public void selectRoute(androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method @MainThread public void setMediaSession(Object?);
+    method @MainThread public void setMediaSessionCompat(android.support.v4.media.session.MediaSessionCompat?);
+    method @MainThread public void setOnPrepareTransferListener(androidx.mediarouter.media.MediaRouter.OnPrepareTransferListener?);
+    method @MainThread public void setRouterParams(androidx.mediarouter.media.MediaRouterParams?);
+    method @MainThread public void unselect(int);
+    method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo updateSelectedRoute(androidx.mediarouter.media.MediaRouteSelector);
+    field public static final int AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE = 1; // 0x1
+    field public static final int AVAILABILITY_FLAG_REQUIRE_MATCH = 2; // 0x2
+    field public static final int CALLBACK_FLAG_FORCE_DISCOVERY = 8; // 0x8
+    field public static final int CALLBACK_FLAG_PERFORM_ACTIVE_SCAN = 1; // 0x1
+    field public static final int CALLBACK_FLAG_REQUEST_DISCOVERY = 4; // 0x4
+    field public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 2; // 0x2
+    field public static final int UNSELECT_REASON_DISCONNECTED = 1; // 0x1
+    field public static final int UNSELECT_REASON_ROUTE_CHANGED = 3; // 0x3
+    field public static final int UNSELECT_REASON_STOPPED = 2; // 0x2
+    field public static final int UNSELECT_REASON_UNKNOWN = 0; // 0x0
+  }
+
+  public abstract static class MediaRouter.Callback {
+    ctor public MediaRouter.Callback();
+    method public void onProviderAdded(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.ProviderInfo);
+    method public void onProviderChanged(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.ProviderInfo);
+    method public void onProviderRemoved(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.ProviderInfo);
+    method public void onRouteAdded(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void onRouteChanged(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void onRoutePresentationDisplayChanged(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void onRouteRemoved(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method @Deprecated public void onRouteSelected(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void onRouteSelected(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo, int);
+    method public void onRouteSelected(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo, int, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method @Deprecated public void onRouteUnselected(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void onRouteUnselected(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo, int);
+    method public void onRouteVolumeChanged(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+  }
+
+  public abstract static class MediaRouter.ControlRequestCallback {
+    ctor public MediaRouter.ControlRequestCallback();
+    method public void onError(String?, android.os.Bundle?);
+    method public void onResult(android.os.Bundle?);
+  }
+
+  public static interface MediaRouter.OnPrepareTransferListener {
+    method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!>? onPrepareTransfer(androidx.mediarouter.media.MediaRouter.RouteInfo, androidx.mediarouter.media.MediaRouter.RouteInfo);
+  }
+
+  public static final class MediaRouter.ProviderInfo {
+    method public android.content.ComponentName getComponentName();
+    method public String getPackageName();
+    method @MainThread public androidx.mediarouter.media.MediaRouteProvider getProviderInstance();
+    method @MainThread public java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!> getRoutes();
+  }
+
+  public static class MediaRouter.RouteInfo {
+    method public boolean canDisconnect();
+    method public int getConnectionState();
+    method public java.util.List<android.content.IntentFilter!> getControlFilters();
+    method public String? getDescription();
+    method public int getDeviceType();
+    method public android.os.Bundle? getExtras();
+    method public android.net.Uri? getIconUri();
+    method public String getId();
+    method public String getName();
+    method public int getPlaybackStream();
+    method public int getPlaybackType();
+    method @MainThread public android.view.Display? getPresentationDisplay();
+    method public androidx.mediarouter.media.MediaRouter.ProviderInfo getProvider();
+    method public android.content.IntentSender? getSettingsIntent();
+    method public int getVolume();
+    method public int getVolumeHandling();
+    method public int getVolumeMax();
+    method @MainThread public boolean isBluetooth();
+    method @Deprecated public boolean isConnecting();
+    method @MainThread public boolean isDefault();
+    method public boolean isDeviceSpeaker();
+    method public boolean isEnabled();
+    method @MainThread public boolean isSelected();
+    method @MainThread public boolean matchesSelector(androidx.mediarouter.media.MediaRouteSelector);
+    method @MainThread public void requestSetVolume(int);
+    method @MainThread public void requestUpdateVolume(int);
+    method @MainThread public void select();
+    method @MainThread public void sendControlRequest(android.content.Intent, androidx.mediarouter.media.MediaRouter.ControlRequestCallback?);
+    method @MainThread public boolean supportsControlAction(String, String);
+    method @MainThread public boolean supportsControlCategory(String);
+    method @MainThread public boolean supportsControlRequest(android.content.Intent);
+    field public static final int CONNECTION_STATE_CONNECTED = 2; // 0x2
+    field public static final int CONNECTION_STATE_CONNECTING = 1; // 0x1
+    field public static final int CONNECTION_STATE_DISCONNECTED = 0; // 0x0
+    field public static final int DEVICE_TYPE_SPEAKER = 2; // 0x2
+    field public static final int DEVICE_TYPE_TV = 1; // 0x1
+    field public static final int PLAYBACK_TYPE_LOCAL = 0; // 0x0
+    field public static final int PLAYBACK_TYPE_REMOTE = 1; // 0x1
+    field public static final int PLAYBACK_VOLUME_FIXED = 0; // 0x0
+    field public static final int PLAYBACK_VOLUME_VARIABLE = 1; // 0x1
+  }
+
+  public class MediaRouterParams {
+    method public int getDialogType();
+    method public boolean isMediaTransferReceiverEnabled();
+    method public boolean isOutputSwitcherEnabled();
+    method public boolean isTransferToLocalEnabled();
+    field public static final int DIALOG_TYPE_DEFAULT = 1; // 0x1
+    field public static final int DIALOG_TYPE_DYNAMIC_GROUP = 2; // 0x2
+    field public static final String ENABLE_GROUP_VOLUME_UX = "androidx.mediarouter.media.MediaRouterParams.ENABLE_GROUP_VOLUME_UX";
+  }
+
+  public static final class MediaRouterParams.Builder {
+    ctor public MediaRouterParams.Builder();
+    ctor public MediaRouterParams.Builder(androidx.mediarouter.media.MediaRouterParams);
+    method public androidx.mediarouter.media.MediaRouterParams build();
+    method public androidx.mediarouter.media.MediaRouterParams.Builder setDialogType(int);
+    method public androidx.mediarouter.media.MediaRouterParams.Builder setMediaTransferReceiverEnabled(boolean);
+    method public androidx.mediarouter.media.MediaRouterParams.Builder setOutputSwitcherEnabled(boolean);
+    method public androidx.mediarouter.media.MediaRouterParams.Builder setTransferToLocalEnabled(boolean);
+  }
+
+  public final class MediaSessionStatus {
+    method public android.os.Bundle asBundle();
+    method public static androidx.mediarouter.media.MediaSessionStatus? fromBundle(android.os.Bundle?);
+    method public android.os.Bundle? getExtras();
+    method public int getSessionState();
+    method public long getTimestamp();
+    method public boolean isQueuePaused();
+    field public static final int SESSION_STATE_ACTIVE = 0; // 0x0
+    field public static final int SESSION_STATE_ENDED = 1; // 0x1
+    field public static final int SESSION_STATE_INVALIDATED = 2; // 0x2
+  }
+
+  public static final class MediaSessionStatus.Builder {
+    ctor public MediaSessionStatus.Builder(int);
+    ctor public MediaSessionStatus.Builder(androidx.mediarouter.media.MediaSessionStatus);
+    method public androidx.mediarouter.media.MediaSessionStatus build();
+    method public androidx.mediarouter.media.MediaSessionStatus.Builder setExtras(android.os.Bundle?);
+    method public androidx.mediarouter.media.MediaSessionStatus.Builder setQueuePaused(boolean);
+    method public androidx.mediarouter.media.MediaSessionStatus.Builder setSessionState(int);
+    method public androidx.mediarouter.media.MediaSessionStatus.Builder setTimestamp(long);
+  }
+
+  public final class MediaTransferReceiver extends android.content.BroadcastReceiver {
+    ctor public MediaTransferReceiver();
+    method public void onReceive(android.content.Context, android.content.Intent);
+  }
+
+  public class RemotePlaybackClient {
+    ctor public RemotePlaybackClient(android.content.Context, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void endSession(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+    method public void enqueue(android.net.Uri, String?, android.os.Bundle?, long, android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback?);
+    method public String? getSessionId();
+    method public void getSessionStatus(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+    method public void getStatus(String, android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback?);
+    method public boolean hasSession();
+    method public boolean isMessagingSupported();
+    method public boolean isQueuingSupported();
+    method public boolean isRemotePlaybackSupported();
+    method public boolean isSessionManagementSupported();
+    method public void pause(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+    method public void play(android.net.Uri, String?, android.os.Bundle?, long, android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback?);
+    method public void release();
+    method public void remove(String, android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback?);
+    method public void resume(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+    method public void seek(String, long, android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback?);
+    method public void sendMessage(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+    method public void setOnMessageReceivedListener(androidx.mediarouter.media.RemotePlaybackClient.OnMessageReceivedListener?);
+    method public void setSessionId(String?);
+    method public void setStatusCallback(androidx.mediarouter.media.RemotePlaybackClient.StatusCallback?);
+    method public void startSession(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+    method public void stop(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+  }
+
+  public abstract static class RemotePlaybackClient.ActionCallback {
+    ctor public RemotePlaybackClient.ActionCallback();
+    method public void onError(String?, int, android.os.Bundle?);
+  }
+
+  public abstract static class RemotePlaybackClient.ItemActionCallback extends androidx.mediarouter.media.RemotePlaybackClient.ActionCallback {
+    ctor public RemotePlaybackClient.ItemActionCallback();
+    method public void onResult(android.os.Bundle, String, androidx.mediarouter.media.MediaSessionStatus?, String, androidx.mediarouter.media.MediaItemStatus);
+  }
+
+  public static interface RemotePlaybackClient.OnMessageReceivedListener {
+    method public void onMessageReceived(String, android.os.Bundle?);
+  }
+
+  public abstract static class RemotePlaybackClient.SessionActionCallback extends androidx.mediarouter.media.RemotePlaybackClient.ActionCallback {
+    ctor public RemotePlaybackClient.SessionActionCallback();
+    method public void onResult(android.os.Bundle, String, androidx.mediarouter.media.MediaSessionStatus?);
+  }
+
+  public abstract static class RemotePlaybackClient.StatusCallback {
+    ctor public RemotePlaybackClient.StatusCallback();
+    method public void onItemStatusChanged(android.os.Bundle?, String, androidx.mediarouter.media.MediaSessionStatus?, String, androidx.mediarouter.media.MediaItemStatus);
+    method public void onSessionChanged(String?);
+    method public void onSessionStatusChanged(android.os.Bundle?, String, androidx.mediarouter.media.MediaSessionStatus?);
+  }
+
+}
+
diff --git a/mediarouter/mediarouter/api/public_plus_experimental_1.4.0-beta02.txt b/mediarouter/mediarouter/api/public_plus_experimental_1.4.0-beta02.txt
new file mode 100644
index 0000000..b1cf094
--- /dev/null
+++ b/mediarouter/mediarouter/api/public_plus_experimental_1.4.0-beta02.txt
@@ -0,0 +1,558 @@
+// Signature format: 4.0
+package androidx.mediarouter.app {
+
+  public class MediaRouteActionProvider extends androidx.core.view.ActionProvider {
+    ctor public MediaRouteActionProvider(android.content.Context);
+    method @Deprecated public void enableDynamicGroup();
+    method public androidx.mediarouter.app.MediaRouteDialogFactory getDialogFactory();
+    method public androidx.mediarouter.app.MediaRouteButton? getMediaRouteButton();
+    method public androidx.mediarouter.media.MediaRouteSelector getRouteSelector();
+    method public android.view.View onCreateActionView();
+    method public androidx.mediarouter.app.MediaRouteButton onCreateMediaRouteButton();
+    method public void setAlwaysVisible(boolean);
+    method public void setDialogFactory(androidx.mediarouter.app.MediaRouteDialogFactory);
+    method public void setRouteSelector(androidx.mediarouter.media.MediaRouteSelector);
+  }
+
+  public class MediaRouteButton extends android.view.View {
+    ctor public MediaRouteButton(android.content.Context);
+    ctor public MediaRouteButton(android.content.Context, android.util.AttributeSet?);
+    ctor public MediaRouteButton(android.content.Context, android.util.AttributeSet?, int);
+    method @Deprecated public void enableDynamicGroup();
+    method public androidx.mediarouter.app.MediaRouteDialogFactory getDialogFactory();
+    method public androidx.mediarouter.media.MediaRouteSelector getRouteSelector();
+    method public void onAttachedToWindow();
+    method public void onDetachedFromWindow();
+    method public void setAlwaysVisible(boolean);
+    method public void setDialogFactory(androidx.mediarouter.app.MediaRouteDialogFactory);
+    method public void setRemoteIndicatorDrawable(android.graphics.drawable.Drawable?);
+    method public void setRouteSelector(androidx.mediarouter.media.MediaRouteSelector);
+    method public boolean showDialog();
+  }
+
+  public class MediaRouteChooserDialog extends androidx.appcompat.app.AppCompatDialog {
+    ctor public MediaRouteChooserDialog(android.content.Context);
+    ctor public MediaRouteChooserDialog(android.content.Context, int);
+    method public androidx.mediarouter.media.MediaRouteSelector getRouteSelector();
+    method public boolean onFilterRoute(androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void onFilterRoutes(java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!>);
+    method public void refreshRoutes();
+    method public void setRouteSelector(androidx.mediarouter.media.MediaRouteSelector);
+  }
+
+  public class MediaRouteChooserDialogFragment extends androidx.fragment.app.DialogFragment {
+    ctor public MediaRouteChooserDialogFragment();
+    method public androidx.mediarouter.media.MediaRouteSelector getRouteSelector();
+    method public androidx.mediarouter.app.MediaRouteChooserDialog onCreateChooserDialog(android.content.Context, android.os.Bundle?);
+    method public void setRouteSelector(androidx.mediarouter.media.MediaRouteSelector);
+  }
+
+  public class MediaRouteControllerDialog extends androidx.appcompat.app.AlertDialog {
+    ctor public MediaRouteControllerDialog(android.content.Context);
+    ctor public MediaRouteControllerDialog(android.content.Context, int);
+    method public android.view.View? getMediaControlView();
+    method public android.support.v4.media.session.MediaSessionCompat.Token? getMediaSession();
+    method public androidx.mediarouter.media.MediaRouter.RouteInfo getRoute();
+    method public boolean isVolumeControlEnabled();
+    method public android.view.View? onCreateMediaControlView(android.os.Bundle?);
+    method public void setVolumeControlEnabled(boolean);
+  }
+
+  public class MediaRouteControllerDialogFragment extends androidx.fragment.app.DialogFragment {
+    ctor public MediaRouteControllerDialogFragment();
+    method public androidx.mediarouter.app.MediaRouteControllerDialog onCreateControllerDialog(android.content.Context, android.os.Bundle?);
+  }
+
+  public class MediaRouteDialogFactory {
+    ctor public MediaRouteDialogFactory();
+    method public static androidx.mediarouter.app.MediaRouteDialogFactory getDefault();
+    method public androidx.mediarouter.app.MediaRouteChooserDialogFragment onCreateChooserDialogFragment();
+    method public androidx.mediarouter.app.MediaRouteControllerDialogFragment onCreateControllerDialogFragment();
+  }
+
+  public class MediaRouteDiscoveryFragment extends androidx.fragment.app.Fragment {
+    ctor public MediaRouteDiscoveryFragment();
+    method public androidx.mediarouter.media.MediaRouter getMediaRouter();
+    method public androidx.mediarouter.media.MediaRouteSelector getRouteSelector();
+    method public androidx.mediarouter.media.MediaRouter.Callback? onCreateCallback();
+    method public int onPrepareCallbackFlags();
+    method public void setRouteSelector(androidx.mediarouter.media.MediaRouteSelector);
+  }
+
+  public final class SystemOutputSwitcherDialogController {
+    method public static boolean showDialog(android.content.Context);
+  }
+
+}
+
+package androidx.mediarouter.media {
+
+  public final class MediaControlIntent {
+    field public static final String ACTION_END_SESSION = "android.media.intent.action.END_SESSION";
+    field public static final String ACTION_ENQUEUE = "android.media.intent.action.ENQUEUE";
+    field public static final String ACTION_GET_SESSION_STATUS = "android.media.intent.action.GET_SESSION_STATUS";
+    field public static final String ACTION_GET_STATUS = "android.media.intent.action.GET_STATUS";
+    field public static final String ACTION_PAUSE = "android.media.intent.action.PAUSE";
+    field public static final String ACTION_PLAY = "android.media.intent.action.PLAY";
+    field public static final String ACTION_REMOVE = "android.media.intent.action.REMOVE";
+    field public static final String ACTION_RESUME = "android.media.intent.action.RESUME";
+    field public static final String ACTION_SEEK = "android.media.intent.action.SEEK";
+    field public static final String ACTION_SEND_MESSAGE = "android.media.intent.action.SEND_MESSAGE";
+    field public static final String ACTION_START_SESSION = "android.media.intent.action.START_SESSION";
+    field public static final String ACTION_STOP = "android.media.intent.action.STOP";
+    field public static final String CATEGORY_LIVE_AUDIO = "android.media.intent.category.LIVE_AUDIO";
+    field public static final String CATEGORY_LIVE_VIDEO = "android.media.intent.category.LIVE_VIDEO";
+    field public static final String CATEGORY_REMOTE_PLAYBACK = "android.media.intent.category.REMOTE_PLAYBACK";
+    field public static final int ERROR_INVALID_ITEM_ID = 3; // 0x3
+    field public static final int ERROR_INVALID_SESSION_ID = 2; // 0x2
+    field public static final int ERROR_UNKNOWN = 0; // 0x0
+    field public static final int ERROR_UNSUPPORTED_OPERATION = 1; // 0x1
+    field public static final String EXTRA_ERROR_CODE = "android.media.intent.extra.ERROR_CODE";
+    field public static final String EXTRA_ITEM_CONTENT_POSITION = "android.media.intent.extra.ITEM_POSITION";
+    field public static final String EXTRA_ITEM_HTTP_HEADERS = "android.media.intent.extra.HTTP_HEADERS";
+    field public static final String EXTRA_ITEM_ID = "android.media.intent.extra.ITEM_ID";
+    field public static final String EXTRA_ITEM_METADATA = "android.media.intent.extra.ITEM_METADATA";
+    field public static final String EXTRA_ITEM_STATUS = "android.media.intent.extra.ITEM_STATUS";
+    field public static final String EXTRA_ITEM_STATUS_UPDATE_RECEIVER = "android.media.intent.extra.ITEM_STATUS_UPDATE_RECEIVER";
+    field public static final String EXTRA_MESSAGE = "android.media.intent.extra.MESSAGE";
+    field public static final String EXTRA_MESSAGE_RECEIVER = "android.media.intent.extra.MESSAGE_RECEIVER";
+    field public static final String EXTRA_SESSION_ID = "android.media.intent.extra.SESSION_ID";
+    field public static final String EXTRA_SESSION_STATUS = "android.media.intent.extra.SESSION_STATUS";
+    field public static final String EXTRA_SESSION_STATUS_UPDATE_RECEIVER = "android.media.intent.extra.SESSION_STATUS_UPDATE_RECEIVER";
+  }
+
+  public final class MediaItemMetadata {
+    field public static final String KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
+    field public static final String KEY_ALBUM_TITLE = "android.media.metadata.ALBUM_TITLE";
+    field public static final String KEY_ARTIST = "android.media.metadata.ARTIST";
+    field public static final String KEY_ARTWORK_URI = "android.media.metadata.ARTWORK_URI";
+    field public static final String KEY_AUTHOR = "android.media.metadata.AUTHOR";
+    field public static final String KEY_COMPOSER = "android.media.metadata.COMPOSER";
+    field public static final String KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
+    field public static final String KEY_DURATION = "android.media.metadata.DURATION";
+    field public static final String KEY_TITLE = "android.media.metadata.TITLE";
+    field public static final String KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
+    field public static final String KEY_YEAR = "android.media.metadata.YEAR";
+  }
+
+  public final class MediaItemStatus {
+    method public android.os.Bundle asBundle();
+    method public static androidx.mediarouter.media.MediaItemStatus? fromBundle(android.os.Bundle?);
+    method public long getContentDuration();
+    method public long getContentPosition();
+    method public android.os.Bundle? getExtras();
+    method public int getPlaybackState();
+    method public long getTimestamp();
+    field public static final String EXTRA_HTTP_RESPONSE_HEADERS = "android.media.status.extra.HTTP_RESPONSE_HEADERS";
+    field public static final String EXTRA_HTTP_STATUS_CODE = "android.media.status.extra.HTTP_STATUS_CODE";
+    field public static final int PLAYBACK_STATE_BUFFERING = 3; // 0x3
+    field public static final int PLAYBACK_STATE_CANCELED = 5; // 0x5
+    field public static final int PLAYBACK_STATE_ERROR = 7; // 0x7
+    field public static final int PLAYBACK_STATE_FINISHED = 4; // 0x4
+    field public static final int PLAYBACK_STATE_INVALIDATED = 6; // 0x6
+    field public static final int PLAYBACK_STATE_PAUSED = 2; // 0x2
+    field public static final int PLAYBACK_STATE_PENDING = 0; // 0x0
+    field public static final int PLAYBACK_STATE_PLAYING = 1; // 0x1
+  }
+
+  public static final class MediaItemStatus.Builder {
+    ctor public MediaItemStatus.Builder(int);
+    ctor public MediaItemStatus.Builder(androidx.mediarouter.media.MediaItemStatus);
+    method public androidx.mediarouter.media.MediaItemStatus build();
+    method public androidx.mediarouter.media.MediaItemStatus.Builder setContentDuration(long);
+    method public androidx.mediarouter.media.MediaItemStatus.Builder setContentPosition(long);
+    method public androidx.mediarouter.media.MediaItemStatus.Builder setExtras(android.os.Bundle?);
+    method public androidx.mediarouter.media.MediaItemStatus.Builder setPlaybackState(int);
+    method public androidx.mediarouter.media.MediaItemStatus.Builder setTimestamp(long);
+  }
+
+  public final class MediaRouteDescriptor {
+    method public android.os.Bundle asBundle();
+    method public boolean canDisconnectAndKeepPlaying();
+    method public static androidx.mediarouter.media.MediaRouteDescriptor? fromBundle(android.os.Bundle?);
+    method public int getConnectionState();
+    method public java.util.List<android.content.IntentFilter!> getControlFilters();
+    method public String? getDescription();
+    method public int getDeviceType();
+    method public android.os.Bundle? getExtras();
+    method public android.net.Uri? getIconUri();
+    method public String getId();
+    method public String getName();
+    method public int getPlaybackStream();
+    method public int getPlaybackType();
+    method public int getPresentationDisplayId();
+    method public android.content.IntentSender? getSettingsActivity();
+    method public int getVolume();
+    method public int getVolumeHandling();
+    method public int getVolumeMax();
+    method @Deprecated public boolean isConnecting();
+    method public boolean isDynamicGroupRoute();
+    method public boolean isEnabled();
+    method public boolean isValid();
+  }
+
+  public static final class MediaRouteDescriptor.Builder {
+    ctor public MediaRouteDescriptor.Builder(String, String);
+    ctor public MediaRouteDescriptor.Builder(androidx.mediarouter.media.MediaRouteDescriptor);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder addControlFilter(android.content.IntentFilter);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder addControlFilters(java.util.Collection<android.content.IntentFilter!>);
+    method public androidx.mediarouter.media.MediaRouteDescriptor build();
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder clearControlFilters();
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setCanDisconnect(boolean);
+    method @Deprecated public androidx.mediarouter.media.MediaRouteDescriptor.Builder setConnecting(boolean);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setConnectionState(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDescription(String?);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDeviceType(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setEnabled(boolean);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setExtras(android.os.Bundle?);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setIconUri(android.net.Uri);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setId(String);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setIsDynamicGroupRoute(boolean);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setName(String);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setPlaybackStream(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setPlaybackType(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setPresentationDisplayId(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setSettingsActivity(android.content.IntentSender?);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVolume(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVolumeHandling(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVolumeMax(int);
+  }
+
+  public final class MediaRouteDiscoveryRequest {
+    ctor public MediaRouteDiscoveryRequest(androidx.mediarouter.media.MediaRouteSelector, boolean);
+    method public android.os.Bundle asBundle();
+    method public static androidx.mediarouter.media.MediaRouteDiscoveryRequest? fromBundle(android.os.Bundle?);
+    method public androidx.mediarouter.media.MediaRouteSelector getSelector();
+    method public boolean isActiveScan();
+    method public boolean isValid();
+  }
+
+  public abstract class MediaRouteProvider {
+    ctor public MediaRouteProvider(android.content.Context);
+    method public final android.content.Context getContext();
+    method public final androidx.mediarouter.media.MediaRouteProviderDescriptor? getDescriptor();
+    method public final androidx.mediarouter.media.MediaRouteDiscoveryRequest? getDiscoveryRequest();
+    method public final android.os.Handler getHandler();
+    method public final androidx.mediarouter.media.MediaRouteProvider.ProviderMetadata getMetadata();
+    method public androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController? onCreateDynamicGroupRouteController(String);
+    method public androidx.mediarouter.media.MediaRouteProvider.RouteController? onCreateRouteController(String);
+    method public void onDiscoveryRequestChanged(androidx.mediarouter.media.MediaRouteDiscoveryRequest?);
+    method public final void setCallback(androidx.mediarouter.media.MediaRouteProvider.Callback?);
+    method public final void setDescriptor(androidx.mediarouter.media.MediaRouteProviderDescriptor?);
+    method public final void setDiscoveryRequest(androidx.mediarouter.media.MediaRouteDiscoveryRequest?);
+  }
+
+  public abstract static class MediaRouteProvider.Callback {
+    ctor public MediaRouteProvider.Callback();
+    method public void onDescriptorChanged(androidx.mediarouter.media.MediaRouteProvider, androidx.mediarouter.media.MediaRouteProviderDescriptor?);
+  }
+
+  public abstract static class MediaRouteProvider.DynamicGroupRouteController extends androidx.mediarouter.media.MediaRouteProvider.RouteController {
+    ctor public MediaRouteProvider.DynamicGroupRouteController();
+    method public String? getGroupableSelectionTitle();
+    method public String? getTransferableSectionTitle();
+    method @Deprecated public final void notifyDynamicRoutesChanged(java.util.Collection<androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor!>);
+    method public final void notifyDynamicRoutesChanged(androidx.mediarouter.media.MediaRouteDescriptor, java.util.Collection<androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor!>);
+    method public abstract void onAddMemberRoute(String);
+    method public abstract void onRemoveMemberRoute(String);
+    method public abstract void onUpdateMemberRoutes(java.util.List<java.lang.String!>?);
+  }
+
+  public static final class MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor {
+    method public androidx.mediarouter.media.MediaRouteDescriptor getRouteDescriptor();
+    method public int getSelectionState();
+    method public boolean isGroupable();
+    method public boolean isTransferable();
+    method public boolean isUnselectable();
+    field public static final int SELECTED = 3; // 0x3
+    field public static final int SELECTING = 2; // 0x2
+    field public static final int UNSELECTED = 1; // 0x1
+    field public static final int UNSELECTING = 0; // 0x0
+  }
+
+  public static final class MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder {
+    ctor public MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder(androidx.mediarouter.media.MediaRouteDescriptor);
+    ctor public MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder(androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor);
+    method public androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor build();
+    method public androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder setIsGroupable(boolean);
+    method public androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder setIsTransferable(boolean);
+    method public androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder setIsUnselectable(boolean);
+    method public androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder setSelectionState(int);
+  }
+
+  public static final class MediaRouteProvider.ProviderMetadata {
+    method public android.content.ComponentName getComponentName();
+    method public String getPackageName();
+  }
+
+  public abstract static class MediaRouteProvider.RouteController {
+    ctor public MediaRouteProvider.RouteController();
+    method public boolean onControlRequest(android.content.Intent, androidx.mediarouter.media.MediaRouter.ControlRequestCallback?);
+    method public void onRelease();
+    method public void onSelect();
+    method public void onSetVolume(int);
+    method @Deprecated public void onUnselect();
+    method public void onUnselect(int);
+    method public void onUpdateVolume(int);
+  }
+
+  public final class MediaRouteProviderDescriptor {
+    method public android.os.Bundle asBundle();
+    method public static androidx.mediarouter.media.MediaRouteProviderDescriptor? fromBundle(android.os.Bundle?);
+    method public java.util.List<androidx.mediarouter.media.MediaRouteDescriptor!> getRoutes();
+    method public boolean isValid();
+    method public boolean supportsDynamicGroupRoute();
+  }
+
+  public static final class MediaRouteProviderDescriptor.Builder {
+    ctor public MediaRouteProviderDescriptor.Builder();
+    ctor public MediaRouteProviderDescriptor.Builder(androidx.mediarouter.media.MediaRouteProviderDescriptor);
+    method public androidx.mediarouter.media.MediaRouteProviderDescriptor.Builder addRoute(androidx.mediarouter.media.MediaRouteDescriptor);
+    method public androidx.mediarouter.media.MediaRouteProviderDescriptor.Builder addRoutes(java.util.Collection<androidx.mediarouter.media.MediaRouteDescriptor!>);
+    method public androidx.mediarouter.media.MediaRouteProviderDescriptor build();
+    method public androidx.mediarouter.media.MediaRouteProviderDescriptor.Builder setSupportsDynamicGroupRoute(boolean);
+  }
+
+  public abstract class MediaRouteProviderService extends android.app.Service {
+    ctor public MediaRouteProviderService();
+    method public androidx.mediarouter.media.MediaRouteProvider? getMediaRouteProvider();
+    method public android.os.IBinder? onBind(android.content.Intent);
+    method public abstract androidx.mediarouter.media.MediaRouteProvider? onCreateMediaRouteProvider();
+    field public static final String SERVICE_INTERFACE = "android.media.MediaRouteProviderService";
+  }
+
+  public final class MediaRouteSelector {
+    method public android.os.Bundle asBundle();
+    method public boolean contains(androidx.mediarouter.media.MediaRouteSelector);
+    method public static androidx.mediarouter.media.MediaRouteSelector? fromBundle(android.os.Bundle?);
+    method public java.util.List<java.lang.String!> getControlCategories();
+    method public boolean hasControlCategory(String?);
+    method public boolean isEmpty();
+    method public boolean isValid();
+    method public boolean matchesControlFilters(java.util.List<android.content.IntentFilter!>?);
+    field public static final androidx.mediarouter.media.MediaRouteSelector! EMPTY;
+  }
+
+  public static final class MediaRouteSelector.Builder {
+    ctor public MediaRouteSelector.Builder();
+    ctor public MediaRouteSelector.Builder(androidx.mediarouter.media.MediaRouteSelector);
+    method public androidx.mediarouter.media.MediaRouteSelector.Builder addControlCategories(java.util.Collection<java.lang.String!>);
+    method public androidx.mediarouter.media.MediaRouteSelector.Builder addControlCategory(String);
+    method public androidx.mediarouter.media.MediaRouteSelector.Builder addSelector(androidx.mediarouter.media.MediaRouteSelector);
+    method public androidx.mediarouter.media.MediaRouteSelector build();
+  }
+
+  public final class MediaRouter {
+    method @MainThread public void addCallback(androidx.mediarouter.media.MediaRouteSelector, androidx.mediarouter.media.MediaRouter.Callback);
+    method @MainThread public void addCallback(androidx.mediarouter.media.MediaRouteSelector, androidx.mediarouter.media.MediaRouter.Callback, int);
+    method @MainThread public void addProvider(androidx.mediarouter.media.MediaRouteProvider);
+    method @MainThread public void addRemoteControlClient(Object);
+    method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo? getBluetoothRoute();
+    method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo getDefaultRoute();
+    method @MainThread public static androidx.mediarouter.media.MediaRouter getInstance(android.content.Context);
+    method public android.support.v4.media.session.MediaSessionCompat.Token? getMediaSessionToken();
+    method @MainThread public java.util.List<androidx.mediarouter.media.MediaRouter.ProviderInfo!> getProviders();
+    method @MainThread public androidx.mediarouter.media.MediaRouterParams? getRouterParams();
+    method @MainThread public java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!> getRoutes();
+    method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo getSelectedRoute();
+    method @MainThread public boolean isRouteAvailable(androidx.mediarouter.media.MediaRouteSelector, int);
+    method @MainThread public void removeCallback(androidx.mediarouter.media.MediaRouter.Callback);
+    method @MainThread public void removeProvider(androidx.mediarouter.media.MediaRouteProvider);
+    method @MainThread public void removeRemoteControlClient(Object);
+    method @MainThread public void selectRoute(androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method @MainThread public void setMediaSession(Object?);
+    method @MainThread public void setMediaSessionCompat(android.support.v4.media.session.MediaSessionCompat?);
+    method @MainThread public void setOnPrepareTransferListener(androidx.mediarouter.media.MediaRouter.OnPrepareTransferListener?);
+    method @MainThread public void setRouterParams(androidx.mediarouter.media.MediaRouterParams?);
+    method @MainThread public void unselect(int);
+    method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo updateSelectedRoute(androidx.mediarouter.media.MediaRouteSelector);
+    field public static final int AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE = 1; // 0x1
+    field public static final int AVAILABILITY_FLAG_REQUIRE_MATCH = 2; // 0x2
+    field public static final int CALLBACK_FLAG_FORCE_DISCOVERY = 8; // 0x8
+    field public static final int CALLBACK_FLAG_PERFORM_ACTIVE_SCAN = 1; // 0x1
+    field public static final int CALLBACK_FLAG_REQUEST_DISCOVERY = 4; // 0x4
+    field public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 2; // 0x2
+    field public static final int UNSELECT_REASON_DISCONNECTED = 1; // 0x1
+    field public static final int UNSELECT_REASON_ROUTE_CHANGED = 3; // 0x3
+    field public static final int UNSELECT_REASON_STOPPED = 2; // 0x2
+    field public static final int UNSELECT_REASON_UNKNOWN = 0; // 0x0
+  }
+
+  public abstract static class MediaRouter.Callback {
+    ctor public MediaRouter.Callback();
+    method public void onProviderAdded(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.ProviderInfo);
+    method public void onProviderChanged(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.ProviderInfo);
+    method public void onProviderRemoved(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.ProviderInfo);
+    method public void onRouteAdded(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void onRouteChanged(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void onRoutePresentationDisplayChanged(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void onRouteRemoved(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method @Deprecated public void onRouteSelected(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void onRouteSelected(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo, int);
+    method public void onRouteSelected(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo, int, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method @Deprecated public void onRouteUnselected(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void onRouteUnselected(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo, int);
+    method public void onRouteVolumeChanged(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+  }
+
+  public abstract static class MediaRouter.ControlRequestCallback {
+    ctor public MediaRouter.ControlRequestCallback();
+    method public void onError(String?, android.os.Bundle?);
+    method public void onResult(android.os.Bundle?);
+  }
+
+  public static interface MediaRouter.OnPrepareTransferListener {
+    method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!>? onPrepareTransfer(androidx.mediarouter.media.MediaRouter.RouteInfo, androidx.mediarouter.media.MediaRouter.RouteInfo);
+  }
+
+  public static final class MediaRouter.ProviderInfo {
+    method public android.content.ComponentName getComponentName();
+    method public String getPackageName();
+    method @MainThread public androidx.mediarouter.media.MediaRouteProvider getProviderInstance();
+    method @MainThread public java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!> getRoutes();
+  }
+
+  public static class MediaRouter.RouteInfo {
+    method public boolean canDisconnect();
+    method public int getConnectionState();
+    method public java.util.List<android.content.IntentFilter!> getControlFilters();
+    method public String? getDescription();
+    method public int getDeviceType();
+    method public android.os.Bundle? getExtras();
+    method public android.net.Uri? getIconUri();
+    method public String getId();
+    method public String getName();
+    method public int getPlaybackStream();
+    method public int getPlaybackType();
+    method @MainThread public android.view.Display? getPresentationDisplay();
+    method public androidx.mediarouter.media.MediaRouter.ProviderInfo getProvider();
+    method public android.content.IntentSender? getSettingsIntent();
+    method public int getVolume();
+    method public int getVolumeHandling();
+    method public int getVolumeMax();
+    method @MainThread public boolean isBluetooth();
+    method @Deprecated public boolean isConnecting();
+    method @MainThread public boolean isDefault();
+    method public boolean isDeviceSpeaker();
+    method public boolean isEnabled();
+    method @MainThread public boolean isSelected();
+    method @MainThread public boolean matchesSelector(androidx.mediarouter.media.MediaRouteSelector);
+    method @MainThread public void requestSetVolume(int);
+    method @MainThread public void requestUpdateVolume(int);
+    method @MainThread public void select();
+    method @MainThread public void sendControlRequest(android.content.Intent, androidx.mediarouter.media.MediaRouter.ControlRequestCallback?);
+    method @MainThread public boolean supportsControlAction(String, String);
+    method @MainThread public boolean supportsControlCategory(String);
+    method @MainThread public boolean supportsControlRequest(android.content.Intent);
+    field public static final int CONNECTION_STATE_CONNECTED = 2; // 0x2
+    field public static final int CONNECTION_STATE_CONNECTING = 1; // 0x1
+    field public static final int CONNECTION_STATE_DISCONNECTED = 0; // 0x0
+    field public static final int DEVICE_TYPE_SPEAKER = 2; // 0x2
+    field public static final int DEVICE_TYPE_TV = 1; // 0x1
+    field public static final int PLAYBACK_TYPE_LOCAL = 0; // 0x0
+    field public static final int PLAYBACK_TYPE_REMOTE = 1; // 0x1
+    field public static final int PLAYBACK_VOLUME_FIXED = 0; // 0x0
+    field public static final int PLAYBACK_VOLUME_VARIABLE = 1; // 0x1
+  }
+
+  public class MediaRouterParams {
+    method public int getDialogType();
+    method public boolean isMediaTransferReceiverEnabled();
+    method public boolean isOutputSwitcherEnabled();
+    method public boolean isTransferToLocalEnabled();
+    field public static final int DIALOG_TYPE_DEFAULT = 1; // 0x1
+    field public static final int DIALOG_TYPE_DYNAMIC_GROUP = 2; // 0x2
+    field public static final String ENABLE_GROUP_VOLUME_UX = "androidx.mediarouter.media.MediaRouterParams.ENABLE_GROUP_VOLUME_UX";
+  }
+
+  public static final class MediaRouterParams.Builder {
+    ctor public MediaRouterParams.Builder();
+    ctor public MediaRouterParams.Builder(androidx.mediarouter.media.MediaRouterParams);
+    method public androidx.mediarouter.media.MediaRouterParams build();
+    method public androidx.mediarouter.media.MediaRouterParams.Builder setDialogType(int);
+    method public androidx.mediarouter.media.MediaRouterParams.Builder setMediaTransferReceiverEnabled(boolean);
+    method public androidx.mediarouter.media.MediaRouterParams.Builder setOutputSwitcherEnabled(boolean);
+    method public androidx.mediarouter.media.MediaRouterParams.Builder setTransferToLocalEnabled(boolean);
+  }
+
+  public final class MediaSessionStatus {
+    method public android.os.Bundle asBundle();
+    method public static androidx.mediarouter.media.MediaSessionStatus? fromBundle(android.os.Bundle?);
+    method public android.os.Bundle? getExtras();
+    method public int getSessionState();
+    method public long getTimestamp();
+    method public boolean isQueuePaused();
+    field public static final int SESSION_STATE_ACTIVE = 0; // 0x0
+    field public static final int SESSION_STATE_ENDED = 1; // 0x1
+    field public static final int SESSION_STATE_INVALIDATED = 2; // 0x2
+  }
+
+  public static final class MediaSessionStatus.Builder {
+    ctor public MediaSessionStatus.Builder(int);
+    ctor public MediaSessionStatus.Builder(androidx.mediarouter.media.MediaSessionStatus);
+    method public androidx.mediarouter.media.MediaSessionStatus build();
+    method public androidx.mediarouter.media.MediaSessionStatus.Builder setExtras(android.os.Bundle?);
+    method public androidx.mediarouter.media.MediaSessionStatus.Builder setQueuePaused(boolean);
+    method public androidx.mediarouter.media.MediaSessionStatus.Builder setSessionState(int);
+    method public androidx.mediarouter.media.MediaSessionStatus.Builder setTimestamp(long);
+  }
+
+  public final class MediaTransferReceiver extends android.content.BroadcastReceiver {
+    ctor public MediaTransferReceiver();
+    method public void onReceive(android.content.Context, android.content.Intent);
+  }
+
+  public class RemotePlaybackClient {
+    ctor public RemotePlaybackClient(android.content.Context, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void endSession(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+    method public void enqueue(android.net.Uri, String?, android.os.Bundle?, long, android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback?);
+    method public String? getSessionId();
+    method public void getSessionStatus(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+    method public void getStatus(String, android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback?);
+    method public boolean hasSession();
+    method public boolean isMessagingSupported();
+    method public boolean isQueuingSupported();
+    method public boolean isRemotePlaybackSupported();
+    method public boolean isSessionManagementSupported();
+    method public void pause(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+    method public void play(android.net.Uri, String?, android.os.Bundle?, long, android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback?);
+    method public void release();
+    method public void remove(String, android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback?);
+    method public void resume(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+    method public void seek(String, long, android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback?);
+    method public void sendMessage(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+    method public void setOnMessageReceivedListener(androidx.mediarouter.media.RemotePlaybackClient.OnMessageReceivedListener?);
+    method public void setSessionId(String?);
+    method public void setStatusCallback(androidx.mediarouter.media.RemotePlaybackClient.StatusCallback?);
+    method public void startSession(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+    method public void stop(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+  }
+
+  public abstract static class RemotePlaybackClient.ActionCallback {
+    ctor public RemotePlaybackClient.ActionCallback();
+    method public void onError(String?, int, android.os.Bundle?);
+  }
+
+  public abstract static class RemotePlaybackClient.ItemActionCallback extends androidx.mediarouter.media.RemotePlaybackClient.ActionCallback {
+    ctor public RemotePlaybackClient.ItemActionCallback();
+    method public void onResult(android.os.Bundle, String, androidx.mediarouter.media.MediaSessionStatus?, String, androidx.mediarouter.media.MediaItemStatus);
+  }
+
+  public static interface RemotePlaybackClient.OnMessageReceivedListener {
+    method public void onMessageReceived(String, android.os.Bundle?);
+  }
+
+  public abstract static class RemotePlaybackClient.SessionActionCallback extends androidx.mediarouter.media.RemotePlaybackClient.ActionCallback {
+    ctor public RemotePlaybackClient.SessionActionCallback();
+    method public void onResult(android.os.Bundle, String, androidx.mediarouter.media.MediaSessionStatus?);
+  }
+
+  public abstract static class RemotePlaybackClient.StatusCallback {
+    ctor public RemotePlaybackClient.StatusCallback();
+    method public void onItemStatusChanged(android.os.Bundle?, String, androidx.mediarouter.media.MediaSessionStatus?, String, androidx.mediarouter.media.MediaItemStatus);
+    method public void onSessionChanged(String?);
+    method public void onSessionStatusChanged(android.os.Bundle?, String, androidx.mediarouter.media.MediaSessionStatus?);
+  }
+
+}
+
diff --git a/mediarouter/mediarouter/api/res-1.4.0-beta02.txt b/mediarouter/mediarouter/api/res-1.4.0-beta02.txt
new file mode 100644
index 0000000..620c3fe
--- /dev/null
+++ b/mediarouter/mediarouter/api/res-1.4.0-beta02.txt
@@ -0,0 +1,4 @@
+dimen mediarouter_chooser_list_item_padding_bottom
+dimen mediarouter_chooser_list_item_padding_end
+dimen mediarouter_chooser_list_item_padding_start
+dimen mediarouter_chooser_list_item_padding_top
diff --git a/mediarouter/mediarouter/api/restricted_1.4.0-beta02.txt b/mediarouter/mediarouter/api/restricted_1.4.0-beta02.txt
new file mode 100644
index 0000000..b1cf094
--- /dev/null
+++ b/mediarouter/mediarouter/api/restricted_1.4.0-beta02.txt
@@ -0,0 +1,558 @@
+// Signature format: 4.0
+package androidx.mediarouter.app {
+
+  public class MediaRouteActionProvider extends androidx.core.view.ActionProvider {
+    ctor public MediaRouteActionProvider(android.content.Context);
+    method @Deprecated public void enableDynamicGroup();
+    method public androidx.mediarouter.app.MediaRouteDialogFactory getDialogFactory();
+    method public androidx.mediarouter.app.MediaRouteButton? getMediaRouteButton();
+    method public androidx.mediarouter.media.MediaRouteSelector getRouteSelector();
+    method public android.view.View onCreateActionView();
+    method public androidx.mediarouter.app.MediaRouteButton onCreateMediaRouteButton();
+    method public void setAlwaysVisible(boolean);
+    method public void setDialogFactory(androidx.mediarouter.app.MediaRouteDialogFactory);
+    method public void setRouteSelector(androidx.mediarouter.media.MediaRouteSelector);
+  }
+
+  public class MediaRouteButton extends android.view.View {
+    ctor public MediaRouteButton(android.content.Context);
+    ctor public MediaRouteButton(android.content.Context, android.util.AttributeSet?);
+    ctor public MediaRouteButton(android.content.Context, android.util.AttributeSet?, int);
+    method @Deprecated public void enableDynamicGroup();
+    method public androidx.mediarouter.app.MediaRouteDialogFactory getDialogFactory();
+    method public androidx.mediarouter.media.MediaRouteSelector getRouteSelector();
+    method public void onAttachedToWindow();
+    method public void onDetachedFromWindow();
+    method public void setAlwaysVisible(boolean);
+    method public void setDialogFactory(androidx.mediarouter.app.MediaRouteDialogFactory);
+    method public void setRemoteIndicatorDrawable(android.graphics.drawable.Drawable?);
+    method public void setRouteSelector(androidx.mediarouter.media.MediaRouteSelector);
+    method public boolean showDialog();
+  }
+
+  public class MediaRouteChooserDialog extends androidx.appcompat.app.AppCompatDialog {
+    ctor public MediaRouteChooserDialog(android.content.Context);
+    ctor public MediaRouteChooserDialog(android.content.Context, int);
+    method public androidx.mediarouter.media.MediaRouteSelector getRouteSelector();
+    method public boolean onFilterRoute(androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void onFilterRoutes(java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!>);
+    method public void refreshRoutes();
+    method public void setRouteSelector(androidx.mediarouter.media.MediaRouteSelector);
+  }
+
+  public class MediaRouteChooserDialogFragment extends androidx.fragment.app.DialogFragment {
+    ctor public MediaRouteChooserDialogFragment();
+    method public androidx.mediarouter.media.MediaRouteSelector getRouteSelector();
+    method public androidx.mediarouter.app.MediaRouteChooserDialog onCreateChooserDialog(android.content.Context, android.os.Bundle?);
+    method public void setRouteSelector(androidx.mediarouter.media.MediaRouteSelector);
+  }
+
+  public class MediaRouteControllerDialog extends androidx.appcompat.app.AlertDialog {
+    ctor public MediaRouteControllerDialog(android.content.Context);
+    ctor public MediaRouteControllerDialog(android.content.Context, int);
+    method public android.view.View? getMediaControlView();
+    method public android.support.v4.media.session.MediaSessionCompat.Token? getMediaSession();
+    method public androidx.mediarouter.media.MediaRouter.RouteInfo getRoute();
+    method public boolean isVolumeControlEnabled();
+    method public android.view.View? onCreateMediaControlView(android.os.Bundle?);
+    method public void setVolumeControlEnabled(boolean);
+  }
+
+  public class MediaRouteControllerDialogFragment extends androidx.fragment.app.DialogFragment {
+    ctor public MediaRouteControllerDialogFragment();
+    method public androidx.mediarouter.app.MediaRouteControllerDialog onCreateControllerDialog(android.content.Context, android.os.Bundle?);
+  }
+
+  public class MediaRouteDialogFactory {
+    ctor public MediaRouteDialogFactory();
+    method public static androidx.mediarouter.app.MediaRouteDialogFactory getDefault();
+    method public androidx.mediarouter.app.MediaRouteChooserDialogFragment onCreateChooserDialogFragment();
+    method public androidx.mediarouter.app.MediaRouteControllerDialogFragment onCreateControllerDialogFragment();
+  }
+
+  public class MediaRouteDiscoveryFragment extends androidx.fragment.app.Fragment {
+    ctor public MediaRouteDiscoveryFragment();
+    method public androidx.mediarouter.media.MediaRouter getMediaRouter();
+    method public androidx.mediarouter.media.MediaRouteSelector getRouteSelector();
+    method public androidx.mediarouter.media.MediaRouter.Callback? onCreateCallback();
+    method public int onPrepareCallbackFlags();
+    method public void setRouteSelector(androidx.mediarouter.media.MediaRouteSelector);
+  }
+
+  public final class SystemOutputSwitcherDialogController {
+    method public static boolean showDialog(android.content.Context);
+  }
+
+}
+
+package androidx.mediarouter.media {
+
+  public final class MediaControlIntent {
+    field public static final String ACTION_END_SESSION = "android.media.intent.action.END_SESSION";
+    field public static final String ACTION_ENQUEUE = "android.media.intent.action.ENQUEUE";
+    field public static final String ACTION_GET_SESSION_STATUS = "android.media.intent.action.GET_SESSION_STATUS";
+    field public static final String ACTION_GET_STATUS = "android.media.intent.action.GET_STATUS";
+    field public static final String ACTION_PAUSE = "android.media.intent.action.PAUSE";
+    field public static final String ACTION_PLAY = "android.media.intent.action.PLAY";
+    field public static final String ACTION_REMOVE = "android.media.intent.action.REMOVE";
+    field public static final String ACTION_RESUME = "android.media.intent.action.RESUME";
+    field public static final String ACTION_SEEK = "android.media.intent.action.SEEK";
+    field public static final String ACTION_SEND_MESSAGE = "android.media.intent.action.SEND_MESSAGE";
+    field public static final String ACTION_START_SESSION = "android.media.intent.action.START_SESSION";
+    field public static final String ACTION_STOP = "android.media.intent.action.STOP";
+    field public static final String CATEGORY_LIVE_AUDIO = "android.media.intent.category.LIVE_AUDIO";
+    field public static final String CATEGORY_LIVE_VIDEO = "android.media.intent.category.LIVE_VIDEO";
+    field public static final String CATEGORY_REMOTE_PLAYBACK = "android.media.intent.category.REMOTE_PLAYBACK";
+    field public static final int ERROR_INVALID_ITEM_ID = 3; // 0x3
+    field public static final int ERROR_INVALID_SESSION_ID = 2; // 0x2
+    field public static final int ERROR_UNKNOWN = 0; // 0x0
+    field public static final int ERROR_UNSUPPORTED_OPERATION = 1; // 0x1
+    field public static final String EXTRA_ERROR_CODE = "android.media.intent.extra.ERROR_CODE";
+    field public static final String EXTRA_ITEM_CONTENT_POSITION = "android.media.intent.extra.ITEM_POSITION";
+    field public static final String EXTRA_ITEM_HTTP_HEADERS = "android.media.intent.extra.HTTP_HEADERS";
+    field public static final String EXTRA_ITEM_ID = "android.media.intent.extra.ITEM_ID";
+    field public static final String EXTRA_ITEM_METADATA = "android.media.intent.extra.ITEM_METADATA";
+    field public static final String EXTRA_ITEM_STATUS = "android.media.intent.extra.ITEM_STATUS";
+    field public static final String EXTRA_ITEM_STATUS_UPDATE_RECEIVER = "android.media.intent.extra.ITEM_STATUS_UPDATE_RECEIVER";
+    field public static final String EXTRA_MESSAGE = "android.media.intent.extra.MESSAGE";
+    field public static final String EXTRA_MESSAGE_RECEIVER = "android.media.intent.extra.MESSAGE_RECEIVER";
+    field public static final String EXTRA_SESSION_ID = "android.media.intent.extra.SESSION_ID";
+    field public static final String EXTRA_SESSION_STATUS = "android.media.intent.extra.SESSION_STATUS";
+    field public static final String EXTRA_SESSION_STATUS_UPDATE_RECEIVER = "android.media.intent.extra.SESSION_STATUS_UPDATE_RECEIVER";
+  }
+
+  public final class MediaItemMetadata {
+    field public static final String KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
+    field public static final String KEY_ALBUM_TITLE = "android.media.metadata.ALBUM_TITLE";
+    field public static final String KEY_ARTIST = "android.media.metadata.ARTIST";
+    field public static final String KEY_ARTWORK_URI = "android.media.metadata.ARTWORK_URI";
+    field public static final String KEY_AUTHOR = "android.media.metadata.AUTHOR";
+    field public static final String KEY_COMPOSER = "android.media.metadata.COMPOSER";
+    field public static final String KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
+    field public static final String KEY_DURATION = "android.media.metadata.DURATION";
+    field public static final String KEY_TITLE = "android.media.metadata.TITLE";
+    field public static final String KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
+    field public static final String KEY_YEAR = "android.media.metadata.YEAR";
+  }
+
+  public final class MediaItemStatus {
+    method public android.os.Bundle asBundle();
+    method public static androidx.mediarouter.media.MediaItemStatus? fromBundle(android.os.Bundle?);
+    method public long getContentDuration();
+    method public long getContentPosition();
+    method public android.os.Bundle? getExtras();
+    method public int getPlaybackState();
+    method public long getTimestamp();
+    field public static final String EXTRA_HTTP_RESPONSE_HEADERS = "android.media.status.extra.HTTP_RESPONSE_HEADERS";
+    field public static final String EXTRA_HTTP_STATUS_CODE = "android.media.status.extra.HTTP_STATUS_CODE";
+    field public static final int PLAYBACK_STATE_BUFFERING = 3; // 0x3
+    field public static final int PLAYBACK_STATE_CANCELED = 5; // 0x5
+    field public static final int PLAYBACK_STATE_ERROR = 7; // 0x7
+    field public static final int PLAYBACK_STATE_FINISHED = 4; // 0x4
+    field public static final int PLAYBACK_STATE_INVALIDATED = 6; // 0x6
+    field public static final int PLAYBACK_STATE_PAUSED = 2; // 0x2
+    field public static final int PLAYBACK_STATE_PENDING = 0; // 0x0
+    field public static final int PLAYBACK_STATE_PLAYING = 1; // 0x1
+  }
+
+  public static final class MediaItemStatus.Builder {
+    ctor public MediaItemStatus.Builder(int);
+    ctor public MediaItemStatus.Builder(androidx.mediarouter.media.MediaItemStatus);
+    method public androidx.mediarouter.media.MediaItemStatus build();
+    method public androidx.mediarouter.media.MediaItemStatus.Builder setContentDuration(long);
+    method public androidx.mediarouter.media.MediaItemStatus.Builder setContentPosition(long);
+    method public androidx.mediarouter.media.MediaItemStatus.Builder setExtras(android.os.Bundle?);
+    method public androidx.mediarouter.media.MediaItemStatus.Builder setPlaybackState(int);
+    method public androidx.mediarouter.media.MediaItemStatus.Builder setTimestamp(long);
+  }
+
+  public final class MediaRouteDescriptor {
+    method public android.os.Bundle asBundle();
+    method public boolean canDisconnectAndKeepPlaying();
+    method public static androidx.mediarouter.media.MediaRouteDescriptor? fromBundle(android.os.Bundle?);
+    method public int getConnectionState();
+    method public java.util.List<android.content.IntentFilter!> getControlFilters();
+    method public String? getDescription();
+    method public int getDeviceType();
+    method public android.os.Bundle? getExtras();
+    method public android.net.Uri? getIconUri();
+    method public String getId();
+    method public String getName();
+    method public int getPlaybackStream();
+    method public int getPlaybackType();
+    method public int getPresentationDisplayId();
+    method public android.content.IntentSender? getSettingsActivity();
+    method public int getVolume();
+    method public int getVolumeHandling();
+    method public int getVolumeMax();
+    method @Deprecated public boolean isConnecting();
+    method public boolean isDynamicGroupRoute();
+    method public boolean isEnabled();
+    method public boolean isValid();
+  }
+
+  public static final class MediaRouteDescriptor.Builder {
+    ctor public MediaRouteDescriptor.Builder(String, String);
+    ctor public MediaRouteDescriptor.Builder(androidx.mediarouter.media.MediaRouteDescriptor);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder addControlFilter(android.content.IntentFilter);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder addControlFilters(java.util.Collection<android.content.IntentFilter!>);
+    method public androidx.mediarouter.media.MediaRouteDescriptor build();
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder clearControlFilters();
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setCanDisconnect(boolean);
+    method @Deprecated public androidx.mediarouter.media.MediaRouteDescriptor.Builder setConnecting(boolean);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setConnectionState(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDescription(String?);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDeviceType(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setEnabled(boolean);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setExtras(android.os.Bundle?);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setIconUri(android.net.Uri);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setId(String);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setIsDynamicGroupRoute(boolean);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setName(String);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setPlaybackStream(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setPlaybackType(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setPresentationDisplayId(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setSettingsActivity(android.content.IntentSender?);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVolume(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVolumeHandling(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVolumeMax(int);
+  }
+
+  public final class MediaRouteDiscoveryRequest {
+    ctor public MediaRouteDiscoveryRequest(androidx.mediarouter.media.MediaRouteSelector, boolean);
+    method public android.os.Bundle asBundle();
+    method public static androidx.mediarouter.media.MediaRouteDiscoveryRequest? fromBundle(android.os.Bundle?);
+    method public androidx.mediarouter.media.MediaRouteSelector getSelector();
+    method public boolean isActiveScan();
+    method public boolean isValid();
+  }
+
+  public abstract class MediaRouteProvider {
+    ctor public MediaRouteProvider(android.content.Context);
+    method public final android.content.Context getContext();
+    method public final androidx.mediarouter.media.MediaRouteProviderDescriptor? getDescriptor();
+    method public final androidx.mediarouter.media.MediaRouteDiscoveryRequest? getDiscoveryRequest();
+    method public final android.os.Handler getHandler();
+    method public final androidx.mediarouter.media.MediaRouteProvider.ProviderMetadata getMetadata();
+    method public androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController? onCreateDynamicGroupRouteController(String);
+    method public androidx.mediarouter.media.MediaRouteProvider.RouteController? onCreateRouteController(String);
+    method public void onDiscoveryRequestChanged(androidx.mediarouter.media.MediaRouteDiscoveryRequest?);
+    method public final void setCallback(androidx.mediarouter.media.MediaRouteProvider.Callback?);
+    method public final void setDescriptor(androidx.mediarouter.media.MediaRouteProviderDescriptor?);
+    method public final void setDiscoveryRequest(androidx.mediarouter.media.MediaRouteDiscoveryRequest?);
+  }
+
+  public abstract static class MediaRouteProvider.Callback {
+    ctor public MediaRouteProvider.Callback();
+    method public void onDescriptorChanged(androidx.mediarouter.media.MediaRouteProvider, androidx.mediarouter.media.MediaRouteProviderDescriptor?);
+  }
+
+  public abstract static class MediaRouteProvider.DynamicGroupRouteController extends androidx.mediarouter.media.MediaRouteProvider.RouteController {
+    ctor public MediaRouteProvider.DynamicGroupRouteController();
+    method public String? getGroupableSelectionTitle();
+    method public String? getTransferableSectionTitle();
+    method @Deprecated public final void notifyDynamicRoutesChanged(java.util.Collection<androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor!>);
+    method public final void notifyDynamicRoutesChanged(androidx.mediarouter.media.MediaRouteDescriptor, java.util.Collection<androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor!>);
+    method public abstract void onAddMemberRoute(String);
+    method public abstract void onRemoveMemberRoute(String);
+    method public abstract void onUpdateMemberRoutes(java.util.List<java.lang.String!>?);
+  }
+
+  public static final class MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor {
+    method public androidx.mediarouter.media.MediaRouteDescriptor getRouteDescriptor();
+    method public int getSelectionState();
+    method public boolean isGroupable();
+    method public boolean isTransferable();
+    method public boolean isUnselectable();
+    field public static final int SELECTED = 3; // 0x3
+    field public static final int SELECTING = 2; // 0x2
+    field public static final int UNSELECTED = 1; // 0x1
+    field public static final int UNSELECTING = 0; // 0x0
+  }
+
+  public static final class MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder {
+    ctor public MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder(androidx.mediarouter.media.MediaRouteDescriptor);
+    ctor public MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder(androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor);
+    method public androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor build();
+    method public androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder setIsGroupable(boolean);
+    method public androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder setIsTransferable(boolean);
+    method public androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder setIsUnselectable(boolean);
+    method public androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.Builder setSelectionState(int);
+  }
+
+  public static final class MediaRouteProvider.ProviderMetadata {
+    method public android.content.ComponentName getComponentName();
+    method public String getPackageName();
+  }
+
+  public abstract static class MediaRouteProvider.RouteController {
+    ctor public MediaRouteProvider.RouteController();
+    method public boolean onControlRequest(android.content.Intent, androidx.mediarouter.media.MediaRouter.ControlRequestCallback?);
+    method public void onRelease();
+    method public void onSelect();
+    method public void onSetVolume(int);
+    method @Deprecated public void onUnselect();
+    method public void onUnselect(int);
+    method public void onUpdateVolume(int);
+  }
+
+  public final class MediaRouteProviderDescriptor {
+    method public android.os.Bundle asBundle();
+    method public static androidx.mediarouter.media.MediaRouteProviderDescriptor? fromBundle(android.os.Bundle?);
+    method public java.util.List<androidx.mediarouter.media.MediaRouteDescriptor!> getRoutes();
+    method public boolean isValid();
+    method public boolean supportsDynamicGroupRoute();
+  }
+
+  public static final class MediaRouteProviderDescriptor.Builder {
+    ctor public MediaRouteProviderDescriptor.Builder();
+    ctor public MediaRouteProviderDescriptor.Builder(androidx.mediarouter.media.MediaRouteProviderDescriptor);
+    method public androidx.mediarouter.media.MediaRouteProviderDescriptor.Builder addRoute(androidx.mediarouter.media.MediaRouteDescriptor);
+    method public androidx.mediarouter.media.MediaRouteProviderDescriptor.Builder addRoutes(java.util.Collection<androidx.mediarouter.media.MediaRouteDescriptor!>);
+    method public androidx.mediarouter.media.MediaRouteProviderDescriptor build();
+    method public androidx.mediarouter.media.MediaRouteProviderDescriptor.Builder setSupportsDynamicGroupRoute(boolean);
+  }
+
+  public abstract class MediaRouteProviderService extends android.app.Service {
+    ctor public MediaRouteProviderService();
+    method public androidx.mediarouter.media.MediaRouteProvider? getMediaRouteProvider();
+    method public android.os.IBinder? onBind(android.content.Intent);
+    method public abstract androidx.mediarouter.media.MediaRouteProvider? onCreateMediaRouteProvider();
+    field public static final String SERVICE_INTERFACE = "android.media.MediaRouteProviderService";
+  }
+
+  public final class MediaRouteSelector {
+    method public android.os.Bundle asBundle();
+    method public boolean contains(androidx.mediarouter.media.MediaRouteSelector);
+    method public static androidx.mediarouter.media.MediaRouteSelector? fromBundle(android.os.Bundle?);
+    method public java.util.List<java.lang.String!> getControlCategories();
+    method public boolean hasControlCategory(String?);
+    method public boolean isEmpty();
+    method public boolean isValid();
+    method public boolean matchesControlFilters(java.util.List<android.content.IntentFilter!>?);
+    field public static final androidx.mediarouter.media.MediaRouteSelector! EMPTY;
+  }
+
+  public static final class MediaRouteSelector.Builder {
+    ctor public MediaRouteSelector.Builder();
+    ctor public MediaRouteSelector.Builder(androidx.mediarouter.media.MediaRouteSelector);
+    method public androidx.mediarouter.media.MediaRouteSelector.Builder addControlCategories(java.util.Collection<java.lang.String!>);
+    method public androidx.mediarouter.media.MediaRouteSelector.Builder addControlCategory(String);
+    method public androidx.mediarouter.media.MediaRouteSelector.Builder addSelector(androidx.mediarouter.media.MediaRouteSelector);
+    method public androidx.mediarouter.media.MediaRouteSelector build();
+  }
+
+  public final class MediaRouter {
+    method @MainThread public void addCallback(androidx.mediarouter.media.MediaRouteSelector, androidx.mediarouter.media.MediaRouter.Callback);
+    method @MainThread public void addCallback(androidx.mediarouter.media.MediaRouteSelector, androidx.mediarouter.media.MediaRouter.Callback, int);
+    method @MainThread public void addProvider(androidx.mediarouter.media.MediaRouteProvider);
+    method @MainThread public void addRemoteControlClient(Object);
+    method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo? getBluetoothRoute();
+    method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo getDefaultRoute();
+    method @MainThread public static androidx.mediarouter.media.MediaRouter getInstance(android.content.Context);
+    method public android.support.v4.media.session.MediaSessionCompat.Token? getMediaSessionToken();
+    method @MainThread public java.util.List<androidx.mediarouter.media.MediaRouter.ProviderInfo!> getProviders();
+    method @MainThread public androidx.mediarouter.media.MediaRouterParams? getRouterParams();
+    method @MainThread public java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!> getRoutes();
+    method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo getSelectedRoute();
+    method @MainThread public boolean isRouteAvailable(androidx.mediarouter.media.MediaRouteSelector, int);
+    method @MainThread public void removeCallback(androidx.mediarouter.media.MediaRouter.Callback);
+    method @MainThread public void removeProvider(androidx.mediarouter.media.MediaRouteProvider);
+    method @MainThread public void removeRemoteControlClient(Object);
+    method @MainThread public void selectRoute(androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method @MainThread public void setMediaSession(Object?);
+    method @MainThread public void setMediaSessionCompat(android.support.v4.media.session.MediaSessionCompat?);
+    method @MainThread public void setOnPrepareTransferListener(androidx.mediarouter.media.MediaRouter.OnPrepareTransferListener?);
+    method @MainThread public void setRouterParams(androidx.mediarouter.media.MediaRouterParams?);
+    method @MainThread public void unselect(int);
+    method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo updateSelectedRoute(androidx.mediarouter.media.MediaRouteSelector);
+    field public static final int AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE = 1; // 0x1
+    field public static final int AVAILABILITY_FLAG_REQUIRE_MATCH = 2; // 0x2
+    field public static final int CALLBACK_FLAG_FORCE_DISCOVERY = 8; // 0x8
+    field public static final int CALLBACK_FLAG_PERFORM_ACTIVE_SCAN = 1; // 0x1
+    field public static final int CALLBACK_FLAG_REQUEST_DISCOVERY = 4; // 0x4
+    field public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 2; // 0x2
+    field public static final int UNSELECT_REASON_DISCONNECTED = 1; // 0x1
+    field public static final int UNSELECT_REASON_ROUTE_CHANGED = 3; // 0x3
+    field public static final int UNSELECT_REASON_STOPPED = 2; // 0x2
+    field public static final int UNSELECT_REASON_UNKNOWN = 0; // 0x0
+  }
+
+  public abstract static class MediaRouter.Callback {
+    ctor public MediaRouter.Callback();
+    method public void onProviderAdded(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.ProviderInfo);
+    method public void onProviderChanged(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.ProviderInfo);
+    method public void onProviderRemoved(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.ProviderInfo);
+    method public void onRouteAdded(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void onRouteChanged(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void onRoutePresentationDisplayChanged(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void onRouteRemoved(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method @Deprecated public void onRouteSelected(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void onRouteSelected(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo, int);
+    method public void onRouteSelected(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo, int, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method @Deprecated public void onRouteUnselected(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void onRouteUnselected(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo, int);
+    method public void onRouteVolumeChanged(androidx.mediarouter.media.MediaRouter, androidx.mediarouter.media.MediaRouter.RouteInfo);
+  }
+
+  public abstract static class MediaRouter.ControlRequestCallback {
+    ctor public MediaRouter.ControlRequestCallback();
+    method public void onError(String?, android.os.Bundle?);
+    method public void onResult(android.os.Bundle?);
+  }
+
+  public static interface MediaRouter.OnPrepareTransferListener {
+    method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!>? onPrepareTransfer(androidx.mediarouter.media.MediaRouter.RouteInfo, androidx.mediarouter.media.MediaRouter.RouteInfo);
+  }
+
+  public static final class MediaRouter.ProviderInfo {
+    method public android.content.ComponentName getComponentName();
+    method public String getPackageName();
+    method @MainThread public androidx.mediarouter.media.MediaRouteProvider getProviderInstance();
+    method @MainThread public java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!> getRoutes();
+  }
+
+  public static class MediaRouter.RouteInfo {
+    method public boolean canDisconnect();
+    method public int getConnectionState();
+    method public java.util.List<android.content.IntentFilter!> getControlFilters();
+    method public String? getDescription();
+    method public int getDeviceType();
+    method public android.os.Bundle? getExtras();
+    method public android.net.Uri? getIconUri();
+    method public String getId();
+    method public String getName();
+    method public int getPlaybackStream();
+    method public int getPlaybackType();
+    method @MainThread public android.view.Display? getPresentationDisplay();
+    method public androidx.mediarouter.media.MediaRouter.ProviderInfo getProvider();
+    method public android.content.IntentSender? getSettingsIntent();
+    method public int getVolume();
+    method public int getVolumeHandling();
+    method public int getVolumeMax();
+    method @MainThread public boolean isBluetooth();
+    method @Deprecated public boolean isConnecting();
+    method @MainThread public boolean isDefault();
+    method public boolean isDeviceSpeaker();
+    method public boolean isEnabled();
+    method @MainThread public boolean isSelected();
+    method @MainThread public boolean matchesSelector(androidx.mediarouter.media.MediaRouteSelector);
+    method @MainThread public void requestSetVolume(int);
+    method @MainThread public void requestUpdateVolume(int);
+    method @MainThread public void select();
+    method @MainThread public void sendControlRequest(android.content.Intent, androidx.mediarouter.media.MediaRouter.ControlRequestCallback?);
+    method @MainThread public boolean supportsControlAction(String, String);
+    method @MainThread public boolean supportsControlCategory(String);
+    method @MainThread public boolean supportsControlRequest(android.content.Intent);
+    field public static final int CONNECTION_STATE_CONNECTED = 2; // 0x2
+    field public static final int CONNECTION_STATE_CONNECTING = 1; // 0x1
+    field public static final int CONNECTION_STATE_DISCONNECTED = 0; // 0x0
+    field public static final int DEVICE_TYPE_SPEAKER = 2; // 0x2
+    field public static final int DEVICE_TYPE_TV = 1; // 0x1
+    field public static final int PLAYBACK_TYPE_LOCAL = 0; // 0x0
+    field public static final int PLAYBACK_TYPE_REMOTE = 1; // 0x1
+    field public static final int PLAYBACK_VOLUME_FIXED = 0; // 0x0
+    field public static final int PLAYBACK_VOLUME_VARIABLE = 1; // 0x1
+  }
+
+  public class MediaRouterParams {
+    method public int getDialogType();
+    method public boolean isMediaTransferReceiverEnabled();
+    method public boolean isOutputSwitcherEnabled();
+    method public boolean isTransferToLocalEnabled();
+    field public static final int DIALOG_TYPE_DEFAULT = 1; // 0x1
+    field public static final int DIALOG_TYPE_DYNAMIC_GROUP = 2; // 0x2
+    field public static final String ENABLE_GROUP_VOLUME_UX = "androidx.mediarouter.media.MediaRouterParams.ENABLE_GROUP_VOLUME_UX";
+  }
+
+  public static final class MediaRouterParams.Builder {
+    ctor public MediaRouterParams.Builder();
+    ctor public MediaRouterParams.Builder(androidx.mediarouter.media.MediaRouterParams);
+    method public androidx.mediarouter.media.MediaRouterParams build();
+    method public androidx.mediarouter.media.MediaRouterParams.Builder setDialogType(int);
+    method public androidx.mediarouter.media.MediaRouterParams.Builder setMediaTransferReceiverEnabled(boolean);
+    method public androidx.mediarouter.media.MediaRouterParams.Builder setOutputSwitcherEnabled(boolean);
+    method public androidx.mediarouter.media.MediaRouterParams.Builder setTransferToLocalEnabled(boolean);
+  }
+
+  public final class MediaSessionStatus {
+    method public android.os.Bundle asBundle();
+    method public static androidx.mediarouter.media.MediaSessionStatus? fromBundle(android.os.Bundle?);
+    method public android.os.Bundle? getExtras();
+    method public int getSessionState();
+    method public long getTimestamp();
+    method public boolean isQueuePaused();
+    field public static final int SESSION_STATE_ACTIVE = 0; // 0x0
+    field public static final int SESSION_STATE_ENDED = 1; // 0x1
+    field public static final int SESSION_STATE_INVALIDATED = 2; // 0x2
+  }
+
+  public static final class MediaSessionStatus.Builder {
+    ctor public MediaSessionStatus.Builder(int);
+    ctor public MediaSessionStatus.Builder(androidx.mediarouter.media.MediaSessionStatus);
+    method public androidx.mediarouter.media.MediaSessionStatus build();
+    method public androidx.mediarouter.media.MediaSessionStatus.Builder setExtras(android.os.Bundle?);
+    method public androidx.mediarouter.media.MediaSessionStatus.Builder setQueuePaused(boolean);
+    method public androidx.mediarouter.media.MediaSessionStatus.Builder setSessionState(int);
+    method public androidx.mediarouter.media.MediaSessionStatus.Builder setTimestamp(long);
+  }
+
+  public final class MediaTransferReceiver extends android.content.BroadcastReceiver {
+    ctor public MediaTransferReceiver();
+    method public void onReceive(android.content.Context, android.content.Intent);
+  }
+
+  public class RemotePlaybackClient {
+    ctor public RemotePlaybackClient(android.content.Context, androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public void endSession(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+    method public void enqueue(android.net.Uri, String?, android.os.Bundle?, long, android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback?);
+    method public String? getSessionId();
+    method public void getSessionStatus(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+    method public void getStatus(String, android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback?);
+    method public boolean hasSession();
+    method public boolean isMessagingSupported();
+    method public boolean isQueuingSupported();
+    method public boolean isRemotePlaybackSupported();
+    method public boolean isSessionManagementSupported();
+    method public void pause(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+    method public void play(android.net.Uri, String?, android.os.Bundle?, long, android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback?);
+    method public void release();
+    method public void remove(String, android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback?);
+    method public void resume(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+    method public void seek(String, long, android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback?);
+    method public void sendMessage(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+    method public void setOnMessageReceivedListener(androidx.mediarouter.media.RemotePlaybackClient.OnMessageReceivedListener?);
+    method public void setSessionId(String?);
+    method public void setStatusCallback(androidx.mediarouter.media.RemotePlaybackClient.StatusCallback?);
+    method public void startSession(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+    method public void stop(android.os.Bundle?, androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback?);
+  }
+
+  public abstract static class RemotePlaybackClient.ActionCallback {
+    ctor public RemotePlaybackClient.ActionCallback();
+    method public void onError(String?, int, android.os.Bundle?);
+  }
+
+  public abstract static class RemotePlaybackClient.ItemActionCallback extends androidx.mediarouter.media.RemotePlaybackClient.ActionCallback {
+    ctor public RemotePlaybackClient.ItemActionCallback();
+    method public void onResult(android.os.Bundle, String, androidx.mediarouter.media.MediaSessionStatus?, String, androidx.mediarouter.media.MediaItemStatus);
+  }
+
+  public static interface RemotePlaybackClient.OnMessageReceivedListener {
+    method public void onMessageReceived(String, android.os.Bundle?);
+  }
+
+  public abstract static class RemotePlaybackClient.SessionActionCallback extends androidx.mediarouter.media.RemotePlaybackClient.ActionCallback {
+    ctor public RemotePlaybackClient.SessionActionCallback();
+    method public void onResult(android.os.Bundle, String, androidx.mediarouter.media.MediaSessionStatus?);
+  }
+
+  public abstract static class RemotePlaybackClient.StatusCallback {
+    ctor public RemotePlaybackClient.StatusCallback();
+    method public void onItemStatusChanged(android.os.Bundle?, String, androidx.mediarouter.media.MediaSessionStatus?, String, androidx.mediarouter.media.MediaItemStatus);
+    method public void onSessionChanged(String?);
+    method public void onSessionStatusChanged(android.os.Bundle?, String, androidx.mediarouter.media.MediaSessionStatus?);
+  }
+
+}
+
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/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProvider.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProvider.java
index ed033e1..34f191f 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProvider.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProvider.java
@@ -602,6 +602,10 @@
          *                   the route controller.
          * @param dynamicRoutes The dynamic route descriptors for published routes.
          *                      At least a selected or selecting route should be included.
+         *
+         * @throws IllegalArgumentException Thrown when no dynamic route descriptors are {@link
+         * DynamicRouteDescriptor#SELECTED SELECTED} or {@link DynamicRouteDescriptor#SELECTING
+         * SELECTING}.
          */
         public final void notifyDynamicRoutesChanged(
                 @NonNull MediaRouteDescriptor groupRoute,
@@ -612,6 +616,23 @@
             if (dynamicRoutes == null) {
                 throw new NullPointerException("dynamicRoutes must not be null");
             }
+
+            boolean hasSelectedRoute = false;
+            for (DynamicRouteDescriptor route: dynamicRoutes) {
+                int state = route.getSelectionState();
+                if (state == DynamicRouteDescriptor.SELECTED
+                        || state == DynamicRouteDescriptor.SELECTING) {
+                    hasSelectedRoute = true;
+                    break;
+                }
+            }
+
+            if (!hasSelectedRoute) {
+                throw new IllegalArgumentException("dynamicRoutes must have at least one selected"
+                        + " or selecting route.");
+
+            }
+
             synchronized (mLock) {
                 if (mExecutor != null) {
                     final OnDynamicRoutesChangedListener listener = mListener;
diff --git a/navigation/navigation-common/api/current.txt b/navigation/navigation-common/api/current.txt
index 2133a87..feb3c32 100644
--- a/navigation/navigation-common/api/current.txt
+++ b/navigation/navigation-common/api/current.txt
@@ -123,10 +123,14 @@
     method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
     method public androidx.lifecycle.ViewModelStore getViewModelStore();
     property public final android.os.Bundle? arguments;
+    property public androidx.lifecycle.viewmodel.CreationExtras defaultViewModelCreationExtras;
+    property public androidx.lifecycle.ViewModelProvider.Factory defaultViewModelProviderFactory;
     property public final androidx.navigation.NavDestination destination;
     property public final String id;
+    property public androidx.lifecycle.Lifecycle lifecycle;
     property public final androidx.lifecycle.SavedStateHandle savedStateHandle;
     property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
+    property public androidx.lifecycle.ViewModelStore viewModelStore;
   }
 
   public final class NavDeepLink {
@@ -505,6 +509,7 @@
     method public final kotlinx.coroutines.flow.StateFlow<java.util.Set<androidx.navigation.NavBackStackEntry>> getTransitionsInProgress();
     method public void markTransitionComplete(androidx.navigation.NavBackStackEntry entry);
     method @CallSuper public void onLaunchSingleTop(androidx.navigation.NavBackStackEntry backStackEntry);
+    method @CallSuper public void onLaunchSingleTopWithTransition(androidx.navigation.NavBackStackEntry backStackEntry);
     method public void pop(androidx.navigation.NavBackStackEntry popUpTo, boolean saveState);
     method public void popWithTransition(androidx.navigation.NavBackStackEntry popUpTo, boolean saveState);
     method public void push(androidx.navigation.NavBackStackEntry backStackEntry);
diff --git a/navigation/navigation-common/api/public_plus_experimental_current.txt b/navigation/navigation-common/api/public_plus_experimental_current.txt
index 2133a87..feb3c32 100644
--- a/navigation/navigation-common/api/public_plus_experimental_current.txt
+++ b/navigation/navigation-common/api/public_plus_experimental_current.txt
@@ -123,10 +123,14 @@
     method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
     method public androidx.lifecycle.ViewModelStore getViewModelStore();
     property public final android.os.Bundle? arguments;
+    property public androidx.lifecycle.viewmodel.CreationExtras defaultViewModelCreationExtras;
+    property public androidx.lifecycle.ViewModelProvider.Factory defaultViewModelProviderFactory;
     property public final androidx.navigation.NavDestination destination;
     property public final String id;
+    property public androidx.lifecycle.Lifecycle lifecycle;
     property public final androidx.lifecycle.SavedStateHandle savedStateHandle;
     property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
+    property public androidx.lifecycle.ViewModelStore viewModelStore;
   }
 
   public final class NavDeepLink {
@@ -505,6 +509,7 @@
     method public final kotlinx.coroutines.flow.StateFlow<java.util.Set<androidx.navigation.NavBackStackEntry>> getTransitionsInProgress();
     method public void markTransitionComplete(androidx.navigation.NavBackStackEntry entry);
     method @CallSuper public void onLaunchSingleTop(androidx.navigation.NavBackStackEntry backStackEntry);
+    method @CallSuper public void onLaunchSingleTopWithTransition(androidx.navigation.NavBackStackEntry backStackEntry);
     method public void pop(androidx.navigation.NavBackStackEntry popUpTo, boolean saveState);
     method public void popWithTransition(androidx.navigation.NavBackStackEntry popUpTo, boolean saveState);
     method public void push(androidx.navigation.NavBackStackEntry backStackEntry);
diff --git a/navigation/navigation-common/api/restricted_current.txt b/navigation/navigation-common/api/restricted_current.txt
index 2133a87..feb3c32 100644
--- a/navigation/navigation-common/api/restricted_current.txt
+++ b/navigation/navigation-common/api/restricted_current.txt
@@ -123,10 +123,14 @@
     method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
     method public androidx.lifecycle.ViewModelStore getViewModelStore();
     property public final android.os.Bundle? arguments;
+    property public androidx.lifecycle.viewmodel.CreationExtras defaultViewModelCreationExtras;
+    property public androidx.lifecycle.ViewModelProvider.Factory defaultViewModelProviderFactory;
     property public final androidx.navigation.NavDestination destination;
     property public final String id;
+    property public androidx.lifecycle.Lifecycle lifecycle;
     property public final androidx.lifecycle.SavedStateHandle savedStateHandle;
     property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
+    property public androidx.lifecycle.ViewModelStore viewModelStore;
   }
 
   public final class NavDeepLink {
@@ -505,6 +509,7 @@
     method public final kotlinx.coroutines.flow.StateFlow<java.util.Set<androidx.navigation.NavBackStackEntry>> getTransitionsInProgress();
     method public void markTransitionComplete(androidx.navigation.NavBackStackEntry entry);
     method @CallSuper public void onLaunchSingleTop(androidx.navigation.NavBackStackEntry backStackEntry);
+    method @CallSuper public void onLaunchSingleTopWithTransition(androidx.navigation.NavBackStackEntry backStackEntry);
     method public void pop(androidx.navigation.NavBackStackEntry popUpTo, boolean saveState);
     method public void popWithTransition(androidx.navigation.NavBackStackEntry popUpTo, boolean saveState);
     method public void push(androidx.navigation.NavBackStackEntry backStackEntry);
diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index 08168f1..9f6e589 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -34,6 +34,7 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
+    api(project(":lifecycle:lifecycle-common"))
     api(project(":lifecycle:lifecycle-runtime-ktx"))
     api(project(":lifecycle:lifecycle-viewmodel-ktx"))
     api("androidx.savedstate:savedstate-ktx:1.2.0")
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphAndroidTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphAndroidTest.kt
index 23c9adf..d674582 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphAndroidTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphAndroidTest.kt
@@ -372,8 +372,10 @@
                 setStartDestination(DESTINATION_ROUTE)
                 addDestination(destination)
             }
+        // even though id was set after route, it should still be able to find the destination
+        // based on route
         val expected = "NavGraph(0x${GRAPH_ID.toString(16)}) route=$GRAPH_ROUTE " +
-            "label=$GRAPH_LABEL startDestination=$DESTINATION_ROUTE"
+            "label=$GRAPH_LABEL startDestination={$destination}"
         assertThat(graph.toString()).isEqualTo(expected)
     }
 }
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt
index a1fe535..2803a2e 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt
@@ -107,7 +107,7 @@
         )
     }
 
-    private var lifecycle = LifecycleRegistry(this)
+    private var _lifecycle = LifecycleRegistry(this)
     private val savedStateRegistryController = SavedStateRegistryController.create(this)
     private var savedStateRegistryAttached = false
     private val defaultFactory by lazy {
@@ -154,9 +154,8 @@
      * [androidx.navigation.NavHostController.setLifecycleOwner], the
      * Lifecycle will be capped at [Lifecycle.State.CREATED].
      */
-    public override fun getLifecycle(): Lifecycle {
-        return lifecycle
-    }
+    override val lifecycle: Lifecycle
+        get() = _lifecycle
 
     /** @suppress */
     @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -191,52 +190,52 @@
             savedStateRegistryController.performRestore(savedState)
         }
         if (hostLifecycleState.ordinal < maxLifecycle.ordinal) {
-            lifecycle.currentState = hostLifecycleState
+            _lifecycle.currentState = hostLifecycleState
         } else {
-            lifecycle.currentState = maxLifecycle
+            _lifecycle.currentState = maxLifecycle
         }
     }
 
-    /**
-     * {@inheritDoc}
-     *
-     * @throws IllegalStateException if called before the [lifecycle] has moved to
-     * [Lifecycle.State.CREATED] or before the [androidx.navigation.NavHost] has called
-     * [androidx.navigation.NavHostController.setViewModelStore].
-     */
-    public override fun getViewModelStore(): ViewModelStore {
-        check(savedStateRegistryAttached) {
-            "You cannot access the NavBackStackEntry's ViewModels until it is added to " +
-                "the NavController's back stack (i.e., the Lifecycle of the NavBackStackEntry " +
-                "reaches the CREATED state)."
+    public override val viewModelStore: ViewModelStore
+        /**
+         * {@inheritDoc}
+         *
+         * @throws IllegalStateException if called before the [lifecycle] has moved to
+         * [Lifecycle.State.CREATED] or before the [androidx.navigation.NavHost] has called
+         * [androidx.navigation.NavHostController.setViewModelStore].
+         */
+        get() {
+            check(savedStateRegistryAttached) {
+                "You cannot access the NavBackStackEntry's ViewModels until it is added to " +
+                    "the NavController's back stack (i.e., the Lifecycle of the " +
+                    "NavBackStackEntry reaches the CREATED state)."
+            }
+            check(lifecycle.currentState != Lifecycle.State.DESTROYED) {
+                "You cannot access the NavBackStackEntry's ViewModels after the " +
+                    "NavBackStackEntry is destroyed."
+            }
+            checkNotNull(viewModelStoreProvider) {
+                "You must call setViewModelStore() on your NavHostController before " +
+                    "accessing the ViewModelStore of a navigation graph."
+            }
+            return viewModelStoreProvider.getViewModelStore(id)
         }
-        check(lifecycle.currentState != Lifecycle.State.DESTROYED) {
-            "You cannot access the NavBackStackEntry's ViewModels after the " +
-                "NavBackStackEntry is destroyed."
-        }
-        checkNotNull(viewModelStoreProvider) {
-            "You must call setViewModelStore() on your NavHostController before accessing the " +
-                "ViewModelStore of a navigation graph."
-        }
-        return viewModelStoreProvider.getViewModelStore(id)
-    }
 
-    public override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
-        return defaultFactory
-    }
+    override val defaultViewModelProviderFactory: ViewModelProvider.Factory = defaultFactory
 
-    override fun getDefaultViewModelCreationExtras(): CreationExtras {
-        val extras = MutableCreationExtras()
-        (context?.applicationContext as? Application)?.let { application ->
-            extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] = application
+    override val defaultViewModelCreationExtras: CreationExtras
+        get() {
+            val extras = MutableCreationExtras()
+            (context?.applicationContext as? Application)?.let { application ->
+                extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] = application
+            }
+            extras[SAVED_STATE_REGISTRY_OWNER_KEY] = this
+            extras[VIEW_MODEL_STORE_OWNER_KEY] = this
+            arguments?.let { args ->
+                extras[DEFAULT_ARGS_KEY] = args
+            }
+            return extras
         }
-        extras[SAVED_STATE_REGISTRY_OWNER_KEY] = this
-        extras[VIEW_MODEL_STORE_OWNER_KEY] = this
-        arguments?.let { args ->
-            extras[DEFAULT_ARGS_KEY] = args
-        }
-        return extras
-    }
 
     override val savedStateRegistry: SavedStateRegistry
         get() = savedStateRegistryController.savedStateRegistry
@@ -251,7 +250,8 @@
     override fun equals(other: Any?): Boolean {
         if (other == null || other !is NavBackStackEntry) return false
         return id == other.id && destination == other.destination &&
-            lifecycle == other.lifecycle && savedStateRegistry == other.savedStateRegistry &&
+            lifecycle == other.lifecycle &&
+            savedStateRegistry == other.savedStateRegistry &&
             (
                 immutableArgs == other.immutableArgs ||
                     immutableArgs?.keySet()
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
index fd67cc1..17995c5 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
@@ -26,6 +26,7 @@
 import androidx.collection.SparseArrayCompat
 import androidx.collection.valueIterator
 import androidx.core.content.res.use
+import androidx.core.net.toUri
 import androidx.navigation.common.R
 import java.util.regex.Pattern
 import kotlin.reflect.KClass
@@ -108,6 +109,35 @@
             }
             return mimeTypeMatchLevel - other.mimeTypeMatchLevel
         }
+
+        /**
+         * Returns true if all args from [DeepLinkMatch.matchingArgs] can be found within
+         * the [arguments].
+         *
+         * This returns true even if the [arguments] contain more args
+         * than [DeepLinkMatch.matchingArgs].
+         *
+         * @param [arguments] The arguments to match with the matchingArgs stored in this
+         * DeepLinkMatch.
+         */
+        public fun hasMatchingArgs(arguments: Bundle?): Boolean {
+            if (arguments == null || matchingArgs == null || matchingArgs.isEmpty) return false
+
+            matchingArgs.keySet().forEach { key ->
+                // the arguments must at least contain every argument stored in this deep link
+                if (!arguments.containsKey(key)) return false
+
+                val type = destination.arguments[key]?.type
+                val matchingArgValue = type?.get(matchingArgs, key)
+                val entryArgValue = type?.get(arguments, key)
+                if (matchingArgValue == null || entryArgValue == null ||
+                    matchingArgValue != entryArgValue
+                ) {
+                    return false
+                }
+            }
+            return true
+        }
     }
 
     /**
@@ -336,6 +366,23 @@
     }
 
     /**
+     * Determines if this NavDestination has a deep link of this route.
+     *
+     * @param [route] The route to match against this [NavDestination.route]
+     * @return The matching [DeepLinkMatch], or null if no match was found.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public fun matchDeepLink(route: String): DeepLinkMatch? {
+        val request = NavDeepLinkRequest.Builder.fromUri(createRoute(route).toUri()).build()
+        val matchingDeepLink = if (this is NavGraph) {
+            matchDeepLinkExcludingChildren(request)
+        } else {
+            matchDeepLink(request)
+        }
+        return matchingDeepLink
+    }
+
+    /**
      * Determines if this NavDestination has a deep link matching the given Uri.
      * @param navDeepLinkRequest The request to match against all deep links added in
      * [addDeepLink]
@@ -410,6 +457,38 @@
     }
 
     /**
+     * Returns true if the [NavBackStackEntry.destination] contains the route.
+     *
+     * The route may be either:
+     * 1. an exact route without arguments
+     * 2. a route containing arguments where no arguments are filled in
+     * 3. a route containing arguments where some or all arguments are filled in
+     *
+     * In the case of 3., it will only match if the entry arguments
+     * match exactly with the arguments that were filled in inside the route.
+     *
+     * @param [route] The route to match with the route of this destination
+     *
+     * @param [arguments] The [NavBackStackEntry.arguments] that was used to navigate
+     * to this destination
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public fun hasRoute(route: String, arguments: Bundle?): Boolean {
+        // this matches based on routePattern
+        if (this.route == route) return true
+
+        // if no match based on routePattern, this means route contains filled in args.
+        val matchingDeepLink = matchDeepLink(route)
+
+        // If matchingDeepLink is null or it has no matching args, the route does not contain
+        // filled in args, we would return false since it didn't pattern-match earlier either.
+        // Any args (partially or completely filled in) must exactly match between
+        // the route and entry's route
+        if (this != matchingDeepLink?.destination) return false
+        return matchingDeepLink.hasMatchingArgs(arguments)
+    }
+
+    /**
      * @return Whether this NavDestination supports outgoing actions
      * @see NavDestination.putAction
      * @suppress
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
index bf3d6d9..6ec0153 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
@@ -76,6 +76,14 @@
     }
 
     /**
+     * Only searches through deep links added directly to this graph. Does not recursively search
+     * through its children as [matchDeepLink] does.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public fun matchDeepLinkExcludingChildren(request: NavDeepLinkRequest): DeepLinkMatch? =
+        super.matchDeepLink(request)
+
+    /**
      * Adds a destination to this NavGraph. The destination must have an
      * [NavDestination.id] id} set.
      *
@@ -189,8 +197,13 @@
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public fun findNode(route: String, searchParents: Boolean): NavDestination? {
+        // first try matching with routePattern
         val id = createRoute(route).hashCode()
-        val destination = nodes[id]
+        val destination = nodes[id] ?: nodes.valueIterator().asSequence().firstOrNull {
+            // if not found with routePattern, try matching with route args
+            it.matchDeepLink(route) != null
+        }
+
         // Search the parent for the NavDestination if it is not a child of this navigation graph
         // and searchParents is true
         return destination
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..aa18277 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 &&
@@ -157,6 +174,29 @@
     }
 
     /**
+     * Informational callback indicating that the given [backStackEntry] has been
+     * affected by a [NavOptions.shouldLaunchSingleTop] operation. This also adds the given and
+     * previous entry to the [set of in progress transitions][transitionsInProgress].
+     * Added entries have their [Lifecycle] capped at [Lifecycle.State.STARTED] until an entry is
+     * passed into the [markTransitionComplete] callback, when they are allowed to go to
+     * [Lifecycle.State.RESUMED] while previous entries have their [Lifecycle] held at
+     * [Lifecycle.State.CREATED] until an entry is passed into the [markTransitionComplete]
+     * callback, when they are allowed to go to  [Lifecycle.State.DESTROYED] and have their state
+     * cleared.
+     *
+     * Replaces the topmost entry with same id with the new [backStackEntry][NavBackStackEntry]
+     *
+     * @param [backStackEntry] the [NavBackStackEntry] to replace the old Entry
+     * within the [backStack]
+     */
+    @CallSuper
+    public open fun onLaunchSingleTopWithTransition(backStackEntry: NavBackStackEntry) {
+        val oldEntry = backStack.value.last { it.id == backStackEntry.id }
+        _transitionsInProgress.value = _transitionsInProgress.value + oldEntry + backStackEntry
+        onLaunchSingleTop(backStackEntry)
+    }
+
+    /**
      * This removes the given [NavBackStackEntry] from the [set of the transitions in
      * progress][transitionsInProgress]. This should be called in conjunction with
      * [pushWithTransition] and [popWithTransition] as those call are responsible for adding
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-compose/build.gradle b/navigation/navigation-compose/build.gradle
index 8275048..4cdcea4 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -28,12 +28,12 @@
 
     implementation(libs.kotlinStdlib)
     implementation("androidx.compose.foundation:foundation-layout:1.0.1")
-    api("androidx.activity:activity-compose:1.6.1")
+    api(project(":activity:activity-compose"))
     api("androidx.compose.animation:animation:1.0.1")
     api("androidx.compose.runtime:runtime:1.0.1")
     api("androidx.compose.runtime:runtime-saveable:1.0.1")
     api("androidx.compose.ui:ui:1.0.1")
-    api(project(":lifecycle:lifecycle-viewmodel-compose"))
+    api("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
     // old version of common-java8 conflicts with newer version, because both have
     // DefaultLifecycleEventObserver.
     // Outside of androidx this is resolved via constraint added to lifecycle-common,
diff --git a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
index a04b504..4003262 100644
--- a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
+++ b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
@@ -49,6 +49,7 @@
 import androidx.compose.ui.test.performClick
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.testing.TestLifecycleOwner
@@ -785,13 +786,12 @@
 
     @Test
     fun testNestedNavHostOnBackPressed() {
-        val lifecycleOwner = TestLifecycleOwner()
         var innerLifecycleOwner = TestLifecycleOwner(Lifecycle.State.RESUMED)
         val onBackPressedDispatcher = OnBackPressedDispatcher()
-        val dispatcherOwner = object : OnBackPressedDispatcherOwner {
-            override fun getLifecycle() = lifecycleOwner.lifecycle
-            override fun getOnBackPressedDispatcher() = onBackPressedDispatcher
-        }
+        val dispatcherOwner =
+            object : OnBackPressedDispatcherOwner, LifecycleOwner by TestLifecycleOwner() {
+                override val onBackPressedDispatcher = onBackPressedDispatcher
+            }
         lateinit var navController: NavHostController
         lateinit var innerNavController: NavHostController
 
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-fragment/src/androidTest/java/androidx/navigation/fragment/DialogFragmentNavigatorTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/DialogFragmentNavigatorTest.kt
index 295a397..22cded3 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/DialogFragmentNavigatorTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/DialogFragmentNavigatorTest.kt
@@ -23,6 +23,9 @@
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentFactory
 import androidx.fragment.app.FragmentManager
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
 import androidx.navigation.NavBackStackEntry
 import androidx.navigation.NavController
 import androidx.navigation.Navigation
@@ -108,10 +111,10 @@
         assertThat(navigatorState.backStack.value)
             .containsExactly(entry)
         dialogNavigator.popBackStack(entry, false)
+        fragmentManager.executePendingTransactions()
         assertThat(navigatorState.backStack.value)
             .isEmpty()
 
-        fragmentManager.executePendingTransactions()
         assertWithMessage("Dialog should not be shown")
             .that(dialogFragment.dialog)
             .isNull()
@@ -162,13 +165,10 @@
             .that(dialogFragment.requireDialog().isShowing)
             .isTrue()
         dialogNavigator.popBackStack(entry, false)
+        fragmentManager.executePendingTransactions()
         assertWithMessage("DialogNavigator should pop dialog off the back stack")
             .that(navigatorState.backStack.value)
             .isEmpty()
-        assertWithMessage("Pop should dismiss the DialogFragment")
-            .that(dialogFragment.requireDialog().isShowing)
-            .isFalse()
-        fragmentManager.executePendingTransactions()
         assertWithMessage("Dismiss should remove the dialog")
             .that(dialogFragment.dialog)
             .isNull()
@@ -181,20 +181,61 @@
     @Test
     fun testDismiss() {
         lateinit var dialogFragment: DialogFragment
+        val entry = createBackStackEntry()
+        var matchCount = 0
+
         fragmentManager.fragmentFactory = object : FragmentFactory() {
             override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
                 return super.instantiate(classLoader, className).also { fragment ->
                     if (fragment is DialogFragment) {
                         dialogFragment = fragment
+                        // observer before navigator to verify down states
+                        dialogFragment.lifecycle.addObserver(object : LifecycleEventObserver {
+                            override fun onStateChanged(
+                                source: LifecycleOwner,
+                                event: Lifecycle.Event
+                            ) {
+                                if (event == Lifecycle.Event.ON_STOP) {
+                                    if (entry.lifecycle.currentState == Lifecycle.State.CREATED) {
+                                        matchCount++
+                                    }
+                                }
+                                if (event == Lifecycle.Event.ON_DESTROY) {
+                                    if (entry.lifecycle.currentState == Lifecycle.State.DESTROYED) {
+                                        matchCount++
+                                    }
+                                    dialogFragment.lifecycle.removeObserver(this)
+                                }
+                            }
+                        })
                     }
                 }
             }
         }
-        val entry = createBackStackEntry()
 
         dialogNavigator.navigate(listOf(entry), null, null)
         assertThat(navigatorState.backStack.value)
             .containsExactly(entry)
+
+        // observer after navigator to verify up states
+        dialogFragment.lifecycle.addObserver(object : LifecycleEventObserver {
+            override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+                if (event == Lifecycle.Event.ON_START) {
+                    if (entry.lifecycle.currentState == Lifecycle.State.STARTED) {
+                        matchCount++
+                    }
+                }
+                if (event == Lifecycle.Event.ON_RESUME) {
+                    if (entry.lifecycle.currentState == Lifecycle.State.RESUMED) {
+                        matchCount++
+                    }
+                }
+                if (event == Lifecycle.Event.ON_DESTROY) {
+                    dialogFragment.lifecycle.removeObserver(this)
+                }
+            }
+        })
+
         fragmentManager.executePendingTransactions()
         assertWithMessage("Dialog should be shown")
             .that(dialogFragment.requireDialog().isShowing)
@@ -202,6 +243,7 @@
 
         dialogFragment.dismiss()
         fragmentManager.executePendingTransactions()
+        assertThat(matchCount).isEqualTo(4)
         assertWithMessage("Dismiss should remove the dialog from the back stack")
             .that(navigatorState.backStack.value)
             .isEmpty()
diff --git a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavGraphViewModelLazyTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavGraphViewModelLazyTest.kt
index 07ce168..dbaab04 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavGraphViewModelLazyTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavGraphViewModelLazyTest.kt
@@ -30,7 +30,6 @@
 import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.SavedStateViewModelFactory
 import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.ViewModelStore
 import androidx.lifecycle.viewmodel.CreationExtras
 import androidx.lifecycle.viewmodel.MutableCreationExtras
@@ -279,15 +278,14 @@
         return View(activity)
     }
 
-    override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
-        return SavedStateViewModelFactory()
-    }
+    override val defaultViewModelProviderFactory = SavedStateViewModelFactory()
 
-    override fun getDefaultViewModelCreationExtras(): CreationExtras {
-        val extras = MutableCreationExtras(super.getDefaultViewModelCreationExtras())
-        extras[DEFAULT_ARGS_KEY] = bundleOf("test" to "value")
-        return extras
-    }
+    override val defaultViewModelCreationExtras: CreationExtras
+        get() {
+            val extras = MutableCreationExtras(super.defaultViewModelCreationExtras)
+            extras[DEFAULT_ARGS_KEY] = bundleOf("test" to "value")
+            return extras
+        }
 }
 
 class TestViewModel : ViewModel()
diff --git a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigator.kt b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigator.kt
index e8ab456..55defac 100644
--- a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigator.kt
+++ b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigator.kt
@@ -24,6 +24,7 @@
 import androidx.fragment.app.FragmentManager
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
 import androidx.navigation.FloatingWindow
 import androidx.navigation.NavBackStackEntry
 import androidx.navigation.NavDestination
@@ -45,34 +46,57 @@
     private val fragmentManager: FragmentManager
 ) : Navigator<Destination>() {
     private val restoredTagsAwaitingAttach = mutableSetOf<String>()
-    private val observer = LifecycleEventObserver { source, event ->
-        if (event == Lifecycle.Event.ON_CREATE) {
-            val dialogFragment = source as DialogFragment
-            val dialogOnBackStack = state.backStack.value.any { it.id == dialogFragment.tag }
-            if (!dialogOnBackStack) {
-                // If the Fragment is no longer on the back stack, it must have been
-                // been popped before it was actually attached to the FragmentManager
-                // (i.e., popped in the same frame as the navigate() call that added it). For
-                // that case, we need to dismiss the dialog to ensure the states stay in sync
-                dialogFragment.dismiss()
-            }
-        } else if (event == Lifecycle.Event.ON_STOP) {
-            val dialogFragment = source as DialogFragment
-            if (!dialogFragment.requireDialog().isShowing) {
-                val beforePopList = state.backStack.value
-                val poppedEntry = checkNotNull(beforePopList.lastOrNull {
-                    it.id == dialogFragment.tag
-                }) {
-                    "Dialog $dialogFragment has already been popped off of the Navigation " +
-                        "back stack"
+    private val observer = object : LifecycleEventObserver {
+        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+            when (event) {
+                Lifecycle.Event.ON_CREATE -> {
+                    val dialogFragment = source as DialogFragment
+                    val dialogOnBackStack = state.backStack.value.any {
+                        it.id == dialogFragment.tag
+                    }
+                    if (!dialogOnBackStack) {
+                        // If the Fragment is no longer on the back stack, it must have been
+                        // been popped before it was actually attached to the FragmentManager
+                        // (i.e., popped in the same frame as the navigate() call that added it).
+                        // For that case, we need to dismiss the dialog to ensure the states stay
+                        // in sync
+                        dialogFragment.dismiss()
+                    }
                 }
-                if (beforePopList.lastOrNull() != poppedEntry) {
-                    Log.i(
-                        TAG, "Dialog $dialogFragment was dismissed while it was not the top " +
-                            "of the back stack, popping all dialogs above this dismissed dialog"
-                    )
+                Lifecycle.Event.ON_RESUME -> {
+                    val dialogFragment = source as DialogFragment
+                    val entry = state.transitionsInProgress.value.lastOrNull { entry ->
+                        entry.id == dialogFragment.tag
+                    }
+                    entry?.let { state.markTransitionComplete(it) }
                 }
-                popBackStack(poppedEntry, false)
+                Lifecycle.Event.ON_STOP -> {
+                    val dialogFragment = source as DialogFragment
+                    if (!dialogFragment.requireDialog().isShowing) {
+                        val beforePopList = state.backStack.value
+                        val poppedEntry = beforePopList.lastOrNull {
+                            it.id == dialogFragment.tag
+                        }
+                        if (beforePopList.lastOrNull() != poppedEntry) {
+                            Log.i(
+                                TAG,
+                                "Dialog $dialogFragment was dismissed while it was not the " +
+                                    "top of the back stack, popping all dialogs above this " +
+                                    "dismissed dialog"
+                            )
+                        }
+                        poppedEntry?.let { state.popWithTransition(it, false) }
+                    }
+                }
+                Lifecycle.Event.ON_DESTROY -> {
+                    val dialogFragment = source as DialogFragment
+                    val entry = state.transitionsInProgress.value.lastOrNull { entry ->
+                        entry.id == dialogFragment.tag
+                    }
+                    entry?.let { state.markTransitionComplete(it) }
+                    dialogFragment.lifecycle.removeObserver(this)
+                }
+                else -> { /* added to exhaust when */ }
             }
         }
     }
@@ -106,11 +130,10 @@
         for (entry in poppedList.reversed()) {
             val existingFragment = fragmentManager.findFragmentByTag(entry.id)
             if (existingFragment != null) {
-                existingFragment.lifecycle.removeObserver(observer)
                 (existingFragment as DialogFragment).dismiss()
             }
         }
-        state.pop(popUpTo, savedState)
+        state.popWithTransition(popUpTo, savedState)
     }
 
     public override fun createDestination(): Destination {
@@ -136,7 +159,7 @@
     ) {
         val dialogFragment = createDialogFragment(entry)
         dialogFragment.show(fragmentManager, entry.id)
-        state.push(entry)
+        state.pushWithTransition(entry)
     }
 
     override fun onLaunchSingleTop(backStackEntry: NavBackStackEntry) {
@@ -159,7 +182,7 @@
 
         val newFragment = createDialogFragment(backStackEntry)
         newFragment.show(fragmentManager, backStackEntry.id)
-        state.onLaunchSingleTop(backStackEntry)
+        state.onLaunchSingleTopWithTransition(backStackEntry)
     }
 
     private fun createDialogFragment(entry: NavBackStackEntry): DialogFragment {
diff --git a/navigation/navigation-runtime/build.gradle b/navigation/navigation-runtime/build.gradle
index a7e476e..06b831b 100644
--- a/navigation/navigation-runtime/build.gradle
+++ b/navigation/navigation-runtime/build.gradle
@@ -46,6 +46,7 @@
     androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
     androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
     androidTestImplementation(libs.kotlinStdlib)
+    androidTestImplementation(libs.kotlinTest)
     androidTestImplementation(libs.multidex)
 
     lintPublish(project(':navigation:navigation-runtime-lint'))
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
index 33fa054..438ccc5 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
@@ -49,6 +49,7 @@
 import androidx.testutils.test
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import kotlin.test.assertFailsWith
 import org.hamcrest.CoreMatchers.allOf
 import org.hamcrest.CoreMatchers.not
 import org.hamcrest.Matchers
@@ -165,6 +166,23 @@
             }
         }
 
+    val nav_singleArg_graph =
+        createNavController().createGraph(route = "nav_root", startDestination = "start_test") {
+            test("start_test")
+            test("second_test/{arg}") {
+                argument("arg") { type = NavType.StringType }
+            }
+        }
+
+    val nav_multiArg_graph =
+        createNavController().createGraph(route = "nav_root", startDestination = "start_test") {
+            test("start_test")
+            test("second_test/{arg}/{arg2}") {
+                argument("arg") { type = NavType.StringType }
+                argument("arg2") { type = NavType.StringType }
+            }
+    }
+
     companion object {
         private const val UNKNOWN_DESTINATION_ID = -1
         private const val TEST_ARG = "test"
@@ -345,6 +363,624 @@
 
     @UiThreadTest
     @Test
+    fun testGetBackStackEntryWithExactRoute() {
+        val navController = createNavController()
+        navController.graph = nav_singleArg_graph
+
+        // first nav with arg filed in
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
+        navController.navigate(deepLink)
+
+        // second nav with arg filled in
+        val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
+        navController.navigate(deepLink2)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13", "second_test/18"]
+        assertThat(navigator.backStack.size).isEqualTo(3)
+
+        val entry1 = navController.getBackStackEntry("second_test/13")
+        assertThat(entry1).isEqualTo(navigator.backStack[1])
+
+        val entry2 = navController.getBackStackEntry("second_test/18")
+        assertThat(entry2).isEqualTo(navigator.backStack[2])
+    }
+
+    @UiThreadTest
+    @Test
+    fun testGetBackStackEntryWithExactRoute_onNavGraph() {
+        val navController = createNavController()
+        navController.graph =
+            navController.createGraph(route = "graph", startDestination = "start") {
+                test("start")
+                navigation(route = "graph2/{arg}", startDestination = "start2") {
+                    argument("arg") { type = NavType.StringType }
+                    test("start2")
+                    navigation(route = "graph3/{arg}", startDestination = "start3") {
+                        argument("arg") { type = NavType.StringType }
+                        test("start3")
+                    }
+                }
+            }
+
+        // fist nested graph
+        val deepLink = Uri.parse("android-app://androidx.navigation/graph2/13")
+        navController.navigate(deepLink)
+
+        // second nested graph
+        val deepLink2 = Uri.parse("android-app://androidx.navigation/graph3/18")
+        navController.navigate(deepLink2)
+
+        val navigator = navController.navigatorProvider.getNavigator(NavGraphNavigator::class.java)
+        // ["graph", "graph2/13", "graph3/18"]
+        assertThat(navigator.backStack.value.size).isEqualTo(3)
+
+        val entry1 = navController.getBackStackEntry("graph2/13")
+        assertThat(entry1).isEqualTo(navigator.backStack.value[1])
+
+        val entry2 = navController.getBackStackEntry("graph3/18")
+        assertThat(entry2).isEqualTo(navigator.backStack.value[2])
+    }
+
+    @UiThreadTest
+    @Test
+    fun testGetBackStackEntryWithExactRoute_multiArgs() {
+        val navController = createNavController()
+        navController.graph = nav_multiArg_graph
+
+        // navigate with both args filed in
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13/18"]
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        val entry1 = navController.getBackStackEntry("second_test/13/18")
+        assertThat(entry1).isEqualTo(navigator.backStack[1])
+    }
+
+    @UiThreadTest
+    @Test
+    fun testGetBackStackEntryWithPartialExactRoute() {
+        val navController = createNavController()
+        navController.graph = nav_multiArg_graph
+
+        // navigate with args partially filed in
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13/{arg2}"]
+        assertThat(navigator.backStack.size).isEqualTo(2)
+        // routes with partially filled in args will also match as long as the args
+        // are filled in the exact same way
+        val entry1 = navController.getBackStackEntry("second_test/13/{arg2}")
+        assertThat(entry1).isEqualTo(navigator.backStack[1])
+    }
+
+    @UiThreadTest
+    @Test
+    fun testGetBackStackEntryWithIncorrectExactRoute() {
+        val navController = createNavController()
+        navController.graph = nav_singleArg_graph
+
+        // navigate with arg filed in
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13"]
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        // route "second_test/18" should not match with any entries in backstack since we never
+        // navigated with args "18"
+        val route = "second_test/18"
+        val exception = assertFailsWith<IllegalArgumentException> {
+            navController.getBackStackEntry(route)
+        }
+        assertThat(exception.message).isEqualTo(
+            "No destination with route $route is on the NavController's " +
+                "back stack. The current destination is ${navController.currentDestination}"
+        )
+    }
+
+    @UiThreadTest
+    @Test
+    fun testGetBackStackEntryWithIncorrectExactRoute_multiArgs() {
+        val navController = createNavController()
+        navController.graph = nav_multiArg_graph
+
+        // navigate with args partially filed in
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13/18"]
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        // route "second_test/13/13" should not match with any entries in backstack
+        val route = "second_test/13/19"
+        val exception = assertFailsWith<IllegalArgumentException> {
+            navController.getBackStackEntry(route)
+        }
+        assertThat(exception.message).isEqualTo(
+            "No destination with route $route is on the NavController's " +
+                "back stack. The current destination is ${navController.currentDestination}"
+        )
+    }
+
+    @UiThreadTest
+    @Test
+    fun testGetBackStackEntryWithAdditionalPartialArgs() {
+        val navController = createNavController()
+        navController.graph = nav_multiArg_graph
+
+        // navigate with args partially filed in
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13/{arg2}"]
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        // route with additional arg "14" should not match
+        val route = "second_test/13/14"
+        val exception = assertFailsWith<IllegalArgumentException> {
+            navController.getBackStackEntry(route)
+        }
+        assertThat(exception.message).isEqualTo(
+            "No destination with route $route is on the NavController's " +
+                "back stack. The current destination is ${navController.currentDestination}"
+        )
+    }
+
+    @UiThreadTest
+    @Test
+    fun testGetBackStackEntryWithMissingPartialArgs() {
+        val navController = createNavController()
+        navController.graph = nav_multiArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13/18"]
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        // route missing arg "18" should not match
+        val route = "second_test/13/{arg2}"
+        val exception = assertFailsWith<IllegalArgumentException> {
+            navController.getBackStackEntry(route)
+        }
+        assertThat(exception.message).isEqualTo(
+            "No destination with route $route is on the NavController's " +
+                "back stack. The current destination is ${navController.currentDestination}"
+        )
+    }
+
+    @UiThreadTest
+    @Test
+    fun testPopBackStack() {
+        val navController = createNavController()
+        navController.graph = nav_singleArg_graph
+
+        // first nav with arg filed in
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
+        navController.navigate(deepLink)
+
+        // second nav with arg filled in
+        val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
+        navController.navigate(deepLink2)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13", "second_test/18"]
+        assertThat(navigator.backStack.size).isEqualTo(3)
+
+        val popped = navController.popBackStack("second_test/{arg}", true)
+        assertThat(popped).isTrue()
+        // only last entry with "second_test/{arg}" has been popped
+        assertThat(navigator.backStack.size).isEqualTo(2)
+    }
+
+    @UiThreadTest
+    @Test
+    fun testPopBackStackWithExactRoute() {
+        val navController = createNavController()
+        navController.graph = nav_singleArg_graph
+
+        // first nav with arg filed in
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
+        navController.navigate(deepLink)
+
+        // second nav with arg filled in
+        val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
+        navController.navigate(deepLink2)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13", "second_test/18"]
+        assertThat(navigator.backStack.size).isEqualTo(3)
+
+        val popped = navController.popBackStack("second_test/13", true)
+        assertThat(popped).isTrue()
+        assertThat(navigator.backStack.size).isEqualTo(1)
+    }
+
+    @UiThreadTest
+    @Test
+    fun testPopBackStackWithExactRoute_multiArgs() {
+        val navController = createNavController()
+        navController.graph = nav_multiArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13/18"]
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        val popped = navController.popBackStack("second_test/13/18", true)
+        assertThat(popped).isTrue()
+        assertThat(navigator.backStack.size).isEqualTo(1)
+    }
+
+    @UiThreadTest
+    @Test
+    fun testPopBackStackWithPartialExactRoute() {
+        val navController = createNavController()
+        navController.graph = nav_multiArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13/{arg2}"]
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        val popped = navController.popBackStack("second_test/13/{arg2}", true)
+        assertThat(popped).isTrue()
+        assertThat(navigator.backStack.size).isEqualTo(1)
+    }
+
+    @UiThreadTest
+    @Test
+    fun testPopBackStackWithIncorrectExactRoute() {
+        val navController = createNavController()
+        navController.graph = nav_multiArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13/18"]
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        val popped = navController.popBackStack("second_test/13/19", true)
+        assertThat(popped).isFalse()
+        assertThat(navigator.backStack.size).isEqualTo(2)
+    }
+    @UiThreadTest
+    @Test
+    fun testPopBackStackWithAdditionalPartialArgs() {
+        val navController = createNavController()
+        navController.graph = nav_multiArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13/{arg2}"]
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        val popped = navController.popBackStack("second_test/13/18", true)
+        assertThat(popped).isFalse()
+        assertThat(navigator.backStack.size).isEqualTo(2)
+    }
+
+    @UiThreadTest
+    @Test
+    fun testPopBackStackWithMissingPartialArgs() {
+        val navController = createNavController()
+        navController.graph = nav_multiArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13/18"]
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        val popped = navController.popBackStack("second_test/13/{arg2}", true)
+        assertThat(popped).isFalse()
+        assertThat(navigator.backStack.size).isEqualTo(2)
+    }
+    @UiThreadTest
+    @Test
+    fun testPopBackStackWithWrongArgOrder() {
+        val navController = createNavController()
+        navController.graph = nav_multiArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13/18"]
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        val popped = navController.popBackStack("second_test/18/13", true)
+        assertThat(popped).isFalse()
+        assertThat(navigator.backStack.size).isEqualTo(2)
+    }
+
+    @UiThreadTest
+    @Test
+    fun testFindDestinationWithRoute() {
+        val navController = createNavController()
+        navController.graph = nav_singleArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
+        navController.navigate(deepLink)
+
+        val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
+        navController.navigate(deepLink2)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        assertThat(navigator.backStack.size).isEqualTo(3)
+
+        // find with general route pattern
+        val foundDest = navController.findDestination("second_test/{arg}")
+        assertThat(foundDest).isNotNull()
+        // since we matching based on general route, should match both destinations
+        assertThat(foundDest).isEqualTo(navigator.backStack[1].destination)
+        assertThat(foundDest).isEqualTo(navigator.backStack[2].destination)
+    }
+
+    @UiThreadTest
+    @Test
+    fun testFindDestinationWithExactRoute() {
+        val navController = createNavController()
+        navController.graph = nav_singleArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
+        navController.navigate(deepLink)
+
+        val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
+        navController.navigate(deepLink2)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        assertThat(navigator.backStack.size).isEqualTo(3)
+
+        // find with route args filled in
+        val foundDest = navController.findDestination("second_test/13")
+        assertThat(foundDest).isNotNull()
+        // Even though NavDestinations doesn't store filled in args, an exact route should
+        // still be matched with the correct NavDestination with the same general pattern
+        assertThat(foundDest).isEqualTo(navigator.backStack[1].destination)
+        assertThat(foundDest).isEqualTo(navigator.backStack[2].destination)
+    }
+
+    @UiThreadTest
+    @Test
+    fun testClearBackStack() {
+        val navController = createNavController()
+        navController.graph = nav_singleArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
+        navController.navigate(deepLink)
+
+        val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
+        navController.navigate(deepLink2)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        assertThat(navigator.backStack.size).isEqualTo(3)
+
+        navController.popBackStack("second_test/{arg}", true, true)
+
+        val cleared = navController.clearBackStack("second_test/{arg}")
+        assertThat(cleared).isTrue()
+    }
+
+    @UiThreadTest
+    @Test
+    fun testClearBackStack_multiArgs() {
+        val navController = createNavController()
+        navController.graph = nav_multiArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/14")
+        navController.navigate(deepLink)
+
+        val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18/19")
+        navController.navigate(deepLink2)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        assertThat(navigator.backStack.size).isEqualTo(3)
+
+        navController.popBackStack("second_test/{arg}/{arg2}", true, true)
+        // multiple args
+        val cleared = navController.clearBackStack("second_test/{arg}/{arg2}")
+        assertThat(cleared).isTrue()
+    }
+
+    @UiThreadTest
+    @Test
+    fun testClearBackStackWithExactRoute() {
+        val navController = createNavController()
+        navController.graph = nav_singleArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
+        navController.navigate(deepLink)
+
+        val deepLink3 = Uri.parse("android-app://androidx.navigation/second_test/18")
+        navController.navigate(deepLink3)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        assertThat(navigator.backStack.size).isEqualTo(3)
+
+        navController.popBackStack("second_test/13", true, true)
+
+        val cleared = navController.clearBackStack("second_test/13")
+        assertThat(cleared).isTrue()
+        assertThat(navigator.backStack.size).isEqualTo(1)
+    }
+
+    @UiThreadTest
+    @Test
+    fun testClearBackStackWithExactRoute_multiArgs() {
+        val navController = createNavController()
+        navController.graph = nav_multiArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/14")
+        navController.navigate(deepLink)
+
+        val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18/19")
+        navController.navigate(deepLink2)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        assertThat(navigator.backStack.size).isEqualTo(3)
+
+        navController.popBackStack("second_test/13/14", true, true)
+        // multiple filled-in args
+        val cleared = navController.clearBackStack("second_test/13/14")
+        assertThat(cleared).isTrue()
+        assertThat(navigator.backStack.size).isEqualTo(1)
+    }
+
+    @UiThreadTest
+    @Test
+    fun testClearBackStackWithDifferentRouteForPopping() {
+        val navController = createNavController()
+        navController.graph = nav_singleArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
+        navController.navigate(deepLink)
+
+        val deepLink3 = Uri.parse("android-app://androidx.navigation/second_test/18")
+        navController.navigate(deepLink3)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        assertThat(navigator.backStack.size).isEqualTo(3)
+
+        navController.popBackStack("second_test/13", true, true)
+        assertThat(navigator.backStack.size).isEqualTo(1)
+
+        // "second_test/18" was popped but it wasn't the route passed in for popping
+        val cleared = navController.clearBackStack("second_test/18")
+        assertThat(cleared).isFalse()
+        assertThat(navigator.backStack.size).isEqualTo(1)
+    }
+
+    @UiThreadTest
+    @Test
+    fun testClearBackStackWithUnpoppedRoute() {
+        val navController = createNavController()
+        navController.graph = nav_singleArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        navController.popBackStack("second_test/13", true, true)
+        assertThat(navigator.backStack.size).isEqualTo(1)
+
+        // start_test was never popped
+        val cleared = navController.clearBackStack("start_test")
+        assertThat(cleared).isFalse()
+        assertThat(navigator.backStack.size).isEqualTo(1)
+    }
+
+    @UiThreadTest
+    @Test
+    fun testClearBackStackWithPartialExactRoute() {
+        val navController = createNavController()
+        navController.graph = nav_multiArg_graph
+
+        // navigate with partial args filled in
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        navController.popBackStack("second_test/13/{arg2}", true, true)
+        val cleared = navController.clearBackStack("second_test/13/{arg2}")
+        assertThat(cleared).isTrue()
+    }
+
+    @UiThreadTest
+    @Test
+    fun testClearBackStackWithAdditionalPartialArgs() {
+        val navController = createNavController()
+        navController.graph = nav_multiArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        navController.popBackStack("second_test/13/{arg2}", true, true)
+        // additional arg2 = 18
+        val cleared = navController.clearBackStack("second_test/13/18")
+        assertThat(cleared).isFalse()
+    }
+
+    @UiThreadTest
+    @Test
+    fun testClearBackStackWithMissingPartialArgs() {
+        val navController = createNavController()
+        navController.graph = nav_multiArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        navController.popBackStack("second_test/13/18", true, true)
+        // missing arg2 = 18
+        val cleared = navController.clearBackStack("second_test/13/{arg2}")
+        assertThat(cleared).isFalse()
+    }
+
+    @UiThreadTest
+    @Test
+    fun testClearBackStackWithWrongArgOrder() {
+        val navController = createNavController()
+        navController.graph = nav_multiArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        navController.popBackStack("second_test/13/18", true, true)
+        // args in wrong order
+        val cleared = navController.clearBackStack("second_test/18/13")
+        assertThat(cleared).isFalse()
+    }
+
+    @UiThreadTest
+    @Test
+    fun testClearBackStackWithNoSavedState() {
+        val navController = createNavController()
+        navController.graph = nav_multiArg_graph
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        assertThat(navigator.backStack.size).isEqualTo(2)
+        // popped without saving state
+        navController.popBackStack("second_test/13/18", true, false)
+        val cleared = navController.clearBackStack("second_test/13/18")
+        assertThat(cleared).isFalse()
+    }
+
+    @UiThreadTest
+    @Test
     fun testNavigateViaDeepLinkDefaultArgs() {
         val navController = createNavController()
         navController.graph = nav_simple_route_graph
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index 4a1ba0f..a36a849 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -489,7 +489,8 @@
     /**
      * Attempts to pop the controller's back stack back to a specific destination.
      *
-     * @param route The topmost destination to retain
+     * @param route The topmost destination to retain. May contain filled in arguments as long as
+     * it is exact match with route used to navigate.
      * @param inclusive Whether the given destination should also be popped.
      * @param saveState Whether the back stack and the state of all destinations between the
      * current destination and the [route] should be saved for later
@@ -506,7 +507,12 @@
         route: String,
         inclusive: Boolean,
         saveState: Boolean = false
-    ): Boolean = popBackStack(createRoute(route).hashCode(), inclusive, saveState)
+    ): Boolean {
+        val popped = popBackStackInternal(route, inclusive, saveState)
+        // Only return true if the pop succeeded and we've dispatched
+        // the change to a new destination
+        return popped && dispatchOnDestinationChanged()
+    }
 
     /**
      * Attempts to pop the controller's back stack back to a specific destination. This does
@@ -561,6 +567,64 @@
             )
             return false
         }
+        return executePopOperations(popOperations, foundDestination, inclusive, saveState)
+    }
+
+    /**
+     * Attempts to pop the controller's back stack back to a specific destination. This does
+     * **not** handle calling [dispatchOnDestinationChanged]
+     *
+     * @param route The topmost destination with this route to retain
+     * @param inclusive Whether the given destination should also be popped.
+     * @param saveState Whether the back stack and the state of all destinations between the
+     * current destination and the destination with [route] should be saved for later to be
+     * restored via [NavOptions.Builder.setRestoreState] or the `restoreState` attribute using
+     * the [NavDestination.id] of the destination with this route (note: this matching ID
+     * is true whether [inclusive] is true or false).
+     *
+     * @return true if the stack was popped at least once, false otherwise
+     */
+    private fun popBackStackInternal(
+        route: String,
+        inclusive: Boolean,
+        saveState: Boolean,
+    ): Boolean {
+        if (backQueue.isEmpty()) {
+            // Nothing to pop if the back stack is empty
+            return false
+        }
+
+        val popOperations = mutableListOf<Navigator<*>>()
+        val foundDestination = backQueue.lastOrNull { entry ->
+            val hasRoute = entry.destination.hasRoute(route, entry.arguments)
+            if (inclusive || !hasRoute) {
+                val navigator = _navigatorProvider.getNavigator<Navigator<*>>(
+                    entry.destination.navigatorName
+                )
+                popOperations.add(navigator)
+            }
+            hasRoute
+        }?.destination
+
+        if (foundDestination == null) {
+            // We were passed a route that doesn't exist on our back stack.
+            // Better to ignore the popBackStack than accidentally popping the entire stack
+            Log.i(
+                TAG,
+                "Ignoring popBackStack to route $route as it was not found " +
+                    "on the current back stack"
+            )
+            return false
+        }
+        return executePopOperations(popOperations, foundDestination, inclusive, saveState)
+    }
+
+    private fun executePopOperations(
+        popOperations: List<Navigator<*>>,
+        foundDestination: NavDestination,
+        inclusive: Boolean,
+        saveState: Boolean,
+    ): Boolean {
         var popped = false
         val savedState = ArrayDeque<NavBackStackEntryState>()
         for (navigator in popOperations) {
@@ -699,14 +763,18 @@
      * via [popBackStack] when using a `saveState` value of `true`.
      *
      * @param route The route of the destination previously used with [popBackStack] with a
-     * `saveState` value of `true`
+     * `saveState` value of `true`. May contain filled in arguments as long as
+     * it is exact match with route used with [popBackStack].
      *
      * @return true if the saved state of the stack associated with [route] was cleared.
      */
     @MainThread
-    public fun clearBackStack(
-        route: String
-    ): Boolean = clearBackStack(createRoute(route).hashCode())
+    public fun clearBackStack(route: String): Boolean {
+        val cleared = clearBackStackInternal(route)
+        // Only return true if the clear succeeded and we've dispatched
+        // the change to a new destination
+        return cleared && dispatchOnDestinationChanged()
+    }
 
     /**
      * Clears any saved state associated with [destinationId] that was previously saved
@@ -737,6 +805,18 @@
         return restored && popBackStackInternal(destinationId, inclusive = true, saveState = false)
     }
 
+    @MainThread
+    private fun clearBackStackInternal(route: String): Boolean {
+        navigatorState.values.forEach { state ->
+            state.isNavigating = true
+        }
+        val restored = restoreStateInternal(route)
+        navigatorState.values.forEach { state ->
+            state.isNavigating = false
+        }
+        return restored && popBackStackInternal(route, inclusive = true, saveState = false)
+    }
+
     /**
      * Attempts to navigate up in the navigation hierarchy. Suitable for when the
      * user presses the "Up" button marked with a left (or start)-facing arrow in the upper left
@@ -1430,16 +1510,17 @@
 
     /** @suppress */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public fun findDestination(destinationRoute: String): NavDestination? {
+    public fun findDestination(route: String): NavDestination? {
         if (_graph == null) {
             return null
         }
-        if (_graph!!.route == destinationRoute) {
+        // if not matched by routePattern, try matching with route args
+        if (_graph!!.route == route || _graph!!.matchDeepLink(route) != null) {
             return _graph
         }
         val currentNode = backQueue.lastOrNull()?.destination ?: _graph!!
         val currentGraph = if (currentNode is NavGraph) currentNode else currentNode.parent!!
-        return currentGraph.findNode(destinationRoute)
+        return currentGraph.findNode(route)
     }
 
     /**
@@ -1802,6 +1883,47 @@
         val backStackState = backStackStates.remove(backStackId)
         // Now restore the back stack from its saved state
         val entries = instantiateBackStack(backStackState)
+        return executeRestoreState(entries, args, navOptions, navigatorExtras)
+    }
+
+    private fun restoreStateInternal(route: String): Boolean {
+        var id = createRoute(route).hashCode()
+        // try to match based on routePattern
+        return if (backStackMap.containsKey(id)) {
+            restoreStateInternal(id, null, null, null)
+        } else {
+            // if it didn't match, it means the route contains filled in arguments and we need
+            // to find the destination that matches this route's general pattern
+            val matchingDestination = findDestination(route)
+            check(matchingDestination != null) {
+                "Restore State failed: route $route cannot be found from the current " +
+                    "destination $currentDestination"
+            }
+
+            id = matchingDestination.id
+            val backStackId = backStackMap[id]
+            // Clear out the state we're going to restore so that it isn't restored a second time
+            backStackMap.values.removeAll { it == backStackId }
+            val backStackState = backStackStates.remove(backStackId)
+
+            val matchingDeepLink = matchingDestination.matchDeepLink(route)
+            // check if the topmost NavBackStackEntryState contains the arguments in this
+            // matchingDeepLink. If not, we didn't find the correct stack.
+            val isCorrectStack = matchingDeepLink!!.hasMatchingArgs(
+                backStackState?.firstOrNull()?.args
+            )
+            if (!isCorrectStack) return false
+            val entries = instantiateBackStack(backStackState)
+            executeRestoreState(entries, null, null, null)
+        }
+    }
+
+    private fun executeRestoreState(
+        entries: List<NavBackStackEntry>,
+        args: Bundle?,
+        navOptions: NavOptions?,
+        navigatorExtras: Navigator.Extras?
+    ): Boolean {
         // Split up the entries by Navigator so we can restore them as an atomic operation
         val entriesGroupedByNavigator = mutableListOf<MutableList<NavBackStackEntry>>()
         entries.filterNot { entry ->
@@ -2276,12 +2398,13 @@
      * [its parent][NavDestination.parent] or grandparent navigation graphs as these
      * destinations are guaranteed to be on the back stack.
      *
-     * @param route route of a destination that exists on the back stack
+     * @param route route of a destination that exists on the back stack. May contain filled in
+     * arguments as long as it is exact match with route used to navigate.
      * @throws IllegalArgumentException if the destination is not on the back stack
      */
     public fun getBackStackEntry(route: String): NavBackStackEntry {
         val lastFromBackStack: NavBackStackEntry? = backQueue.lastOrNull { entry ->
-            entry.destination.route == route
+            entry.destination.hasRoute(route, entry.arguments)
         }
         requireNotNull(lastFromBackStack) {
             "No destination with route $route is on the NavController's back stack. The " +
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-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt b/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt
index a046004..c3e20c9 100644
--- a/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt
+++ b/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt
@@ -142,11 +142,22 @@
             suffix = """
                 android {
                     namespace 'androidx.navigation.testapp'
+                    compileOptions {
+                        sourceCompatibility = JavaVersion.VERSION_1_8
+                        targetCompatibility = JavaVersion.VERSION_1_8
+                    }
                 }
                 dependencies {
                     implementation "${projectSetup.props.kotlinStblib}"
                     implementation "${projectSetup.props.navigationRuntime}"
                 }
+                tasks.withType(
+                    org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+                ).configureEach {
+                    kotlinOptions {
+                        jvmTarget = "1.8"
+                    }
+                }
             """.trimIndent()
         )
     }
diff --git a/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/KotlinPluginTest.kt b/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/KotlinPluginTest.kt
index a325ede..489c3d3 100644
--- a/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/KotlinPluginTest.kt
+++ b/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/KotlinPluginTest.kt
@@ -53,11 +53,22 @@
                             applicationIdSuffix ".foo"
                         }
                     }
+                    compileOptions {
+                        sourceCompatibility = JavaVersion.VERSION_1_8
+                        targetCompatibility = JavaVersion.VERSION_1_8
+                    }
                 }
                 dependencies {
                     implementation "${projectSetup.props.kotlinStblib}"
                     implementation "${projectSetup.props.navigationRuntime}"
                 }
+                tasks.withType(
+                    org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+                ).configureEach {
+                    kotlinOptions {
+                        jvmTarget = "1.8"
+                    }
+                }
             """.trimIndent()
         )
         runGradle("assembleDebug").assertSuccessfulTask("assembleDebug")
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/paging/paging-common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt b/paging/paging-common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt
index b88a6bbd..556f8ba 100644
--- a/paging/paging-common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt
@@ -34,8 +34,8 @@
 import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.yield
 
@@ -338,8 +338,8 @@
      *
      * @sample androidx.paging.samples.loadStateFlowSample
      */
-    public val loadStateFlow: Flow<CombinedLoadStates> =
-        combinedLoadStatesCollection.stateFlow.filterNotNull()
+    public val loadStateFlow: StateFlow<CombinedLoadStates?> =
+        combinedLoadStatesCollection.stateFlow
 
     private val _onPagesUpdatedFlow: MutableSharedFlow<Unit> = MutableSharedFlow(
         replay = 0,
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt
index 0985d16..4740924 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt
@@ -41,6 +41,7 @@
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.consumeAsFlow
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.flowOf
@@ -217,7 +218,7 @@
         pageEventCh.trySend(
             localLoadStateUpdate(refreshLocal = Loading)
         )
-        assertThat(differ.loadStateFlow.first()).isEqualTo(
+        assertThat(differ.nonNullLoadStateFlow.first()).isEqualTo(
             localLoadStatesOf(refreshLocal = Loading)
         )
 
@@ -999,7 +1000,7 @@
         var combinedLoadStates: CombinedLoadStates? = null
         var itemCount = -1
         val loadStateJob = launch {
-            differ.loadStateFlow.collect {
+            differ.nonNullLoadStateFlow.collect {
                 combinedLoadStates = it
                 itemCount = differ.size
             }
@@ -1049,7 +1050,7 @@
         // Should not immediately emit without a real value to a new collector.
         val combinedLoadStates = mutableListOf<CombinedLoadStates>()
         val loadStateJob = launch {
-            differ.loadStateFlow.collect {
+            differ.nonNullLoadStateFlow.collect {
                 combinedLoadStates.add(it)
             }
         }
@@ -1074,7 +1075,7 @@
         // Should emit real values to new collectors immediately
         val newCombinedLoadStates = mutableListOf<CombinedLoadStates>()
         val newLoadStateJob = launch {
-            differ.loadStateFlow.collect {
+            differ.nonNullLoadStateFlow.collect {
                 newCombinedLoadStates.add(it)
             }
         }
@@ -1096,7 +1097,7 @@
         // Should not immediately emit without a real value to a new collector.
         val combinedLoadStates = mutableListOf<CombinedLoadStates>()
         val loadStateJob = launch {
-            differ.loadStateFlow.collect {
+            differ.nonNullLoadStateFlow.collect {
                 combinedLoadStates.add(it)
             }
         }
@@ -1144,7 +1145,7 @@
         // New observers should receive the previous state.
         val newCombinedLoadStates = mutableListOf<CombinedLoadStates>()
         val newLoadStateJob = launch {
-            differ.loadStateFlow.collect {
+            differ.nonNullLoadStateFlow.collect {
                 newCombinedLoadStates.add(it)
             }
         }
@@ -1173,7 +1174,7 @@
         // Should not immediately emit without a real value to a new collector.
         val combinedLoadStates = mutableListOf<CombinedLoadStates>()
         val loadStateJob = launch {
-            differ.loadStateFlow.collect {
+            differ.nonNullLoadStateFlow.collect {
                 combinedLoadStates.add(it)
             }
         }
@@ -1221,7 +1222,7 @@
         // New observers should receive the previous state.
         val newCombinedLoadStates = mutableListOf<CombinedLoadStates>()
         val newLoadStateJob = launch {
-            differ.loadStateFlow.collect {
+            differ.nonNullLoadStateFlow.collect {
                 newCombinedLoadStates.add(it)
             }
         }
@@ -1249,7 +1250,7 @@
 
         val combinedLoadStates = mutableListOf<CombinedLoadStates>()
         backgroundScope.launch {
-            differ.loadStateFlow.collect {
+            differ.nonNullLoadStateFlow.collect {
                 combinedLoadStates.add(it)
             }
         }
@@ -2194,6 +2195,8 @@
 
     private val _localLoadStates = mutableListOf<CombinedLoadStates>()
 
+    val nonNullLoadStateFlow = loadStateFlow.filterNotNull()
+
     fun newCombinedLoadStates(): List<CombinedLoadStates?> {
         val newCombinedLoadStates = _localLoadStates.toList()
         _localLoadStates.clear()
@@ -2202,7 +2205,7 @@
 
     fun collectLoadStates(): Job {
         return coroutineScope.launch {
-            loadStateFlow.collect { combinedLoadStates ->
+            nonNullLoadStateFlow.collect { combinedLoadStates ->
                 _localLoadStates.add(combinedLoadStates)
             }
         }
diff --git a/paging/paging-compose/src/androidTest/java/androidx/paging/compose/LazyPagingItemsTest.kt b/paging/paging-compose/src/androidTest/java/androidx/paging/compose/LazyPagingItemsTest.kt
index 49e029e..9095b67 100644
--- a/paging/paging-compose/src/androidTest/java/androidx/paging/compose/LazyPagingItemsTest.kt
+++ b/paging/paging-compose/src/androidTest/java/androidx/paging/compose/LazyPagingItemsTest.kt
@@ -99,6 +99,37 @@
     }
 
     @Test
+    fun lazyPagingLoadStateAfterRefresh() {
+        val pager = createPager()
+        val loadStates: MutableList<CombinedLoadStates> = mutableListOf()
+
+        lateinit var lazyPagingItems: LazyPagingItems<Int>
+        rule.setContent {
+            lazyPagingItems = pager.flow.collectAsLazyPagingItems()
+            loadStates.add(lazyPagingItems.loadState)
+        }
+
+        // we only want loadStates after manual refresh
+        loadStates.clear()
+        lazyPagingItems.refresh()
+        rule.waitForIdle()
+
+        assertThat(loadStates).isNotEmpty()
+        val expected = CombinedLoadStates(
+            refresh = LoadState.Loading,
+            prepend = LoadState.NotLoading(false),
+            append = LoadState.NotLoading(false),
+            source = LoadStates(
+                LoadState.Loading,
+                LoadState.NotLoading(false),
+                LoadState.NotLoading(false)
+            ),
+            mediator = null
+        )
+        assertThat(loadStates.first()).isEqualTo(expected)
+    }
+
+    @Test
     fun lazyPagingColumnShowsItems() {
         val pager = createPager()
         rule.setContent {
diff --git a/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt b/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt
index 30d786d..7ba0385 100644
--- a/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt
+++ b/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt
@@ -45,6 +45,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.withContext
 
 /**
@@ -179,17 +180,18 @@
      * A [CombinedLoadStates] object which represents the current loading state.
      */
     public var loadState: CombinedLoadStates by mutableStateOf(
-        CombinedLoadStates(
-            refresh = InitialLoadStates.refresh,
-            prepend = InitialLoadStates.prepend,
-            append = InitialLoadStates.append,
-            source = InitialLoadStates
+        pagingDataDiffer.loadStateFlow.value
+            ?: CombinedLoadStates(
+                refresh = InitialLoadStates.refresh,
+                prepend = InitialLoadStates.prepend,
+                append = InitialLoadStates.append,
+                source = InitialLoadStates
+            )
         )
-    )
         private set
 
     internal suspend fun collectLoadState() {
-        pagingDataDiffer.loadStateFlow.collect {
+        pagingDataDiffer.loadStateFlow.filterNotNull().collect {
             loadState = it
         }
     }
diff --git a/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt b/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt
index 287aeef..81bce0e 100644
--- a/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt
+++ b/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt
@@ -29,6 +29,7 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
@@ -340,7 +341,7 @@
      *
      * @sample androidx.paging.samples.loadStateFlowSample
      */
-    val loadStateFlow: Flow<CombinedLoadStates> = differBase.loadStateFlow
+    val loadStateFlow: Flow<CombinedLoadStates> = differBase.loadStateFlow.filterNotNull()
 
     /**
      * A hot [Flow] that emits after the pages presented to the UI are updated, even if the
diff --git a/paging/paging-testing/api/current.txt b/paging/paging-testing/api/current.txt
index bcde83a..6f25f9e 100644
--- a/paging/paging-testing/api/current.txt
+++ b/paging/paging-testing/api/current.txt
@@ -7,8 +7,10 @@
 
   public final class SnapshotLoader<Value> {
     method public suspend Object? appendScrollWhile(kotlin.jvm.functions.Function1<Value,java.lang.Boolean> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
+    method public suspend Object? flingTo(int index, kotlin.coroutines.Continuation<kotlin.Unit>);
     method public suspend Object? prependScrollWhile(kotlin.jvm.functions.Function1<Value,java.lang.Boolean> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
     method public suspend Object? refresh(kotlin.coroutines.Continuation<kotlin.Unit>);
+    method public suspend Object? scrollTo(int index, kotlin.coroutines.Continuation<kotlin.Unit>);
   }
 
   public final class StaticListPagingSourceFactoryKt {
diff --git a/paging/paging-testing/api/public_plus_experimental_current.txt b/paging/paging-testing/api/public_plus_experimental_current.txt
index bcde83a..6f25f9e 100644
--- a/paging/paging-testing/api/public_plus_experimental_current.txt
+++ b/paging/paging-testing/api/public_plus_experimental_current.txt
@@ -7,8 +7,10 @@
 
   public final class SnapshotLoader<Value> {
     method public suspend Object? appendScrollWhile(kotlin.jvm.functions.Function1<Value,java.lang.Boolean> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
+    method public suspend Object? flingTo(int index, kotlin.coroutines.Continuation<kotlin.Unit>);
     method public suspend Object? prependScrollWhile(kotlin.jvm.functions.Function1<Value,java.lang.Boolean> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
     method public suspend Object? refresh(kotlin.coroutines.Continuation<kotlin.Unit>);
+    method public suspend Object? scrollTo(int index, kotlin.coroutines.Continuation<kotlin.Unit>);
   }
 
   public final class StaticListPagingSourceFactoryKt {
diff --git a/paging/paging-testing/api/restricted_current.txt b/paging/paging-testing/api/restricted_current.txt
index bcde83a..6f25f9e 100644
--- a/paging/paging-testing/api/restricted_current.txt
+++ b/paging/paging-testing/api/restricted_current.txt
@@ -7,8 +7,10 @@
 
   public final class SnapshotLoader<Value> {
     method public suspend Object? appendScrollWhile(kotlin.jvm.functions.Function1<Value,java.lang.Boolean> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
+    method public suspend Object? flingTo(int index, kotlin.coroutines.Continuation<kotlin.Unit>);
     method public suspend Object? prependScrollWhile(kotlin.jvm.functions.Function1<Value,java.lang.Boolean> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
     method public suspend Object? refresh(kotlin.coroutines.Continuation<kotlin.Unit>);
+    method public suspend Object? scrollTo(int index, kotlin.coroutines.Continuation<kotlin.Unit>);
   }
 
   public final class StaticListPagingSourceFactoryKt {
diff --git a/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt b/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt
index 0f96639..8b8daf7 100644
--- a/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt
+++ b/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt
@@ -16,6 +16,7 @@
 
 package androidx.paging.testing
 
+import androidx.paging.CombinedLoadStates
 import androidx.paging.DifferCallback
 import androidx.paging.ItemSnapshotList
 import androidx.paging.LoadState
@@ -31,7 +32,9 @@
 import kotlinx.coroutines.cancelAndJoin
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.debounce
 import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.firstOrNull
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
@@ -108,7 +111,7 @@
      *
      * The collection job is cancelled automatically after [loadOperations] completes.
       */
-    val job = coroutineScope.launch {
+    val collectPagingData = coroutineScope.launch {
         this@asSnapshot.collectLatest {
             incrementGeneration(loader)
             differ.collectFrom(it)
@@ -129,7 +132,7 @@
         loader.loadOperations()
         differ.awaitNotLoading()
 
-        job.cancelAndJoin()
+        collectPagingData.cancelAndJoin()
 
         differ.snapshot().items
     }
@@ -139,13 +142,24 @@
  * Awaits until both source and mediator states are NotLoading. We do not care about the state of
  * endOfPaginationReached. Source and mediator states need to be checked individually because
  * the aggregated LoadStates can reflect `NotLoading` when source states are `Loading`.
+ *
+ * We debounce(1ms) to prevent returning too early if this collected a `NotLoading` from the
+ * previous load. Without a way to determine whether the `NotLoading` it collected was from
+ * a previous operation or current operation, we debounce 1ms to allow collection on a potential
+ * incoming `Loading` state.
  */
+@OptIn(kotlinx.coroutines.FlowPreview::class)
 internal suspend fun <Value : Any> PagingDataDiffer<Value>.awaitNotLoading() {
-    loadStateFlow.filter {
-        it.source.isIdle() && it.mediator?.isIdle() ?: true
+    loadStateFlow.filterNotNull().debounce(1).filter {
+        it.isIdle()
     }.firstOrNull()
 }
 
+private fun CombinedLoadStates?.isIdle(): Boolean {
+    if (this == null) return false
+    return source.isIdle() && mediator?.isIdle() ?: true
+}
+
 private fun LoadStates.isIdle(): Boolean {
     return refresh is LoadState.NotLoading && append is LoadState.NotLoading &&
         prepend is LoadState.NotLoading
diff --git a/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt b/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt
index e4f73ac..fbb8cde 100644
--- a/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt
+++ b/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt
@@ -17,15 +17,16 @@
 package androidx.paging.testing
 
 import androidx.paging.DifferCallback
-import androidx.paging.PagingData
-import androidx.paging.PagingDataDiffer
-import androidx.paging.PagingSource
 import androidx.paging.LoadType.APPEND
 import androidx.paging.LoadType.PREPEND
 import androidx.paging.PagingConfig
+import androidx.paging.PagingData
+import androidx.paging.PagingDataDiffer
+import androidx.paging.PagingSource
 import androidx.paging.testing.LoaderCallback.CallbackType.ON_INSERTED
 import java.util.concurrent.atomic.AtomicInteger
 import java.util.concurrent.atomic.AtomicReference
+import kotlin.math.abs
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.first
@@ -123,6 +124,202 @@
     }
 
     /**
+     * Imitates scrolling from current index to the target index. It waits for an item to be loaded
+     * in before triggering load on next item. Returns all available data that has been scrolled
+     * through.
+     *
+     * The scroll direction (prepend or append) is dependent on current index and target index. In
+     * general, scrolling to a smaller index triggers [PREPEND] while scrolling to a larger
+     * index triggers [APPEND].
+     *
+     * When [PagingConfig.enablePlaceholders] is false, the [index] is scoped within currently
+     * loaded items. For example, in a list of items(0-20) with currently loaded items(10-15),
+     * index[0] = item(10), index[4] = item(15).
+     *
+     * Supports [index] beyond currently loaded items when [PagingConfig.enablePlaceholders]
+     * is false:
+     * 1. For prepends, it supports negative indices for as long as there are still available
+     * data to load from. For example, take a list of items(0-20), pageSize = 1, with currently
+     * loaded items(10-15). With index[0] = item(10), a `scrollTo(-4)` will scroll to item(6) and
+     * update index[0] = item(6).
+     * 2. For appends, it supports indices >= loadedDataSize. For example, take a list of
+     * items(0-20), pageSize = 1, with currently loaded items(10-15). With
+     * index[4] = item(15), a `scrollTo(7)` will scroll to item(18) and update
+     * index[7] = item(18).
+     * Note that both examples does not account for prefetches.
+
+     * The [index] accounts for separators/headers/footers where each one of those consumes one
+     * scrolled index.
+     *
+     * For both append/prepend, this function stops loading prior to fulfilling requested scroll
+     * distance if there are no more data to load from.
+     *
+     * @param [index] The target index to scroll to
+     *
+     * @see [flingTo] for faking a scroll that continues scrolling without waiting for items to
+     * be loaded in. Supports jumping.
+     */
+    public suspend fun scrollTo(index: Int): @JvmSuppressWildcards Unit {
+        differ.awaitNotLoading()
+        appendOrPrependScrollTo(index)
+        differ.awaitNotLoading()
+    }
+
+    /**
+     * Scrolls from current index to targeted [index].
+     *
+     * Internally this method scrolls until it fulfills requested index
+     * differential (Math.abs(requested index - current index)) rather than scrolling
+     * to the exact requested index. This is because item indices can shift depending on scroll
+     * direction and placeholders. Therefore we try to fulfill the expected amount of scrolling
+     * rather than the actual requested index.
+     */
+    private suspend fun appendOrPrependScrollTo(index: Int) {
+        val startIndex = generations.value.lastAccessedIndex.get()
+        val loadType = if (startIndex > index) LoadType.PREPEND else LoadType.APPEND
+        val scrollCount = abs(startIndex - index)
+        awaitScroll(loadType, scrollCount)
+    }
+
+    /**
+     * Imitates flinging from current index to the target index. It will continue scrolling
+     * even as data is being loaded in. Returns all available data that has been scrolled
+     * through.
+     *
+     * The scroll direction (prepend or append) is dependent on current index and target index. In
+     * general, scrolling to a smaller index triggers [PREPEND] while scrolling to a larger
+     * index triggers [APPEND].
+     *
+     * This function will scroll into placeholders. This means jumping is supported when
+     * [PagingConfig.enablePlaceholders] is true and the amount of placeholders traversed
+     * has reached [PagingConfig.jumpThreshold]. Jumping is disabled when
+     * [PagingConfig.enablePlaceholders] is false.
+     *
+     * When [PagingConfig.enablePlaceholders] is false, the [index] is scoped within currently
+     * loaded items. For example, in a list of items(0-20) with currently loaded items(10-15),
+     * index[0] = item(10), index[4] = item(15).
+     *
+     * Supports [index] beyond currently loaded items when [PagingConfig.enablePlaceholders]
+     * is false:
+     * 1. For prepends, it supports negative indices for as long as there are still available
+     * data to load from. For example, take a list of items(0-20), pageSize = 1, with currently
+     * loaded items(10-15). With index[0] = item(10), a `scrollTo(-4)` will scroll to item(6) and
+     * update index[0] = item(6).
+     * 2. For appends, it supports indices >= loadedDataSize. For example, take a list of
+     * items(0-20), pageSize = 1, with currently loaded items(10-15). With
+     * index[4] = item(15), a `scrollTo(7)` will scroll to item(18) and update
+     * index[7] = item(18).
+     * Note that both examples does not account for prefetches.
+
+     * The [index] accounts for separators/headers/footers where each one of those consumes one
+     * scrolled index.
+     *
+     * For both append/prepend, this function stops loading prior to fulfilling requested scroll
+     * distance if there are no more data to load from.
+     *
+     * @param [index] The target index to scroll to
+     *
+     * @see [scrollTo] for faking scrolls that awaits for placeholders to load before continuing
+     * to scroll.
+     */
+    public suspend fun flingTo(index: Int): @JvmSuppressWildcards Unit {
+        differ.awaitNotLoading()
+        appendOrPrependFlingTo(index)
+        differ.awaitNotLoading()
+    }
+
+    /**
+     * We start scrolling from startIndex +/- 1 so we don't accidentally trigger
+     * a prefetch on the opposite direction.
+     */
+    private suspend fun appendOrPrependFlingTo(index: Int) {
+        val startIndex = generations.value.lastAccessedIndex.get()
+        val loadType = if (startIndex > index) LoadType.PREPEND else LoadType.APPEND
+
+        when (loadType) {
+            LoadType.PREPEND -> prependFlingTo(startIndex, index)
+            LoadType.APPEND -> appendFlingTo(startIndex, index)
+        }
+    }
+
+    /**
+     * Prepend flings to target index.
+     *
+     * If target index is negative, from index[0] onwards it will normal scroll until it fulfills
+     * remaining distance.
+     */
+    private suspend fun prependFlingTo(startIndex: Int, index: Int) {
+        var lastAccessedIndex = startIndex
+        val endIndex = maxOf(0, index)
+        // first, fast scroll to index or zero
+        for (i in startIndex - 1 downTo endIndex) {
+            differ[i]
+            lastAccessedIndex = i
+        }
+        setLastAccessedIndex(lastAccessedIndex)
+        // for negative indices, we delegate remainder of scrolling (distance below zero)
+        // to the awaiting version.
+        if (index < 0) {
+            val scrollCount = abs(index)
+            flingToOutOfBounds(LoadType.PREPEND, lastAccessedIndex, scrollCount)
+        }
+    }
+
+    /**
+     * Append flings to target index.
+     *
+     * If target index is beyond [PagingDataDiffer.size] - 1, from index(differ.size) and onwards,
+     * it will normal scroll until it fulfills remaining distance.
+     */
+    private suspend fun appendFlingTo(startIndex: Int, index: Int) {
+        var lastAccessedIndex = startIndex
+        val endIndex = minOf(index, differ.size - 1)
+        // first, fast scroll to endIndex
+        for (i in startIndex + 1..endIndex) {
+            differ[i]
+            lastAccessedIndex = i
+        }
+        setLastAccessedIndex(lastAccessedIndex)
+        // for indices at or beyond differ.size, we delegate remainder of scrolling (distance
+        // beyond differ.size) to the awaiting version.
+        if (index >= differ.size) {
+            val scrollCount = index - lastAccessedIndex
+            flingToOutOfBounds(LoadType.APPEND, lastAccessedIndex, scrollCount)
+        }
+    }
+
+    /**
+     * Delegated work from [flingTo] that is responsible for scrolling to indices that is
+     * beyond the range of [0 to differ.size-1].
+     *
+     * When [PagingConfig.enablePlaceholders] is true, this function is no-op because
+     * there is no more data to load from.
+     *
+     * When [PagingConfig.enablePlaceholders] is false, its delegated work to [awaitScroll]
+     * essentially loops (trigger next page --> await for next page) until
+     * it fulfills remaining (out of bounds) requested scroll distance.
+     */
+    private suspend fun flingToOutOfBounds(
+        loadType: LoadType,
+        lastAccessedIndex: Int,
+        scrollCount: Int
+    ) {
+        // Wait for the page triggered by differ[lastAccessedIndex] to load in. This gives us the
+        // offsetIndex for next differ.get() because the current lastAccessedIndex is already the
+        // boundary index, such that differ[lastAccessedIndex +/- 1] will throw IndexOutOfBounds.
+        val (_, offsetIndex) = awaitLoad(lastAccessedIndex)
+        setLastAccessedIndex(offsetIndex)
+        // starts loading from the offsetIndex and scrolls the remaining requested distance
+        awaitScroll(loadType, scrollCount)
+    }
+
+    private suspend fun awaitScroll(loadType: LoadType, scrollCount: Int) {
+        repeat(scrollCount) {
+            awaitNextItem(loadType) ?: return
+        }
+    }
+
+    /**
      * Triggers load for next item, awaits for it to be loaded and returns the loaded item.
      *
      * It calculates the next load index based on loadType and this generation's
@@ -168,6 +365,7 @@
     @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
     private suspend fun awaitLoad(index: Int): Pair<Value, Int> {
         differ[index]
+        differ.awaitNotLoading()
         var offsetIndex = index
 
         // awaits for the item to be loaded
diff --git a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
index b65ce1a..8150e6f 100644
--- a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
+++ b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
@@ -18,14 +18,19 @@
 
 import androidx.paging.Pager
 import androidx.paging.PagingConfig
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
 import androidx.paging.cachedIn
+import androidx.paging.insertSeparators
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
@@ -33,14 +38,28 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import org.junit.runners.Parameterized
 
 @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
-@RunWith(JUnit4::class)
-class PagerFlowSnapshotTest {
+@RunWith(Parameterized::class)
+class PagerFlowSnapshotTest(
+    private val loadDelay: Long
+) {
+    companion object {
+        @Parameterized.Parameters
+        @JvmStatic
+        fun withLoadDelay(): Array<Long> {
+            return arrayOf(0, 10000)
+        }
+    }
 
     private val testScope = TestScope(UnconfinedTestDispatcher())
 
+    private fun createFactory(dataFlow: Flow<List<Int>>) = WrappedPagingSourceFactory(
+        dataFlow.asPagingSourceFactory(testScope.backgroundScope),
+        loadDelay
+    )
+
     @Before
     fun init() {
         Dispatchers.setMain(UnconfinedTestDispatcher())
@@ -49,14 +68,9 @@
     @Test
     fun initialRefresh() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
-
-        val pager = Pager(
-            config = CONFIG,
-            pagingSourceFactory = factory
-        )
+        val pager = createPager(dataFlow)
         testScope.runTest {
-            val snapshot = pager.flow.asSnapshot(this) {}
+            val snapshot = pager.asSnapshot(this) {}
             // first page + prefetched page
             assertThat(snapshot).containsExactlyElementsIn(
                 listOf(0, 1, 2, 3, 4, 5, 6, 7)
@@ -65,15 +79,28 @@
     }
 
     @Test
+    fun initialRefresh_withSeparators() {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPager(dataFlow).map { pagingData ->
+            pagingData.insertSeparators { before: Int?, after: Int? ->
+                if (before != null && after != null) "sep" else null
+            }
+        }
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {}
+            // loads 8[initial 5 + prefetch 3] items total, including separators
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, "sep", 1, "sep", 2, "sep", 3, "sep", 4)
+            )
+        }
+    }
+
+    @Test
     fun initialRefresh_withoutPrefetch() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG_NO_PREFETCH,
-            pagingSourceFactory = factory
-        )
+        val pager = createPagerNoPrefetch(dataFlow)
         testScope.runTest {
-            val snapshot = pager.flow.asSnapshot(this) {}
+            val snapshot = pager.asSnapshot(this) {}
 
             assertThat(snapshot).containsExactlyElementsIn(
                 listOf(0, 1, 2, 3, 4)
@@ -84,14 +111,9 @@
     @Test
     fun initialRefresh_withInitialKey() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG,
-            initialKey = 10,
-            pagingSourceFactory = factory
-        )
+        val pager = createPager(dataFlow, 10)
         testScope.runTest {
-            val snapshot = pager.flow.asSnapshot(this) {}
+            val snapshot = pager.asSnapshot(this) {}
 
             assertThat(snapshot).containsExactlyElementsIn(
                 listOf(7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
@@ -102,14 +124,9 @@
     @Test
     fun initialRefresh_withInitialKey_withoutPrefetch() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG_NO_PREFETCH,
-            initialKey = 10,
-            pagingSourceFactory = factory
-        )
+        val pager = createPagerNoPrefetch(dataFlow, 10)
         testScope.runTest {
-            val snapshot = pager.flow.asSnapshot(this) {}
+            val snapshot = pager.asSnapshot(this) {}
 
             assertThat(snapshot).containsExactlyElementsIn(
                 listOf(10, 11, 12, 13, 14)
@@ -120,13 +137,9 @@
     @Test
     fun emptyInitialRefresh() {
         val dataFlow = emptyFlow<List<Int>>()
-        val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
-        val pager = Pager(
-            config = CONFIG,
-            pagingSourceFactory = factory
-        )
+        val pager = createPager(dataFlow)
         testScope.runTest {
-            val snapshot = pager.flow.asSnapshot(this) {}
+            val snapshot = pager.asSnapshot(this) {}
 
             assertThat(snapshot).containsExactlyElementsIn(
                 emptyList<Int>()
@@ -137,12 +150,7 @@
     @Test
     fun manualRefresh() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
-        val pager = Pager(
-            config = CONFIG_NO_PREFETCH,
-            pagingSourceFactory = factory
-        ).flow.cachedIn(testScope.backgroundScope)
-
+        val pager = createPagerNoPrefetch(dataFlow).cachedIn(testScope.backgroundScope)
         testScope.runTest {
             val snapshot = pager.asSnapshot(this) {
                 refresh()
@@ -156,13 +164,9 @@
     @Test
     fun manualEmptyRefresh() {
         val dataFlow = emptyFlow<List<Int>>()
-        val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
-        val pager = Pager(
-            config = CONFIG_NO_PREFETCH,
-            pagingSourceFactory = factory
-        )
+        val pager = createPagerNoPrefetch(dataFlow)
         testScope.runTest {
-            val snapshot = pager.flow.asSnapshot(this) {
+            val snapshot = pager.asSnapshot(this) {
                 refresh()
             }
             assertThat(snapshot).containsExactlyElementsIn(
@@ -174,13 +178,9 @@
     @Test
     fun appendWhile() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
-        val pager = Pager(
-            config = CONFIG,
-            pagingSourceFactory = factory,
-        )
+        val pager = createPager(dataFlow)
         testScope.runTest {
-            val snapshot = pager.flow.asSnapshot(this) {
+            val snapshot = pager.asSnapshot(this) {
                 appendScrollWhile { item: Int ->
                     item < 14
                 }
@@ -196,13 +196,53 @@
     }
 
     @Test
+    fun appendWhile_withDrops() {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPagerWithDrops(dataFlow)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                appendScrollWhile { item ->
+                    item < 14
+                }
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                // dropped [0-10]
+                listOf(11, 12, 13, 14, 15, 16, 17, 18, 19)
+            )
+        }
+    }
+
+    @Test
+    fun appendWhile_withSeparators() {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPager(dataFlow).map { pagingData ->
+            pagingData.insertSeparators { before: Int?, _ ->
+                if (before == 9 || before == 12) "sep" else null
+            }
+        }
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                appendScrollWhile { item ->
+                    item !is Int || item < 14
+                }
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                // initial load [0-4]
+                // prefetched [5-7]
+                // appended [8-16]
+                // prefetched [17-19]
+                listOf(
+                    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "sep", 10, 11, 12, "sep", 13, 14, 15,
+                    16, 17, 18, 19
+                )
+            )
+        }
+    }
+
+    @Test
     fun appendWhile_withoutPrefetch() {
         val dataFlow = flowOf(List(50) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG_NO_PREFETCH,
-            pagingSourceFactory = factory,
-        ).flow
+        val pager = createPagerNoPrefetch(dataFlow)
         testScope.runTest {
             val snapshot = pager.asSnapshot(this) {
                 appendScrollWhile { item: Int ->
@@ -220,11 +260,7 @@
     @Test
     fun appendWhile_withoutPlaceholders() {
         val dataFlow = flowOf(List(50) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG_NO_PLACEHOLDERS,
-            pagingSourceFactory = factory,
-        ).flow
+        val pager = createPagerNoPlaceholders(dataFlow)
         testScope.runTest {
             val snapshot = pager.asSnapshot(this) {
                 appendScrollWhile { item: Int ->
@@ -244,14 +280,9 @@
     @Test
     fun prependWhile() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
-        val pager = Pager(
-            config = CONFIG,
-            initialKey = 20,
-            pagingSourceFactory = factory,
-        )
+        val pager = createPager(dataFlow, 20)
         testScope.runTest {
-            val snapshot = pager.flow.asSnapshot(this) {
+            val snapshot = pager.asSnapshot(this) {
                 prependScrollWhile { item: Int ->
                     item > 14
                 }
@@ -267,14 +298,53 @@
     }
 
     @Test
+    fun prependWhile_withDrops() {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPagerWithDrops(dataFlow, 20)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                prependScrollWhile { item: Int ->
+                    item > 14
+                }
+            }
+            // dropped [20-27]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(11, 12, 13, 14, 15, 16, 17, 18, 19)
+            )
+        }
+    }
+
+    @Test
+    fun prependWhile_withSeparators() {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = createPager(dataFlow, 20).map { pagingData ->
+            pagingData.insertSeparators { before: Int?, _ ->
+                if (before == 14 || before == 18) "sep" else null
+            }
+        }
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                prependScrollWhile { item ->
+                    item !is Int || item > 14
+                }
+            }
+            // initial load [20-24]
+            // prefetched [17-19], no append prefetch because separator fulfilled prefetchDistance
+            // prepended [14-16]
+            // prefetched [11-13]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    11, 12, 13, 14, "sep", 15, 16, 17, 18, "sep", 19, 20, 21, 22, 23,
+                    24, 25, 26, 27
+                )
+            )
+        }
+    }
+
+    @Test
     fun prependWhile_withoutPrefetch() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG_NO_PREFETCH,
-            initialKey = 20,
-            pagingSourceFactory = factory,
-        ).flow
+        val pager = createPagerNoPrefetch(dataFlow, 20)
         testScope.runTest {
             val snapshot = pager.asSnapshot(this) {
                 prependScrollWhile { item: Int ->
@@ -292,12 +362,7 @@
     @Test
     fun prependWhile_withoutPlaceholders() {
         val dataFlow = flowOf(List(50) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG_NO_PLACEHOLDERS,
-            initialKey = 30,
-            pagingSourceFactory = factory,
-        ).flow
+        val pager = createPagerNoPlaceholders(dataFlow, 30)
         testScope.runTest {
             val snapshot = pager.asSnapshot(this) {
                 prependScrollWhile { item: Int ->
@@ -319,14 +384,9 @@
     @Test
     fun appendWhile_withInitialKey() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG,
-            initialKey = 10,
-            pagingSourceFactory = factory,
-        )
+        val pager = createPager(dataFlow, 10)
         testScope.runTest {
-            val snapshot = pager.flow.asSnapshot(this) {
+            val snapshot = pager.asSnapshot(this) {
                 appendScrollWhile { item: Int ->
                     item < 18
                 }
@@ -344,14 +404,9 @@
     @Test
     fun appendWhile_withInitialKey_withoutPlaceholders() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG_NO_PLACEHOLDERS,
-            initialKey = 10,
-            pagingSourceFactory = factory,
-        )
+        val pager = createPagerNoPlaceholders(dataFlow, 10)
         testScope.runTest {
-            val snapshot = pager.flow.asSnapshot(this) {
+            val snapshot = pager.asSnapshot(this) {
                 appendScrollWhile { item: Int ->
                     item != 19
                 }
@@ -369,14 +424,9 @@
     @Test
     fun appendWhile_withInitialKey_withoutPrefetch() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG_NO_PREFETCH,
-            initialKey = 10,
-            pagingSourceFactory = factory,
-        )
+        val pager = createPagerNoPrefetch(dataFlow, 10)
         testScope.runTest {
-            val snapshot = pager.flow.asSnapshot(this) {
+            val snapshot = pager.asSnapshot(this) {
                 appendScrollWhile { item: Int ->
                     item < 18
                 }
@@ -392,13 +442,9 @@
     @Test
     fun prependWhile_withoutInitialKey() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG,
-            pagingSourceFactory = factory,
-        )
+        val pager = createPager(dataFlow)
         testScope.runTest {
-            val snapshot = pager.flow.asSnapshot(this) {
+            val snapshot = pager.asSnapshot(this) {
                 prependScrollWhile { item: Int ->
                     item > -3
                 }
@@ -414,11 +460,7 @@
     @Test
     fun consecutiveAppendWhile() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG,
-            pagingSourceFactory = factory,
-        ).flow.cachedIn(testScope.backgroundScope)
+        val pager = createPager(dataFlow).cachedIn(testScope.backgroundScope)
         testScope.runTest {
             val snapshot1 = pager.asSnapshot(this) {
                 appendScrollWhile { item: Int ->
@@ -447,12 +489,7 @@
     @Test
     fun consecutivePrependWhile() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG_NO_PREFETCH,
-            initialKey = 20,
-            pagingSourceFactory = factory,
-        ).flow.cachedIn(testScope.backgroundScope)
+        val pager = createPagerNoPrefetch(dataFlow, 20).cachedIn(testScope.backgroundScope)
         testScope.runTest {
             val snapshot1 = pager.asSnapshot(this) {
                 prependScrollWhile { item: Int ->
@@ -476,13 +513,9 @@
     @Test
     fun appendWhile_outOfBounds_returnsCurrentlyLoadedItems() {
         val dataFlow = flowOf(List(10) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG,
-            pagingSourceFactory = factory,
-        )
+        val pager = createPager(dataFlow)
         testScope.runTest {
-            val snapshot = pager.flow.asSnapshot(this) {
+            val snapshot = pager.asSnapshot(this) {
                 appendScrollWhile { item: Int ->
                     // condition scrolls till end of data since we only have 10 items
                     item < 18
@@ -499,14 +532,9 @@
     @Test
     fun prependWhile_outOfBounds_returnsCurrentlyLoadedItems() {
         val dataFlow = flowOf(List(20) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG,
-            initialKey = 10,
-            pagingSourceFactory = factory,
-        )
+        val pager = createPager(dataFlow, 10)
         testScope.runTest {
-            val snapshot = pager.flow.asSnapshot(this) {
+            val snapshot = pager.asSnapshot(this) {
                 prependScrollWhile { item: Int ->
                     // condition scrolls till index = 0
                     item > -3
@@ -525,11 +553,7 @@
     @Test
     fun refreshAndAppendWhile() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG,
-            pagingSourceFactory = factory,
-        ).flow.cachedIn(testScope.backgroundScope)
+        val pager = createPager(dataFlow).cachedIn(testScope.backgroundScope)
         testScope.runTest {
             val snapshot = pager.asSnapshot(this) {
                 refresh() // triggers second gen
@@ -546,12 +570,7 @@
     @Test
     fun refreshAndPrependWhile() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG,
-            initialKey = 20,
-            pagingSourceFactory = factory,
-        ).flow.cachedIn(testScope.backgroundScope)
+        val pager = createPager(dataFlow, 20).cachedIn(testScope.backgroundScope)
         testScope.runTest {
             val snapshot = pager.asSnapshot(this) {
                 // this prependScrollWhile does not cause paging to load more items
@@ -578,11 +597,7 @@
     @Test
     fun appendWhileAndRefresh() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG,
-            pagingSourceFactory = factory,
-        ).flow.cachedIn(testScope.backgroundScope)
+        val pager = createPager(dataFlow).cachedIn(testScope.backgroundScope)
         testScope.runTest {
             val snapshot = pager.asSnapshot(this) {
                 appendScrollWhile { item: Int ->
@@ -602,12 +617,7 @@
     @Test
     fun prependWhileAndRefresh() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
-        val pager = Pager(
-            config = CONFIG,
-            initialKey = 15,
-            pagingSourceFactory = factory,
-        ).flow.cachedIn(testScope.backgroundScope)
+        val pager = createPager(dataFlow, 15).cachedIn(testScope.backgroundScope)
         testScope.runTest {
             val snapshot = pager.asSnapshot(this) {
                 prependScrollWhile { item: Int ->
@@ -625,12 +635,45 @@
     }
 
     @Test
-    fun consecutiveGenerations_fromSharedFlow() {
+    fun consecutiveGenerations_fromFlow() {
+        val loadDelay = 500 + loadDelay
+        // wait for 500 + loadDelay between each emission
+        val dataFlow = flow {
+            emit(emptyList())
+            delay(loadDelay)
+
+            emit(List(30) { it })
+            delay(loadDelay)
+
+            emit(List(30) { it + 30 })
+        }
+        val pager = createPagerNoPrefetch(dataFlow).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot1 = pager.asSnapshot(this) { }
+            assertThat(snapshot1).containsExactlyElementsIn(
+                emptyList<Int>()
+            )
+
+            delay(500)
+
+            val snapshot2 = pager.asSnapshot(this) { }
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4)
+            )
+
+            delay(500)
+
+            val snapshot3 = pager.asSnapshot(this) { }
+            assertThat(snapshot3).containsExactlyElementsIn(
+                listOf(30, 31, 32, 33, 34)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveGenerations_fromSharedFlow_emitAfterRefresh() {
         val dataFlow = MutableSharedFlow<List<Int>>()
-        val pager = Pager(
-            config = CONFIG_NO_PREFETCH,
-            pagingSourceFactory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
-        ).flow.cachedIn(testScope.backgroundScope)
+        val pager = createPagerNoPrefetch(dataFlow).cachedIn(testScope.backgroundScope)
         testScope.runTest {
             val snapshot1 = pager.asSnapshot(this) { }
             assertThat(snapshot1).containsExactlyElementsIn(
@@ -654,37 +697,24 @@
     }
 
     @Test
-    fun consecutiveGenerations_fromFlow() {
-        val dataFlow = flow {
-            // first gen
-            emit(emptyList())
-            delay(500)
-            // second gen
-            emit(List(30) { it })
-            delay(500)
-            // third gen
-            emit(List(30) { it + 30 })
-        }
-        val pager = Pager(
-            config = CONFIG_NO_PREFETCH,
-            pagingSourceFactory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
-        ).flow.cachedIn(testScope.backgroundScope)
+    fun consecutiveGenerations_fromSharedFlow_emitBeforeRefresh() {
+        val dataFlow = MutableSharedFlow<List<Int>>()
+        val pager = createPagerNoPrefetch(dataFlow).cachedIn(testScope.backgroundScope)
         testScope.runTest {
+            dataFlow.emit(emptyList())
             val snapshot1 = pager.asSnapshot(this) { }
             assertThat(snapshot1).containsExactlyElementsIn(
                 emptyList<Int>()
             )
 
-            val snapshot2 = pager.asSnapshot(this) {
-                delay(500)
-            }
+            dataFlow.emit(List(30) { it })
+            val snapshot2 = pager.asSnapshot(this) { }
             assertThat(snapshot2).containsExactlyElementsIn(
                 listOf(0, 1, 2, 3, 4)
             )
 
-            val snapshot3 = pager.asSnapshot(this) {
-                delay(500)
-            }
+            dataFlow.emit(List(30) { it + 30 })
+            val snapshot3 = pager.asSnapshot(this) { }
             assertThat(snapshot3).containsExactlyElementsIn(
                 listOf(30, 31, 32, 33, 34)
             )
@@ -692,29 +722,57 @@
     }
 
     @Test
-    fun consecutiveGenerations_withInitialKey_nullRefreshKey() {
+    fun consecutiveGenerations_nonNullRefreshKey() {
+        val loadDelay = 500 + loadDelay
         val dataFlow = flow {
             // first gen
             emit(List(20) { it })
-            delay(500)
+            // wait for refresh + append
+            delay(loadDelay * 2)
             // second gen
             emit(List(20) { it })
         }
-        val pager = Pager(
-            config = CONFIG_NO_PREFETCH,
-            initialKey = 10,
-            pagingSourceFactory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
-        ).flow.cachedIn(testScope.backgroundScope)
+        val pager = createPagerNoPrefetch(dataFlow).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot1 = pager.asSnapshot(this) {
+                // we scroll to register a non-null anchorPos
+                appendScrollWhile { item: Int ->
+                    item < 5
+                }
+            }
+            assertThat(snapshot1).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7)
+            )
+
+            delay(1000)
+            val snapshot2 = pager.asSnapshot(this) { }
+            // anchorPos = 5, refreshKey = 3
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(3, 4, 5, 6, 7)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveGenerations_withInitialKey_nullRefreshKey() {
+        val loadDelay = 500 + loadDelay
+        // wait for 500 + loadDelay between each emission
+        val dataFlow = flow {
+            // first gen
+            emit(List(20) { it })
+            delay(loadDelay)
+            // second gen
+            emit(List(20) { it })
+        }
+        val pager = createPagerNoPrefetch(dataFlow, 10).cachedIn(testScope.backgroundScope)
         testScope.runTest {
             val snapshot1 = pager.asSnapshot(this) { }
             assertThat(snapshot1).containsExactlyElementsIn(
                 listOf(10, 11, 12, 13, 14)
             )
 
-            val snapshot2 = pager.asSnapshot(this) {
-                // wait for second gen to complete
-                delay(500)
-            }
+            delay(500)
+            val snapshot2 = pager.asSnapshot(this) { }
             assertThat(snapshot2).containsExactlyElementsIn(
                 listOf(0, 1, 2, 3, 4)
             )
@@ -723,18 +781,16 @@
 
     @Test
     fun consecutiveGenerations_withInitialKey_nonNullRefreshKey() {
+        val loadDelay = 500 + loadDelay
         val dataFlow = flow {
             // first gen
             emit(List(20) { it })
-            delay(500)
+            // wait for refresh + append
+            delay(loadDelay * 2)
             // second gen
             emit(List(20) { it })
         }
-        val pager = Pager(
-            config = CONFIG_NO_PREFETCH,
-            initialKey = 10,
-            pagingSourceFactory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
-        ).flow.cachedIn(testScope.backgroundScope)
+        val pager = createPagerNoPrefetch(dataFlow, 10).cachedIn(testScope.backgroundScope)
         testScope.runTest {
             val snapshot1 = pager.asSnapshot(this) {
                 // we scroll to register a non-null anchorPos
@@ -746,9 +802,8 @@
                 listOf(10, 11, 12, 13, 14, 15, 16, 17)
             )
 
-            val snapshot2 = pager.asSnapshot(this) {
-                delay(500)
-            }
+            delay(1000)
+            val snapshot2 = pager.asSnapshot(this) { }
             // anchorPos = 15, refreshKey = 13
             assertThat(snapshot2).containsExactlyElementsIn(
                 listOf(13, 14, 15, 16, 17)
@@ -756,21 +811,1406 @@
         }
     }
 
-    val CONFIG = PagingConfig(
-        pageSize = 3,
-        initialLoadSize = 5,
-    )
+    @Test
+    fun prependScroll() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(42)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [41-46]
+            // prefetched [38-40]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
 
-    val CONFIG_NO_PLACEHOLDERS = PagingConfig(
-        pageSize = 3,
-        initialLoadSize = 5,
-        enablePlaceholders = false,
-        prefetchDistance = 3
-    )
+    @Test
+    fun prependScroll_withDrops() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithDrops(dataFlow, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(42)
+            }
+            // dropped [47-57]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(38, 39, 40, 41, 42, 43, 44, 45, 46)
+            )
+        }
+    }
 
-    val CONFIG_NO_PREFETCH = PagingConfig(
-        pageSize = 3,
-        initialLoadSize = 5,
-        prefetchDistance = 0
-    )
+    @Test
+    fun prependScroll_withSeparators() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, 50).map { pagingData ->
+            pagingData.insertSeparators { before: Int?, _ ->
+                if (before == 42 || before == 49) "sep" else null
+            }
+        }
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(42)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [41-46]
+            // prefetched [38-40]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    38, 39, 40, 41, 42, "sep", 43, 44, 45, 46, 47, 48, 49, "sep", 50, 51, 52,
+                    53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun consecutivePrependScroll() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(42)
+                scrollTo(38)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [38-46]
+            // prefetched [35-37]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
+                    51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun consecutivePrependScroll_multiSnapshots() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(42)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [41-46]
+            // prefetched [38-40]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57
+                )
+            )
+
+            val snapshot2 = pager.asSnapshot(this) {
+                scrollTo(38)
+            }
+            // prefetched [35-37]
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(
+                    35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
+                    51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_indexOutOfBounds() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, 5).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(-5)
+            }
+            // ensure index is capped when no more data to load
+            // initial load [5-9]
+            // prefetched [2-4], [10-12]
+            // scrollTo prepended [0-1]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_accessPageBoundary() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, 50).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(47)
+            }
+            // ensure that SnapshotLoader waited for last prefetch before returning
+            // initial load [50-54]
+            // prefetched [47-49], [55-57] - expect only one extra page to be prefetched after this
+            // scrollTo prepended [44-46]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_withoutPrefetch() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPrefetch(dataFlow, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(42)
+            }
+            // initial load [50-54]
+            // prepended [41-49]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54)
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_withoutPlaceholders() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, 50).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                // Without placeholders, first loaded page always starts at index[0]
+                scrollTo(0)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [44-46]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_withoutPlaceholders_indexOutOfBounds() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, 50).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(-5)
+            }
+            // ensure it honors negative indices starting with index[0] = item[47]
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // scrollTo prepended [41-46]
+            // prefetched [38-40]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_withoutPlaceholders_indexOutOfBoundsIsCapped() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, 5).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(-5)
+            }
+            // ensure index is capped when no more data to load
+            // initial load [5-9]
+            // prefetched [2-4], [10-12]
+            // scrollTo prepended [0-1]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
+            )
+        }
+    }
+
+    @Test
+    fun consecutivePrependScroll_withoutPlaceholders() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, 50).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                // Without placeholders, first loaded page always starts at index[0]
+                scrollTo(-1)
+                // Without placeholders, first loaded page always starts at index[0]
+                scrollTo(-5)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // first scrollTo prepended [41-46]
+            // index[0] is now anchored to [41]
+            // second scrollTo prepended [35-40]
+            // prefetched [32-34]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
+                    50, 51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun consecutivePrependScroll_withoutPlaceholders_multiSnapshot() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, 50).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                // Without placeholders, first loaded page always starts at index[0]
+                scrollTo(-1)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // scrollTo prepended [44-46]
+            // prefetched [41-43]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
+            )
+
+            val snapshot2 = pager.asSnapshot(this) {
+                // Without placeholders, first loaded page always starts at index[0]
+                scrollTo(-5)
+            }
+            // scrollTo prepended [35-40]
+            // prefetched [32-34]
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(
+                    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
+                    50, 51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_withoutPlaceholders_noPrefetchTriggered() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = Pager(
+            config = PagingConfig(
+                pageSize = 4,
+                initialLoadSize = 8,
+                enablePlaceholders = false,
+                // a small prefetchDistance to prevent prefetch until we scroll to boundary
+                prefetchDistance = 1
+            ),
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                // Without placeholders, first loaded page always starts at index[0]
+                scrollTo(0)
+            }
+            // initial load [50-57]
+            // no prefetch after initial load because it didn't hit prefetch distance
+            // scrollTo prepended [46-49]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
+            )
+        }
+    }
+
+    @Test
+    fun appendScroll() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(12)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+        }
+    }
+
+    @Test
+    fun appendScroll_withDrops() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithDrops(dataFlow)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(12)
+            }
+            // dropped [0-7]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+        }
+    }
+
+    @Test
+    fun appendScroll_withSeparators() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow).map { pagingData ->
+            pagingData.insertSeparators { before: Int?, _ ->
+                if (before == 0 || before == 14) "sep" else null
+            }
+        }
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(12)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, "sep", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, "sep", 15, 16)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveAppendScroll() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(12)
+                scrollTo(18)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-19]
+            // prefetched [20-22]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+                21, 22)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveAppendScroll_multiSnapshots() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(12)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+
+            val snapshot2 = pager.asSnapshot(this) {
+                scrollTo(18)
+            }
+            // appended [17-19]
+            // prefetched [20-22]
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
+                    18, 19, 20, 21, 22
+                )
+            )
+        }
+    }
+
+    @Test
+    fun appendScroll_indexOutOfBounds() {
+        val dataFlow = flowOf(List(15) { it })
+        val pager = createPager(dataFlow).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                // index out of bounds
+                scrollTo(50)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // scrollTo appended [8-10]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)
+            )
+        }
+    }
+
+    @Test
+    fun appendScroll_accessPageBoundary() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                // after initial Load and prefetch, max loaded index is 7
+                scrollTo(7)
+            }
+            // ensure that SnapshotLoader waited for last prefetch before returning
+            // initial load [0-4]
+            // prefetched [5-7] - expect only one extra page to be prefetched after this
+            // scrollTo appended [8-10]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+            )
+        }
+    }
+
+    @Test
+    fun appendScroll_withoutPrefetch() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPrefetch(dataFlow)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(10)
+            }
+            // initial load [0-4]
+            // appended [5-10]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+            )
+        }
+    }
+
+    @Test
+    fun appendScroll_withoutPlaceholders() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                // scroll to max loaded index
+                scrollTo(7)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // scrollTo appended [8-10]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+            )
+        }
+    }
+
+    @Test
+    fun appendScroll_withoutPlaceholders_indexOutOfBounds() {
+        val dataFlow = flowOf(List(20) { it })
+        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                // 12 is larger than differ.size = 8 after initial refresh
+                scrollTo(12)
+            }
+            // ensure it honors scrollTo indices >= differ.size
+            // initial load [0-4]
+            // prefetched [5-7]
+            // scrollTo appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+        }
+    }
+
+    @Test
+    fun appendToScroll_withoutPlaceholders_indexOutOfBoundsIsCapped() {
+        val dataFlow = flowOf(List(20) { it })
+        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(50)
+            }
+            // ensure index is still capped to max index available
+            // initial load [0-4]
+            // prefetched [5-7]
+            // scrollTo appended [8-19]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveAppendScroll_withoutPlaceholders() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(12)
+                scrollTo(17)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // first scrollTo appended [8-13]
+            // second scrollTo appended [14-19]
+            // prefetched [19-22]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+                21, 22)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveAppendScroll_withoutPlaceholders_multiSnapshot() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(12)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // scrollTo appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+
+            val snapshot2 = pager.asSnapshot(this) {
+                scrollTo(17)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // first scrollTo appended [8-13]
+            // second scrollTo appended [14-19]
+            // prefetched [19-22]
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+                    21, 22)
+            )
+        }
+    }
+
+    @Test
+    fun scrollTo_indexAccountsForSeparators() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow)
+        val pagerWithSeparator = pager.map { pagingData ->
+            pagingData.insertSeparators { before: Int?, _ ->
+                if (before == 6) "sep" else null
+            }
+        }
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(8)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-10]
+            // prefetched [11-13]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)
+            )
+
+            val snapshotWithSeparator = pagerWithSeparator.asSnapshot(this) {
+                scrollTo(8)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-10]
+            // no prefetch on [11-13] because separator fulfilled prefetchDistance
+            assertThat(snapshotWithSeparator).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, "sep", 7, 8, 9, 10)
+            )
+        }
+    }
+
+    @Test
+    fun prependFling() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(42)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [41-46]
+            // prefetched [38-40]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_withDrops() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithDrops(dataFlow, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(42)
+            }
+            // dropped [47-57]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(38, 39, 40, 41, 42, 43, 44, 45, 46)
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_withSeparators() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, 50).map { pagingData ->
+            pagingData.insertSeparators { before: Int?, _ ->
+                if (before == 42 || before == 49) "sep" else null
+            }
+        }
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(42)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [41-46]
+            // prefetched [38-40]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    38, 39, 40, 41, 42, "sep", 43, 44, 45, 46, 47, 48, 49, "sep", 50, 51, 52,
+                    53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun consecutivePrependFling() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(42)
+                flingTo(38)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [38-46]
+            // prefetched [35-37]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
+                    51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun consecutivePrependFling_multiSnapshots() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(42)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [41-46]
+            // prefetched [38-40]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57
+                )
+            )
+
+            val snapshot2 = pager.asSnapshot(this) {
+                flingTo(38)
+            }
+            // prefetched [35-37]
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(
+                    35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
+                    51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+    @Test
+    fun prependFling_jump() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithJump(dataFlow, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(30)
+                // jump triggered when flingTo registered lastAccessedIndex[30], refreshKey[28]
+            }
+            // initial load [28-32]
+            // prefetched [25-27], [33-35]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35)
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_scrollThenJump() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithJump(dataFlow, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(43)
+                flingTo(30)
+                // jump triggered when flingTo registered lastAccessedIndex[30], refreshKey[28]
+            }
+            // initial load [28-32]
+            // prefetched [25-27], [33-35]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35)
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_jumpThenFling() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithJump(dataFlow, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(30)
+                // jump triggered when flingTo registered lastAccessedIndex[30], refreshKey[28]
+                flingTo(22)
+            }
+            // initial load [28-32]
+            // prefetched [25-27], [33-35]
+            // flingTo prepended [22-24]
+            // prefetched [19-21]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35)
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_indexOutOfBounds() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, 10)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(-3)
+            }
+            // initial load [10-14]
+            // prefetched [7-9], [15-17]
+            // flingTo prepended [0-6]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_accessPageBoundary() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                // page boundary
+                flingTo(44)
+            }
+            // ensure that SnapshotLoader waited for last prefetch before returning
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [44-46] - expect only one extra page to be prefetched after this
+            // prefetched [41-43]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_withoutPlaceholders() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, 50).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                // Without placeholders, first loaded page always starts at index[0]
+                flingTo(0)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [44-46]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_withoutPlaceholders_indexOutOfBounds() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, 50)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(-8)
+            }
+            // ensure we honor negative indices if there is data to load
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // prepended [38-46]
+            // prefetched [35-37]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53,
+                    54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_withoutPlaceholders_indexOutOfBoundsIsCapped() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, 5).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(-20)
+            }
+            // ensure index is capped when no more data to load
+            // initial load [5-9]
+            // prefetched [2-4], [10-12]
+            // flingTo prepended [0-1]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
+            )
+        }
+    }
+
+    @Test
+    fun consecutivePrependFling_withoutPlaceholders() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, 50).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                // Without placeholders, first loaded page always starts at index[0]
+                flingTo(-1)
+                // Without placeholders, first loaded page always starts at index[0]
+                flingTo(-5)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // first flingTo prepended [41-46]
+            // index[0] is now anchored to [41]
+            // second flingTo prepended [35-40]
+            // prefetched [32-34]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(
+                    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
+                    50, 51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun consecutivePrependFling_withoutPlaceholders_multiSnapshot() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow, 50).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                // Without placeholders, first loaded page always starts at index[0]
+                flingTo(-1)
+            }
+            // initial load [50-54]
+            // prefetched [47-49], [55-57]
+            // flingTo prepended [44-46]
+            // prefetched [41-43]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
+            )
+
+            val snapshot2 = pager.asSnapshot(this) {
+                // Without placeholders, first loaded page always starts at index[0]
+                flingTo(-5)
+            }
+            // flingTo prepended [35-40]
+            // prefetched [32-34]
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(
+                    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
+                    50, 51, 52, 53, 54, 55, 56, 57
+                )
+            )
+        }
+    }
+
+    @Test
+    fun prependFling_withoutPlaceholders_indexPrecision() {
+        val dataFlow = flowOf(List(100) { it })
+        // load sizes and prefetch set to 1 to test precision of flingTo indexing
+        val pager = Pager(
+            config = PagingConfig(
+                pageSize = 1,
+                initialLoadSize = 1,
+                enablePlaceholders = false,
+                prefetchDistance = 1
+            ),
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.asSnapshot(this) {
+                // after refresh, lastAccessedIndex == index[2] == item(9)
+                flingTo(-1)
+            }
+            // initial load [50]
+            // prefetched [49], [51]
+            // prepended [48]
+            // prefetched [47]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(47, 48, 49, 50, 51)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(12)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_withDrops() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithDrops(dataFlow)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(12)
+            }
+            // dropped [0-7]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_withSeparators() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow).map { pagingData ->
+            pagingData.insertSeparators { before: Int?, _ ->
+                if (before == 0 || before == 14) "sep" else null
+            }
+        }
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(12)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, "sep", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, "sep", 15, 16)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveAppendFling() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(12)
+                flingTo(18)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-19]
+            // prefetched [20-22]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+                    21, 22)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveAppendFling_multiSnapshots() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(12)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+
+            val snapshot2 = pager.asSnapshot(this) {
+                flingTo(18)
+            }
+            // appended [17-19]
+            // prefetched [20-22]
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
+                    18, 19, 20, 21, 22
+                )
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_jump() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithJump(dataFlow)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(30)
+                // jump triggered when flingTo registered lastAccessedIndex[30], refreshKey[28]
+            }
+            // initial load [28-32]
+            // prefetched [25-27], [33-35]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_scrollThenJump() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithJump(dataFlow)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                scrollTo(30)
+                flingTo(43)
+                // jump triggered when flingTo registered lastAccessedIndex[43], refreshKey[41]
+            }
+            // initial load [41-45]
+            // prefetched [38-40], [46-48]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_jumpThenFling() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerWithJump(dataFlow)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(30)
+                // jump triggered when flingTo registered lastAccessedIndex[30], refreshKey[28]
+                flingTo(38)
+            }
+            // initial load [28-32]
+            // prefetched [25-27], [33-35]
+            // flingTo appended [36-38]
+            // prefetched [39-41]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_indexOutOfBounds() {
+        val dataFlow = flowOf(List(15) { it })
+        val pager = createPager(dataFlow).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                // index out of bounds
+                flingTo(50)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // flingTo appended [8-10]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_accessPageBoundary() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(dataFlow).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                // after initial Load and prefetch, max loaded index is 7
+                flingTo(7)
+            }
+            // ensure that SnapshotLoader waited for last prefetch before returning
+            // initial load [0-4]
+            // prefetched [5-7] - expect only one extra page to be prefetched after this
+            // flingTo appended [8-10]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_withoutPlaceholders() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                // scroll to max loaded index
+                flingTo(7)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // flingTo appended [8-10]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_withoutPlaceholders_indexOutOfBounds() {
+        val dataFlow = flowOf(List(20) { it })
+        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                // 12 is larger than differ.size = 8 after initial refresh
+                flingTo(12)
+            }
+            // ensure it honors scrollTo indices >= differ.size
+            // initial load [0-4]
+            // prefetched [5-7]
+            // flingTo appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_withoutPlaceholders_indexOutOfBoundsIsCapped() {
+        val dataFlow = flowOf(List(20) { it })
+        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(50)
+            }
+            // ensure index is still capped to max index available
+            // initial load [0-4]
+            // prefetched [5-7]
+            // flingTo appended [8-19]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveAppendFling_withoutPlaceholders() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(12)
+                flingTo(17)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // first flingTo appended [8-13]
+            // second flingTo appended [14-19]
+            // prefetched [19-22]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+                    21, 22)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveAppendFling_withoutPlaceholders_multiSnapshot() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPagerNoPlaceholders(dataFlow).cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(12)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // flingTo appended [8-13]
+            // prefetched [14-16]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+
+            val snapshot2 = pager.asSnapshot(this) {
+                flingTo(17)
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            // first flingTo appended [8-13]
+            // second flingTo appended [14-19]
+            // prefetched [19-22]
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+                    21, 22)
+            )
+        }
+    }
+
+    @Test
+    fun appendFling_withoutPlaceholders_indexPrecision() {
+        val dataFlow = flowOf(List(100) { it })
+        // load sizes and prefetch set to 1 to test precision of flingTo indexing
+        val pager = Pager(
+            config = PagingConfig(
+                pageSize = 1,
+                initialLoadSize = 1,
+                enablePlaceholders = false,
+                prefetchDistance = 1
+            ),
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.asSnapshot(this) {
+                // after refresh, lastAccessedIndex == index[2] == item(9)
+                flingTo(2)
+            }
+            // initial load [0]
+            // prefetched [1]
+            // appended [2]
+            // prefetched [3]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3)
+            )
+        }
+    }
+
+    @Test
+    fun flingTo_indexAccountsForSeparators() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = createPager(
+            dataFlow,
+            PagingConfig(
+                pageSize = 1,
+                initialLoadSize = 1,
+                prefetchDistance = 1
+            ),
+            50
+        )
+        val pagerWithSeparator = pager.map { pagingData ->
+            pagingData.insertSeparators { before: Int?, _ ->
+                if (before == 49) "sep" else null
+            }
+        }
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                flingTo(51)
+            }
+            // initial load [50]
+            // prefetched [49], [51]
+            // flingTo [51] accessed item[51]prefetched [52]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(49, 50, 51, 52)
+            )
+
+            val snapshotWithSeparator = pagerWithSeparator.asSnapshot(this) {
+                flingTo(51)
+            }
+            // initial load [50]
+            // prefetched [49], [51]
+            // flingTo [51] accessed item[50], no prefetch triggered
+            assertThat(snapshotWithSeparator).containsExactlyElementsIn(
+                listOf(49, "sep", 50, 51)
+            )
+        }
+    }
+
+    private fun createPager(dataFlow: Flow<List<Int>>, initialKey: Int = 0) =
+        createPager(
+            dataFlow,
+            PagingConfig(pageSize = 3, initialLoadSize = 5),
+            initialKey
+        )
+
+    private fun createPagerNoPlaceholders(dataFlow: Flow<List<Int>>, initialKey: Int = 0) =
+        createPager(
+            dataFlow,
+            PagingConfig(
+                pageSize = 3,
+                initialLoadSize = 5,
+                enablePlaceholders = false,
+                prefetchDistance = 3
+            ),
+            initialKey)
+
+    private fun createPagerNoPrefetch(dataFlow: Flow<List<Int>>, initialKey: Int = 0) =
+        createPager(
+            dataFlow,
+            PagingConfig(pageSize = 3, initialLoadSize = 5, prefetchDistance = 0),
+            initialKey
+        )
+
+    private fun createPagerWithJump(dataFlow: Flow<List<Int>>, initialKey: Int = 0) =
+        createPager(
+            dataFlow,
+            PagingConfig(pageSize = 3, initialLoadSize = 5, jumpThreshold = 5),
+            initialKey
+        )
+
+    private fun createPagerWithDrops(dataFlow: Flow<List<Int>>, initialKey: Int = 0) =
+        createPager(
+            dataFlow,
+            PagingConfig(pageSize = 3, initialLoadSize = 5, maxSize = 9),
+            initialKey
+        )
+
+    private fun createPager(
+        dataFlow: Flow<List<Int>>,
+        config: PagingConfig,
+        initialKey: Int = 0,
+    ) = Pager(
+            config = config,
+            initialKey = initialKey,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow
+}
+
+private class WrappedPagingSourceFactory(
+    private val factory: () -> PagingSource<Int, Int>,
+    private val loadDelay: Long,
+) : () -> PagingSource<Int, Int> {
+    override fun invoke(): PagingSource<Int, Int> = TestPagingSource(factory(), loadDelay)
+}
+
+private class TestPagingSource(
+    private val originalSource: PagingSource<Int, Int>,
+    private val loadDelay: Long,
+) : PagingSource<Int, Int>() {
+    init {
+        originalSource.registerInvalidatedCallback {
+            invalidate()
+        }
+    }
+    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Int> {
+        delay(loadDelay)
+        return originalSource.load(params)
+    }
+
+    override fun getRefreshKey(state: PagingState<Int, Int>) = originalSource.getRefreshKey(state)
+
+    override val jumpingSupported: Boolean = originalSource.jumpingSupported
 }
\ No newline at end of file
diff --git a/playground-common/androidx-shared.properties b/playground-common/androidx-shared.properties
index 8ae78c0..6ae45af 100644
--- a/playground-common/androidx-shared.properties
+++ b/playground-common/androidx-shared.properties
@@ -70,7 +70,14 @@
 kotlin.mpp.stability.nowarn=true
 # b/227307216
 kotlin.mpp.absentAndroidTarget.nowarn=true
+# As of October 3 2022, AGP 7.4.0-alpha08 is higher than AGP 7.3
+# Presumably complains if using a non-stable AGP, which we are regularly doing to test pre-stable.
+kotlin.mpp.androidGradlePluginCompatibility.nowarn=true
 
 # Properties we often want to toggle
 # ksp.version.check=false
 # androidx.compose.multiplatformEnabled=true
+
+kotlin.mpp.androidSourceSetLayoutVersion=1
+
+
diff --git a/playground-common/gradle/wrapper/gradle-wrapper.properties b/playground-common/gradle/wrapper/gradle-wrapper.properties
index 03ca076..f0da89b 100644
--- a/playground-common/gradle/wrapper/gradle-wrapper.properties
+++ b/playground-common/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionSha256Sum=7ba68c54029790ab444b39d7e293d3236b2632631fb5f2e012bb28b4ff669e4b
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
+distributionSha256Sum=28ebe9afc20564bcdc39bfe36f6b60a373e40be2c3c307a0028b545b8ccf6ba0
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-rc-2-bin.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index f653d40..597d61c 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -25,7 +25,7 @@
 kotlin.code.style=official
 # Disable docs
 androidx.enableDocumentation=false
-androidx.playground.snapshotBuildId=9410467
-androidx.playground.metalavaBuildId=9370379
+androidx.playground.snapshotBuildId=9519019
+androidx.playground.metalavaBuildId=9508658
 androidx.playground.dokkaBuildId=7472101
 androidx.studio.type=playground
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/camera/camera-viewfinder/api/res-1.1.0-beta03.txt b/privacysandbox/ads/ads-adservices-java/api/res-current.txt
similarity index 100%
copy from camera/camera-viewfinder/api/res-1.1.0-beta03.txt
copy to 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..49c545a
--- /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()
+                .setAdsSdkName("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()
+                        .setAdsSdkName("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..9512955
--- /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()
+            .setAdsSdkName(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..30cd307
--- /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 adsSdkName, optional boolean shouldRecordObservation);
+    method public String getAdsSdkName();
+    method public boolean getShouldRecordObservation();
+    property public final String adsSdkName;
+    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 setAdsSdkName(String adsSdkName);
+    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..30cd307
--- /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 adsSdkName, optional boolean shouldRecordObservation);
+    method public String getAdsSdkName();
+    method public boolean getShouldRecordObservation();
+    property public final String adsSdkName;
+    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 setAdsSdkName(String adsSdkName);
+    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/camera/camera-viewfinder/api/res-1.1.0-beta03.txt b/privacysandbox/ads/ads-adservices/api/res-current.txt
similarity index 100%
rename from camera/camera-viewfinder/api/res-1.1.0-beta03.txt
rename to 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..30cd307
--- /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 adsSdkName, optional boolean shouldRecordObservation);
+    method public String getAdsSdkName();
+    method public boolean getShouldRecordObservation();
+    property public final String adsSdkName;
+    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 setAdsSdkName(String adsSdkName);
+    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..297e54c
--- /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: adsSdkName=sdk1, shouldRecordObservation=false"
+        val request = GetTopicsRequest("sdk1", false)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+
+        // Verify Builder.
+        val request2 = GetTopicsRequest.Builder()
+            .setAdsSdkName("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..80e915a
--- /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()
+                .setAdsSdkName(mSdkName)
+                .setShouldRecordObservation(true)
+                .build()
+
+            managerCompat!!.getTopics(request)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(android.adservices.topics.GetTopicsRequest::class.java)
+        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()
+            .setAdsSdkName(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..eaaae29
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt
@@ -0,0 +1,98 @@
+/*
+ * 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. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @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..7c57149
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt
@@ -0,0 +1,94 @@
+/*
+ * 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. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @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..f41199b
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt
@@ -0,0 +1,219 @@
+/*
+ * 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. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @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..ee47562
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt
@@ -0,0 +1,260 @@
+/*
+ * 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. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @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..af07f77
--- /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 adsSdkName 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 adsSdkName: String = "",
+    @get:JvmName("shouldRecordObservation")
+    val shouldRecordObservation: Boolean = false
+) {
+    override fun toString(): String {
+        return "GetTopicsRequest: " +
+            "adsSdkName=$adsSdkName, shouldRecordObservation=$shouldRecordObservation"
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is GetTopicsRequest) return false
+        return this.adsSdkName == other.adsSdkName &&
+            this.shouldRecordObservation == other.shouldRecordObservation
+    }
+
+    override fun hashCode(): Int {
+        var hash = adsSdkName.hashCode()
+        hash = 31 * hash + shouldRecordObservation.hashCode()
+        return hash
+    }
+
+    /**
+     * Builder for [GetTopicsRequest].
+     */
+    public class Builder() {
+        private var adsSdkName: 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 adsSdkName the Ads Sdk Name.
+         */
+        fun setAdsSdkName(adsSdkName: String): Builder = apply { this.adsSdkName = adsSdkName }
+
+        /**
+         * 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(adsSdkName.isNotEmpty()) { "adsSdkName must be set" }
+            return GetTopicsRequest(adsSdkName, 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..488125d
--- /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.adsSdkName)
+                .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/generator/AbstractSdkProviderGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
index c9e8b12..6c070aa 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
@@ -41,7 +41,7 @@
             return null
         }
         val packageName = api.getOnlyService().type.packageName
-        val className = "AbstractSandboxedSdkProvider"
+        val className = "AbstractSandboxedSdkProviderCompat"
         val classSpec =
             TypeSpec.classBuilder(className)
                 .superclass(superclassName)
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/CompatSdkProviderGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/CompatSdkProviderGenerator.kt
index 9934deb..00b4801 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/CompatSdkProviderGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/CompatSdkProviderGenerator.kt
@@ -23,7 +23,6 @@
 import com.squareup.kotlinpoet.ClassName
 import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.KModifier
-import com.squareup.kotlinpoet.MemberName
 
 /** SDK Provider generator that uses the SDK Runtime library to communicate with the sandbox. */
 internal class CompatSdkProviderGenerator(parsedApi: ParsedApi) :
@@ -31,14 +30,6 @@
     companion object {
         private val sandboxedSdkCompatClass =
             ClassName("androidx.privacysandbox.sdkruntime.core", "SandboxedSdkCompat")
-        private val sandboxedSdkCompatCreateMethod =
-            MemberName(
-                ClassName(
-                    sandboxedSdkCompatClass.packageName,
-                    sandboxedSdkCompatClass.simpleName,
-                    "Companion"
-                ), "create"
-            )
     }
 
     override val superclassName =
@@ -52,8 +43,8 @@
             "val sdk = ${createServiceFunctionName(api.getOnlyService())}(context!!)"
         )
         addStatement(
-            "return %M(%T(sdk))",
-            sandboxedSdkCompatCreateMethod,
+            "return %T(%T(sdk))",
+            sandboxedSdkCompatClass,
             api.getOnlyService().stubDelegateNameSpec()
         )
     }
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..f1c766a 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
@@ -19,6 +19,8 @@
 import androidx.privacysandbox.tools.core.model.AnnotatedInterface
 import androidx.privacysandbox.tools.core.model.Method
 import androidx.privacysandbox.tools.core.model.Parameter
+import androidx.privacysandbox.tools.core.model.Types.any
+import androidx.privacysandbox.tools.core.model.Types.sandboxedUiAdapter
 import com.google.devtools.ksp.getDeclaredFunctions
 import com.google.devtools.ksp.getDeclaredProperties
 import com.google.devtools.ksp.isPublic
@@ -29,9 +31,13 @@
 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 validInterfaceModifiers = setOf(Modifier.PUBLIC)
     private val validMethodModifiers = setOf(Modifier.PUBLIC, Modifier.SUSPEND)
+    private val validInterfaceSuperTypes = setOf(sandboxedUiAdapter)
 
     fun parseInterface(interfaceDeclaration: KSClassDeclaration): AnnotatedInterface {
         check(interfaceDeclaration.classKind == ClassKind.INTERFACE) {
@@ -71,10 +77,23 @@
                 })."
             )
         }
+        val superTypes = interfaceDeclaration.superTypes.map {
+            typeParser.parseFromDeclaration(it.resolve().declaration)
+        }.filterNot { it == any }.toList()
+        val invalidSuperTypes =
+            superTypes.filterNot { validInterfaceSuperTypes.contains(it) }
+        if (invalidSuperTypes.isNotEmpty()) {
+            logger.error(
+                "Error in $name: annotated interface inherits prohibited types (${
+                    superTypes.map { it.simpleName }.sorted().joinToString(limit = 3)
+                })."
+            )
+        }
 
         val methods = interfaceDeclaration.getDeclaredFunctions().map(::parseMethod).toList()
         return AnnotatedInterface(
             type = typeParser.parseFromDeclaration(interfaceDeclaration),
+            superTypes = superTypes,
             methods = methods,
         )
     }
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt
index 71b9761..4d334f8 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt
@@ -34,11 +34,7 @@
         val inputSources = loadSourcesFromDirectory(inputTestDataDir)
         val expectedKotlinSources = loadSourcesFromDirectory(outputTestDataDir)
 
-        val result = compileWithPrivacySandboxKspCompiler(
-            inputSources,
-            platformStubs = PlatformStubs.API_33,
-            extraProcessorOptions = mapOf("skip_sdk_runtime_compat_library" to "true")
-        )
+        val result = compileWithPrivacySandboxKspCompiler(inputSources)
         assertThat(result).succeeds()
 
         val expectedAidlFilepath = listOf(
@@ -48,6 +44,7 @@
             "com/mysdk/IMyInterfaceTransactionCallback.java",
             "com/mysdk/IMySdk.java",
             "com/mysdk/IMySecondInterface.java",
+            "com/mysdk/IMyUiInterface.java",
             "com/mysdk/IMySecondInterfaceTransactionCallback.java",
             "com/mysdk/IResponseTransactionCallback.java",
             "com/mysdk/IStringTransactionCallback.java",
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt
deleted file mode 100644
index 1ef27b0..0000000
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.privacysandbox.tools.apicompiler
-
-import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertThat
-import androidx.privacysandbox.tools.testing.loadSourcesFromDirectory
-import java.io.File
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class SdkRuntimeLibrarySdkTest {
-    @Test
-    fun compileServiceInterface_ok() {
-        val inputTestDataDir = File("src/test/test-data/sdkruntimelibrarysdk/input")
-        val outputTestDataDir = File("src/test/test-data/sdkruntimelibrarysdk/output")
-        val inputSources = loadSourcesFromDirectory(inputTestDataDir)
-        val expectedKotlinSources = loadSourcesFromDirectory(outputTestDataDir)
-
-        val result = compileWithPrivacySandboxKspCompiler(inputSources)
-        assertThat(result).succeeds()
-
-        val expectedAidlFilepath = listOf(
-            "com/mysdk/ICancellationSignal.java",
-            "com/mysdk/IBackwardsCompatibleSdk.java",
-            "com/mysdk/IStringTransactionCallback.java",
-            "com/mysdk/ParcelableStackFrame.java",
-            "com/mysdk/PrivacySandboxThrowableParcel.java",
-        )
-        assertThat(result).hasAllExpectedGeneratedSourceFilesAndContent(
-            expectedKotlinSources,
-            expectedAidlFilepath
-        )
-    }
-}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkWithPackagesTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkWithPackagesTest.kt
index 51b78ad..7928c89 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkWithPackagesTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkWithPackagesTest.kt
@@ -35,11 +35,7 @@
         val inputSources = loadSourcesFromDirectory(inputTestDataDir)
         val expectedKotlinSources = loadSourcesFromDirectory(outputTestDataDir)
 
-        val result = compileWithPrivacySandboxKspCompiler(
-            inputSources,
-            platformStubs = PlatformStubs.API_33,
-            extraProcessorOptions = mapOf("skip_sdk_runtime_compat_library" to "true")
-        )
+        val result = compileWithPrivacySandboxKspCompiler(inputSources)
         assertThat(result).succeeds()
 
         val expectedAidlFilepath = listOf(
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
index e6b6e3d..94f7888 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
@@ -16,6 +16,7 @@
 
 package androidx.privacysandbox.tools.apicompiler
 
+import androidx.privacysandbox.tools.testing.allTestLibraryStubs
 import androidx.privacysandbox.tools.testing.CompilationTestHelper
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.compiler.TestCompilationResult
@@ -28,7 +29,7 @@
  */
 fun compileWithPrivacySandboxKspCompiler(
     sources: List<Source>,
-    platformStubs: PlatformStubs = PlatformStubs.SDK_RUNTIME_LIBRARY,
+    addLibraryStubs: Boolean = true,
     extraProcessorOptions: Map<String, String> = mapOf(),
 ): TestCompilationResult {
     val provider = PrivacySandboxKspCompiler.Provider()
@@ -41,118 +42,9 @@
     }
 
     return CompilationTestHelper.compileAll(
-        sources + platformStubs.sources,
+        if (addLibraryStubs) sources + allTestLibraryStubs
+        else sources,
         symbolProcessorProviders = listOf(provider),
         processorOptions = processorOptions,
     )
 }
-
-enum class PlatformStubs(val sources: List<Source>) {
-    API_33(syntheticApi33PrivacySandboxStubs),
-    SDK_RUNTIME_LIBRARY(syntheticSdkRuntimeLibraryStubs),
-}
-
-// SDK Runtime library is not available in AndroidX prebuilts, so while that's the case we use fake
-// stubs to run our compilation tests.
-private val syntheticSdkRuntimeLibraryStubs = listOf(
-    Source.kotlin(
-        "androidx/privacysandbox/sdkruntime/core/SandboxedSdkCompat.kt", """
-        |package androidx.privacysandbox.sdkruntime.core
-        |
-        |import android.os.IBinder
-        |
-        |@Suppress("UNUSED_PARAMETER")
-        |sealed class SandboxedSdkCompat {
-        |    abstract fun getInterface(): IBinder?
-        |
-        |    companion object {
-        |        fun create(binder: IBinder): SandboxedSdkCompat = throw RuntimeException("Stub!")
-        |    }
-        |}
-        |""".trimMargin()
-    ),
-    Source.kotlin(
-        "androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderCompat.kt", """
-        |package androidx.privacysandbox.sdkruntime.core
-        |
-        |import android.content.Context
-        |import android.os.Bundle
-        |import android.view.View
-        |
-        |@Suppress("UNUSED_PARAMETER")
-        |abstract class SandboxedSdkProviderCompat {
-        |   var context: Context? = null
-        |       private set
-        |   fun attachContext(context: Context): Unit = throw RuntimeException("Stub!")
-        |
-        |   abstract fun onLoadSdk(params: Bundle): SandboxedSdkCompat
-        |
-        |   open fun beforeUnloadSdk() {}
-        |
-        |   abstract fun getView(
-        |       windowContext: Context,
-        |       params: Bundle,
-        |       width: Int,
-        |       height: Int
-        |   ): View
-        |}
-        |""".trimMargin()
-    )
-)
-
-// PrivacySandbox platform APIs are not available in AndroidX prebuilts nor are they stable, so
-// while that's the case we use fake stubs to run our compilation tests.
-val syntheticApi33PrivacySandboxStubs = listOf(
-    Source.java(
-        "android.app.sdksandbox.SandboxedSdk", """
-        |package android.app.sdksandbox;
-        |
-        |import android.os.IBinder;
-        |
-        |public final class SandboxedSdk {
-        |    public SandboxedSdk(IBinder sdkInterface) {}
-        |    public IBinder getInterface() { throw new RuntimeException("Stub!"); }
-        |}
-        |""".trimMargin()
-    ),
-    Source.java(
-        "android.app.sdksandbox.SandboxedSdkProvider", """
-        |package android.app.sdksandbox;
-        |
-        |import android.content.Context;
-        |import android.os.Bundle;
-        |import android.view.View;
-        |
-        |public abstract class SandboxedSdkProvider {
-        |    public final void attachContext(Context context) {
-        |        throw new RuntimeException("Stub!");
-        |    }
-        |    public final Context getContext() {
-        |        throw new RuntimeException("Stub!");
-        |    }
-        |    public abstract SandboxedSdk onLoadSdk(Bundle params)
-        |        throws LoadSdkException;
-        |
-        |    public void beforeUnloadSdk() {}
-        |
-        |    public abstract View getView(
-        |        Context windowContext, Bundle params, int width, int height);
-        |}
-        |""".trimMargin()
-    ),
-    Source.java(
-        "android.app.sdksandbox.LoadSdkException", """
-        |package android.app.sdksandbox;
-        |
-        |@SuppressWarnings("serial")
-        |public final class LoadSdkException extends Exception {}
-        |""".trimMargin()
-    ),
-    Source.java(
-        "android.app.sdksandbox.SandboxedSdkContext", """
-        |package android.app.sdksandbox;
-        |
-        |public final class SandboxedSdkContext {}
-        |""".trimMargin()
-    ),
-)
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/WithoutRuntimeLibrarySdkTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/WithoutRuntimeLibrarySdkTest.kt
new file mode 100644
index 0000000..388e130
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/WithoutRuntimeLibrarySdkTest.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.tools.apicompiler
+
+import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertThat
+import androidx.privacysandbox.tools.testing.loadSourcesFromDirectory
+import java.io.File
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class WithoutRuntimeLibrarySdkTest {
+    @Test
+    fun compileServiceInterface_ok() {
+        val inputTestDataDir = File("src/test/test-data/withoutruntimelibrarysdk/input")
+        val outputTestDataDir = File("src/test/test-data/withoutruntimelibrarysdk/output")
+        val inputSources = loadSourcesFromDirectory(inputTestDataDir)
+        val expectedKotlinSources = loadSourcesFromDirectory(outputTestDataDir)
+
+        val result = compileWithPrivacySandboxKspCompiler(
+            inputSources,
+            addLibraryStubs = false,
+            extraProcessorOptions = mapOf("skip_sdk_runtime_compat_library" to "true")
+        )
+        assertThat(result).succeeds()
+
+        val expectedAidlFilepath = listOf(
+            "com/mysdk/ICancellationSignal.java",
+            "com/mysdk/IWithoutRuntimeLibrarySdk.java",
+            "com/mysdk/IStringTransactionCallback.java",
+            "com/mysdk/ParcelableStackFrame.java",
+            "com/mysdk/PrivacySandboxThrowableParcel.java",
+        )
+        assertThat(result).hasAllExpectedGeneratedSourceFilesAndContent(
+            expectedKotlinSources,
+            expectedAidlFilepath
+        )
+    }
+}
\ No newline at end of file
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..730e107 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
@@ -93,6 +93,64 @@
     }
 
     @Test
+    fun parseInterfaceInheritance_ok() {
+        val serviceSource =
+            Source.kotlin(
+                "com/mysdk/MySdk.kt",
+                """
+                    package com.mysdk
+                    import androidx.privacysandbox.tools.PrivacySandboxService
+                    @PrivacySandboxService
+                    interface MySdk {
+                        suspend fun doStuff(): MyUiInterface
+                    }
+                """
+            )
+        val interfaceSource =
+            Source.kotlin(
+                "com/mysdk/MyUiInterface.kt",
+                """
+                    |package com.mysdk
+                    |import androidx.privacysandbox.tools.PrivacySandboxInterface
+                    |import androidx.privacysandbox.ui.core.SandboxedUiAdapter as SUiAdapter
+                    |
+                    |@PrivacySandboxInterface
+                    |interface MyUiInterface : SUiAdapter {
+                    |}
+                """.trimMargin()
+            )
+        assertThat(parseSources(serviceSource, interfaceSource)).isEqualTo(
+            ParsedApi(
+                services = setOf(
+                    AnnotatedInterface(
+                        type = Type(packageName = "com.mysdk", simpleName = "MySdk"),
+                        methods = listOf(
+                            Method(
+                                name = "doStuff",
+                                parameters = listOf(),
+                                returnType = Type(
+                                    packageName = "com.mysdk",
+                                    simpleName = "MyUiInterface"
+                                ),
+                                isSuspend = true,
+                            ),
+                        )
+                    )
+                ),
+                interfaces = setOf(
+                    AnnotatedInterface(
+                        type = Type(packageName = "com.mysdk", simpleName = "MyUiInterface"),
+                        superTypes = listOf(
+                            Types.sandboxedUiAdapter,
+                        ),
+                        methods = listOf()
+                    )
+                )
+            )
+        )
+    }
+
+    @Test
     fun serviceAnnotatedClass_fails() {
         val source = Source.kotlin(
             "com/mysdk/MySdk.kt", """
@@ -202,6 +260,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 +324,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/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/util/KspTestRunner.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/util/KspTestRunner.kt
index c14e1fe..31aee1f2 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/util/KspTestRunner.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/util/KspTestRunner.kt
@@ -16,10 +16,11 @@
 
 package androidx.privacysandbox.tools.apicompiler.util
 
-import androidx.privacysandbox.tools.core.model.ParsedApi
 import androidx.privacysandbox.tools.apicompiler.parser.ApiParser
-import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertThat
+import androidx.privacysandbox.tools.core.model.ParsedApi
 import androidx.privacysandbox.tools.testing.CompilationResultSubject
+import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertThat
+import androidx.privacysandbox.tools.testing.allTestLibraryStubs
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
 import androidx.room.compiler.processing.util.compiler.compile
@@ -40,7 +41,7 @@
         compile(
             Files.createTempDirectory("test").toFile(),
             TestCompilationArguments(
-                sources = sources.toList(),
+                sources = sources.toList() + allTestLibraryStubs,
                 symbolProcessorProviders = listOf(provider),
             )
         )
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/input/com/mysdk/MySdk.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/input/com/mysdk/MySdk.kt
index 91fa3c6..73f1eb9 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/input/com/mysdk/MySdk.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/input/com/mysdk/MySdk.kt
@@ -4,6 +4,7 @@
 import androidx.privacysandbox.tools.PrivacySandboxInterface
 import androidx.privacysandbox.tools.PrivacySandboxService
 import androidx.privacysandbox.tools.PrivacySandboxValue
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
 
 @PrivacySandboxService
 interface MySdk {
@@ -40,6 +41,11 @@
 }
 
 @PrivacySandboxInterface
+interface MyUiInterface : SandboxedUiAdapter {
+    fun doSomethingForUi(x: Int, y: Int)
+}
+
+@PrivacySandboxInterface
 interface MySecondInterface {
     suspend fun doIntStuff(x: List<Int>): List<Int>
 
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/AbstractSandboxedSdkProvider.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/AbstractSandboxedSdkProvider.kt
deleted file mode 100644
index 43c54ac..0000000
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/AbstractSandboxedSdkProvider.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.mysdk
-
-import android.app.sdksandbox.SandboxedSdk
-import android.app.sdksandbox.SandboxedSdkProvider
-import android.content.Context
-import android.os.Bundle
-import android.view.View
-import kotlin.Int
-
-public abstract class AbstractSandboxedSdkProvider : SandboxedSdkProvider() {
-  public override fun onLoadSdk(params: Bundle): SandboxedSdk {
-    val sdk = createMySdk(context!!)
-    return SandboxedSdk(MySdkStubDelegate(sdk))
-  }
-
-  public override fun getView(
-    windowContext: Context,
-    params: Bundle,
-    width: Int,
-    height: Int,
-  ): View {
-    TODO("Implement")
-  }
-
-  protected abstract fun createMySdk(context: Context): MySdk
-}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/AbstractSandboxedSdkProviderCompat.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/AbstractSandboxedSdkProviderCompat.kt
new file mode 100644
index 0000000..afe24f8
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/AbstractSandboxedSdkProviderCompat.kt
@@ -0,0 +1,26 @@
+package com.mysdk
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
+import kotlin.Int
+
+public abstract class AbstractSandboxedSdkProviderCompat : SandboxedSdkProviderCompat() {
+  public override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
+    val sdk = createMySdk(context!!)
+    return SandboxedSdkCompat(MySdkStubDelegate(sdk))
+  }
+
+  public override fun getView(
+    windowContext: Context,
+    params: Bundle,
+    width: Int,
+    height: Int,
+  ): View {
+    TODO("Implement")
+  }
+
+  protected abstract fun createMySdk(context: Context): MySdk
+}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MyUiInterfaceStubDelegate.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MyUiInterfaceStubDelegate.kt
new file mode 100644
index 0000000..9ab1bee
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MyUiInterfaceStubDelegate.kt
@@ -0,0 +1,12 @@
+package com.mysdk
+
+import kotlin.Int
+import kotlin.Unit
+
+public class MyUiInterfaceStubDelegate internal constructor(
+  public val `delegate`: MyUiInterface,
+) : IMyUiInterface.Stub() {
+  public override fun doSomethingForUi(x: Int, y: Int): Unit {
+    delegate.doSomethingForUi(x, y)
+  }
+}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/input/com/mysdk/BackwardsCompatibleSdk.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/input/com/mysdk/BackwardsCompatibleSdk.kt
deleted file mode 100644
index 8424489..0000000
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/input/com/mysdk/BackwardsCompatibleSdk.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.mysdk
-
-import androidx.privacysandbox.tools.PrivacySandboxCallback
-import androidx.privacysandbox.tools.PrivacySandboxInterface
-import androidx.privacysandbox.tools.PrivacySandboxService
-import androidx.privacysandbox.tools.PrivacySandboxValue
-
-@PrivacySandboxService
-interface BackwardsCompatibleSdk {
-    suspend fun doStuff(x: Int, y: Int): String
-}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/AbstractSandboxedSdkProvider.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/AbstractSandboxedSdkProvider.kt
deleted file mode 100644
index 5a0d588..0000000
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/AbstractSandboxedSdkProvider.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.mysdk
-
-import android.content.Context
-import android.os.Bundle
-import android.view.View
-import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
-import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat.Companion.create
-import androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
-import kotlin.Int
-
-public abstract class AbstractSandboxedSdkProvider : SandboxedSdkProviderCompat() {
-  public override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
-    val sdk = createBackwardsCompatibleSdk(context!!)
-    return create(BackwardsCompatibleSdkStubDelegate(sdk))
-  }
-
-  public override fun getView(
-    windowContext: Context,
-    params: Bundle,
-    width: Int,
-    height: Int,
-  ): View {
-    TODO("Implement")
-  }
-
-  protected abstract fun createBackwardsCompatibleSdk(context: Context): BackwardsCompatibleSdk
-}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/BackwardsCompatibleSdkFactory.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/BackwardsCompatibleSdkFactory.kt
deleted file mode 100644
index 63749650..0000000
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/BackwardsCompatibleSdkFactory.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.mysdk
-
-import android.os.IBinder
-import androidx.privacysandbox.tools.`internal`.GeneratedPublicApi
-
-@GeneratedPublicApi
-public object BackwardsCompatibleSdkFactory {
-    @Suppress("UNUSED_PARAMETER")
-    public fun wrapToBackwardsCompatibleSdk(binder: IBinder): BackwardsCompatibleSdk = throw
-            RuntimeException("Stub!")
-}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/BackwardsCompatibleSdkStubDelegate.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/BackwardsCompatibleSdkStubDelegate.kt
deleted file mode 100644
index bca930d..0000000
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/BackwardsCompatibleSdkStubDelegate.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.mysdk
-
-import com.mysdk.PrivacySandboxThrowableParcelConverter.toThrowableParcel
-import kotlin.Int
-import kotlin.Unit
-import kotlinx.coroutines.DelicateCoroutinesApi
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-
-public class BackwardsCompatibleSdkStubDelegate internal constructor(
-  public val `delegate`: BackwardsCompatibleSdk,
-) : IBackwardsCompatibleSdk.Stub() {
-  public override fun doStuff(
-    x: Int,
-    y: Int,
-    transactionCallback: IStringTransactionCallback,
-  ): Unit {
-    @OptIn(DelicateCoroutinesApi::class)
-    val job = GlobalScope.launch(Dispatchers.Main) {
-      try {
-        val result = delegate.doStuff(x, y)
-        transactionCallback.onSuccess(result)
-      }
-      catch (t: Throwable) {
-        transactionCallback.onFailure(toThrowableParcel(t))
-      }
-    }
-    val cancellationSignal = TransportCancellationCallback() { job.cancel() }
-    transactionCallback.onCancellable(cancellationSignal)
-  }
-}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkwithpackages/output/com/mysdk/AbstractSandboxedSdkProvider.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkwithpackages/output/com/mysdk/AbstractSandboxedSdkProvider.kt
deleted file mode 100644
index 43c54ac..0000000
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkwithpackages/output/com/mysdk/AbstractSandboxedSdkProvider.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.mysdk
-
-import android.app.sdksandbox.SandboxedSdk
-import android.app.sdksandbox.SandboxedSdkProvider
-import android.content.Context
-import android.os.Bundle
-import android.view.View
-import kotlin.Int
-
-public abstract class AbstractSandboxedSdkProvider : SandboxedSdkProvider() {
-  public override fun onLoadSdk(params: Bundle): SandboxedSdk {
-    val sdk = createMySdk(context!!)
-    return SandboxedSdk(MySdkStubDelegate(sdk))
-  }
-
-  public override fun getView(
-    windowContext: Context,
-    params: Bundle,
-    width: Int,
-    height: Int,
-  ): View {
-    TODO("Implement")
-  }
-
-  protected abstract fun createMySdk(context: Context): MySdk
-}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkwithpackages/output/com/mysdk/AbstractSandboxedSdkProviderCompat.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkwithpackages/output/com/mysdk/AbstractSandboxedSdkProviderCompat.kt
new file mode 100644
index 0000000..afe24f8
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkwithpackages/output/com/mysdk/AbstractSandboxedSdkProviderCompat.kt
@@ -0,0 +1,26 @@
+package com.mysdk
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
+import kotlin.Int
+
+public abstract class AbstractSandboxedSdkProviderCompat : SandboxedSdkProviderCompat() {
+  public override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
+    val sdk = createMySdk(context!!)
+    return SandboxedSdkCompat(MySdkStubDelegate(sdk))
+  }
+
+  public override fun getView(
+    windowContext: Context,
+    params: Bundle,
+    width: Int,
+    height: Int,
+  ): View {
+    TODO("Implement")
+  }
+
+  protected abstract fun createMySdk(context: Context): MySdk
+}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/withoutruntimelibrarysdk/input/com/mysdk/WithoutRuntimeLibrarySdk.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/withoutruntimelibrarysdk/input/com/mysdk/WithoutRuntimeLibrarySdk.kt
new file mode 100644
index 0000000..c06a208
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/withoutruntimelibrarysdk/input/com/mysdk/WithoutRuntimeLibrarySdk.kt
@@ -0,0 +1,11 @@
+package com.mysdk
+
+import androidx.privacysandbox.tools.PrivacySandboxCallback
+import androidx.privacysandbox.tools.PrivacySandboxInterface
+import androidx.privacysandbox.tools.PrivacySandboxService
+import androidx.privacysandbox.tools.PrivacySandboxValue
+
+@PrivacySandboxService
+interface WithoutRuntimeLibrarySdk {
+    suspend fun doStuff(x: Int, y: Int): String
+}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/withoutruntimelibrarysdk/output/com/mysdk/AbstractSandboxedSdkProviderCompat.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/withoutruntimelibrarysdk/output/com/mysdk/AbstractSandboxedSdkProviderCompat.kt
new file mode 100644
index 0000000..744be58
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/withoutruntimelibrarysdk/output/com/mysdk/AbstractSandboxedSdkProviderCompat.kt
@@ -0,0 +1,26 @@
+package com.mysdk
+
+import android.app.sdksandbox.SandboxedSdk
+import android.app.sdksandbox.SandboxedSdkProvider
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import kotlin.Int
+
+public abstract class AbstractSandboxedSdkProviderCompat : SandboxedSdkProvider() {
+  public override fun onLoadSdk(params: Bundle): SandboxedSdk {
+    val sdk = createWithoutRuntimeLibrarySdk(context!!)
+    return SandboxedSdk(WithoutRuntimeLibrarySdkStubDelegate(sdk))
+  }
+
+  public override fun getView(
+    windowContext: Context,
+    params: Bundle,
+    width: Int,
+    height: Int,
+  ): View {
+    TODO("Implement")
+  }
+
+  protected abstract fun createWithoutRuntimeLibrarySdk(context: Context): WithoutRuntimeLibrarySdk
+}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/withoutruntimelibrarysdk/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt
similarity index 100%
rename from privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt
rename to privacysandbox/tools/tools-apicompiler/src/test/test-data/withoutruntimelibrarysdk/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/TransportCancellationCallback.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/withoutruntimelibrarysdk/output/com/mysdk/TransportCancellationCallback.kt
similarity index 100%
rename from privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/TransportCancellationCallback.kt
rename to privacysandbox/tools/tools-apicompiler/src/test/test-data/withoutruntimelibrarysdk/output/com/mysdk/TransportCancellationCallback.kt
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/withoutruntimelibrarysdk/output/com/mysdk/WithoutRuntimeLibrarySdkFactory.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/withoutruntimelibrarysdk/output/com/mysdk/WithoutRuntimeLibrarySdkFactory.kt
new file mode 100644
index 0000000..71eaa79
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/withoutruntimelibrarysdk/output/com/mysdk/WithoutRuntimeLibrarySdkFactory.kt
@@ -0,0 +1,11 @@
+package com.mysdk
+
+import android.os.IBinder
+import androidx.privacysandbox.tools.`internal`.GeneratedPublicApi
+
+@GeneratedPublicApi
+public object WithoutRuntimeLibrarySdkFactory {
+    @Suppress("UNUSED_PARAMETER")
+    public fun wrapToWithoutRuntimeLibrarySdk(binder: IBinder): WithoutRuntimeLibrarySdk = throw
+            RuntimeException("Stub!")
+}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/withoutruntimelibrarysdk/output/com/mysdk/WithoutRuntimeLibrarySdkStubDelegate.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/withoutruntimelibrarysdk/output/com/mysdk/WithoutRuntimeLibrarySdkStubDelegate.kt
new file mode 100644
index 0000000..3a82d43
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/withoutruntimelibrarysdk/output/com/mysdk/WithoutRuntimeLibrarySdkStubDelegate.kt
@@ -0,0 +1,32 @@
+package com.mysdk
+
+import com.mysdk.PrivacySandboxThrowableParcelConverter.toThrowableParcel
+import kotlin.Int
+import kotlin.Unit
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+
+public class WithoutRuntimeLibrarySdkStubDelegate internal constructor(
+  public val `delegate`: WithoutRuntimeLibrarySdk,
+) : IWithoutRuntimeLibrarySdk.Stub() {
+  public override fun doStuff(
+    x: Int,
+    y: Int,
+    transactionCallback: IStringTransactionCallback,
+  ): Unit {
+    @OptIn(DelicateCoroutinesApi::class)
+    val job = GlobalScope.launch(Dispatchers.Main) {
+      try {
+        val result = delegate.doStuff(x, y)
+        transactionCallback.onSuccess(result)
+      }
+      catch (t: Throwable) {
+        transactionCallback.onFailure(toThrowableParcel(t))
+      }
+    }
+    val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+    transactionCallback.onCancellable(cancellationSignal)
+  }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt
index 02ecc7d..e7e87a0 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt
@@ -33,12 +33,13 @@
     fun generate(annotatedInterface: AnnotatedInterface): FileSpec {
         val annotatedInterfaceType =
             TypeSpec.interfaceBuilder(annotatedInterface.type.poetClassName()).build {
+                addSuperinterfaces(annotatedInterface.superTypes.map { it.poetClassName() })
                 addFunctions(annotatedInterface.methods.map(::generateInterfaceMethod))
             }
 
         return FileSpec.get(annotatedInterface.type.packageName, annotatedInterfaceType)
             .toBuilder().build {
-            addCommonSettings()
+                addCommonSettings()
         }
     }
 
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt
index b756b24..d655f8d 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt
@@ -22,8 +22,8 @@
 import androidx.privacysandbox.tools.PrivacySandboxValue
 import java.nio.file.Path
 import kotlinx.metadata.KmClass
-import kotlinx.metadata.jvm.KotlinClassHeader
 import kotlinx.metadata.jvm.KotlinClassMetadata
+import kotlinx.metadata.jvm.Metadata
 import org.objectweb.asm.ClassReader
 import org.objectweb.asm.Opcodes
 import org.objectweb.asm.Type
@@ -91,7 +91,7 @@
         // ASM models annotation attributes as flat List<Objects>, so the unchecked cast is
         // inevitable when some of these objects have type parameters, like the lists below.
         @Suppress("UNCHECKED_CAST")
-        val header = KotlinClassHeader(
+        val metadataAnnotation = Metadata(
             kind = metadataValues["k"] as Int?,
             metadataVersion = (metadataValues["mv"] as? List<Int>?)?.toIntArray(),
             data1 = (metadataValues["d1"] as? List<String>?)?.toTypedArray(),
@@ -101,7 +101,7 @@
             extraString = metadataValues["xs"] as? String?,
         )
 
-        return when (val metadata = KotlinClassMetadata.read(header)) {
+        return when (val metadata = KotlinClassMetadata.read(metadataAnnotation)) {
             is KotlinClassMetadata.Class -> metadata.toKmClass()
             else -> throw PrivacySandboxParsingException(
                 "Unable to parse Kotlin metadata from ${classNode.name}. " +
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
index 6155ff6..f0bb98a 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
@@ -22,6 +22,7 @@
 import androidx.privacysandbox.tools.core.model.Parameter
 import androidx.privacysandbox.tools.core.model.ParsedApi
 import androidx.privacysandbox.tools.core.model.Type
+import androidx.privacysandbox.tools.core.model.Types
 import androidx.privacysandbox.tools.core.model.ValueProperty
 import androidx.privacysandbox.tools.core.validator.ModelValidator
 import java.nio.file.Path
@@ -55,6 +56,7 @@
 
     private fun parseInterface(service: KmClass, annotationName: String): AnnotatedInterface {
         val type = parseClassName(service.name)
+        val superTypes = service.supertypes.map(this::parseType).filterNot { it == Types.any }
 
         if (!Flag.Class.IS_INTERFACE(service.flags)) {
             throw PrivacySandboxParsingException(
@@ -65,7 +67,8 @@
 
         return AnnotatedInterface(
             type = type,
-            service.functions.map(this::parseMethod),
+            superTypes = superTypes,
+            methods = service.functions.map(this::parseMethod),
         )
     }
 
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt
index 860b155..73ce728 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.privacysandbox.tools.core.Metadata
 import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertCompiles
+import androidx.privacysandbox.tools.testing.allTestLibraryStubs
 import androidx.privacysandbox.tools.testing.hasAllExpectedGeneratedSourceFilesAndContent
 import androidx.privacysandbox.tools.testing.loadSourcesFromDirectory
 import androidx.room.compiler.processing.util.Source
@@ -49,7 +50,7 @@
 
     @Test
     fun generatedApi_compiles() {
-        assertCompiles(generatedSources)
+        assertCompiles(generatedSources + allTestLibraryStubs)
     }
 
     @Test
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt
index fade128..ea01aa8 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt
@@ -18,6 +18,7 @@
 
 import androidx.privacysandbox.tools.apipackager.PrivacySandboxApiPackager
 import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertCompiles
+import androidx.privacysandbox.tools.testing.allTestLibraryStubs
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.compiler.TestCompilationResult
 import java.nio.file.Files.createTempDirectory
@@ -31,13 +32,17 @@
  *
  * @param descriptorResources map of extra resources that will be added to descriptors jar keyed by
  *      their relative path.
+ * @param addLibraryStubs whether to include latest Android platform API stubs that support the
+ * Privacy Sandbox.
  */
 fun compileIntoInterfaceDescriptorsJar(
     sources: List<Source>,
     descriptorResources: Map<Path, ByteArray> = mapOf(),
+    addLibraryStubs: Boolean = true,
 ): Path {
+    val testSources = if (addLibraryStubs) sources + allTestLibraryStubs else sources
     val tempDir = createTempDirectory("compile").also { it.toFile().deleteOnExit() }
-    val result = assertCompiles(sources.toList())
+    val result = assertCompiles(testSources.toList())
     val sdkInterfaceDescriptors = tempDir.resolve("sdk-interface-descriptors.jar")
     val outputClasspath = mergedClasspath(result)
     descriptorResources.forEach { (relativePath, contents) ->
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
index efc6bf0..1909e41 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
@@ -27,6 +27,7 @@
 import androidx.privacysandbox.tools.core.model.Types.asNullable
 import androidx.privacysandbox.tools.core.model.ValueProperty
 import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertCompiles
+import androidx.privacysandbox.tools.testing.allTestLibraryStubs
 import androidx.room.compiler.processing.util.Source
 import androidx.testutils.assertThrows
 import com.google.common.truth.Truth.assertThat
@@ -39,34 +40,47 @@
     @Test
     fun annotatedInterface_isParsed() {
         val source = Source.kotlin(
-            "com/mysdk/TestSandboxSdk.kt", """
-                    package com.mysdk
-                    import androidx.privacysandbox.tools.PrivacySandboxCallback
-                    import androidx.privacysandbox.tools.PrivacySandboxInterface
-                    import androidx.privacysandbox.tools.PrivacySandboxService
-                    import androidx.privacysandbox.tools.PrivacySandboxValue
-                    @PrivacySandboxService
-                    interface MySdk {
-                      fun doSomething(magicNumber: Int, awesomeString: String?)
-                      suspend fun getPayload(request: PayloadRequest): PayloadResponse
-                      suspend fun getInterface(): MyInterface
-                      suspend fun processList(list: List<Long>): List<Long>
-                    }
-                    @PrivacySandboxInterface
-                    interface MyInterface {
-                      suspend fun getMorePayload(request: PayloadRequest): PayloadResponse
-                    }
-                    @PrivacySandboxValue
-                    data class PayloadType(val size: Long, val appId: String)
-                    @PrivacySandboxValue
-                    data class PayloadResponse(val url: String)
-                    @PrivacySandboxValue
-                    data class PayloadRequest(val type: PayloadType)
-                    @PrivacySandboxCallback
-                    interface CustomCallback {
-                      fun onComplete(status: Int)
-                    }
-                """,
+            "com/mysdk/TestSandboxSdk.kt",
+            """
+                    |package com.mysdk
+                    |
+                    |import androidx.privacysandbox.tools.PrivacySandboxCallback
+                    |import androidx.privacysandbox.tools.PrivacySandboxInterface
+                    |import androidx.privacysandbox.tools.PrivacySandboxService
+                    |import androidx.privacysandbox.tools.PrivacySandboxValue
+                    |import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+                    |
+                    |@PrivacySandboxService
+                    |interface MySdk {
+                    |  fun doSomething(magicNumber: Int, awesomeString: String?)
+                    |  suspend fun getPayload(request: PayloadRequest): PayloadResponse
+                    |  suspend fun getInterface(): MyInterface
+                    |  suspend fun getUiInterface(): MyUiInterface
+                    |  suspend fun processList(list: List<Long>): List<Long>
+                    |}
+                    |
+                    |@PrivacySandboxInterface
+                    |interface MyInterface {
+                    |  suspend fun getMorePayload(request: PayloadRequest): PayloadResponse
+                    |}
+                    |
+                    |@PrivacySandboxInterface
+                    |interface MyUiInterface : SandboxedUiAdapter {}
+                    |
+                    |@PrivacySandboxValue
+                    |data class PayloadType(val size: Long, val appId: String)
+                    |
+                    |@PrivacySandboxValue
+                    |data class PayloadResponse(val url: String)
+                    |
+                    |@PrivacySandboxValue
+                    |data class PayloadRequest(val type: PayloadType)
+                    |
+                    |@PrivacySandboxCallback
+                    |interface CustomCallback {
+                    |  fun onComplete(status: Int)
+                    |}
+                """.trimMargin(),
         )
 
         val expectedPayloadType = AnnotatedValue(
@@ -122,6 +136,12 @@
                         isSuspend = true,
                     ),
                     Method(
+                        name = "getUiInterface",
+                        parameters = listOf(),
+                        returnType = Type("com.mysdk", "MyUiInterface"),
+                        isSuspend = true,
+                    ),
+                    Method(
                         name = "processList",
                         parameters = listOf(
                             Parameter(
@@ -134,7 +154,7 @@
                     ),
                 )
             )
-        val expectedInterface =
+        val expectedInterfaces = listOf(
             AnnotatedInterface(
                 type = Type(packageName = "com.mysdk", simpleName = "MyInterface"),
                 methods = listOf(
@@ -150,7 +170,13 @@
                         isSuspend = true,
                     )
                 )
-            )
+            ),
+            AnnotatedInterface(
+                type = Type(packageName = "com.mysdk", simpleName = "MyUiInterface"),
+                superTypes = listOf(Types.sandboxedUiAdapter),
+                methods = listOf(),
+            ),
+        )
         val expectedCallback = AnnotatedInterface(
             type = Type(packageName = "com.mysdk", simpleName = "CustomCallback"),
                 methods = listOf(
@@ -176,7 +202,7 @@
             expectedPayloadResponse,
         )
         assertThat(actualApi.callbacks).containsExactly(expectedCallback)
-        assertThat(actualApi.interfaces).containsExactly(expectedInterface)
+        assertThat(actualApi.interfaces).containsExactlyElementsIn(expectedInterfaces)
     }
 
     @Test
@@ -378,7 +404,7 @@
     }
 
     private fun compileAndParseApi(vararg sources: Source): ParsedApi {
-        val classpath = mergedClasspath(assertCompiles(sources.toList()))
+        val classpath = mergedClasspath(assertCompiles(sources.toList() + allTestLibraryStubs))
         return ApiStubParser.parse(classpath)
     }
 }
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/input/com/sdk/MySdk.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/input/com/sdk/MySdk.kt
index 8ace244..b64faf5 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/input/com/sdk/MySdk.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/input/com/sdk/MySdk.kt
@@ -2,6 +2,7 @@
 
 import androidx.privacysandbox.tools.PrivacySandboxInterface
 import androidx.privacysandbox.tools.PrivacySandboxService
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
 
 @PrivacySandboxService
 interface MySdk {
@@ -20,6 +21,6 @@
 }
 
 @PrivacySandboxInterface
-interface MySecondInterface {
+interface MySecondInterface : SandboxedUiAdapter {
    fun doStuff()
 }
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterface.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterface.kt
index b858ed5..0505e08 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterface.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterface.kt
@@ -1,5 +1,7 @@
 package com.sdk
 
-public interface MySecondInterface {
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+
+public interface MySecondInterface : SandboxedUiAdapter {
     public fun doStuff(): Unit
 }
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterfaceClientProxy.kt
index 67e7d28a..9880002 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterfaceClientProxy.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterfaceClientProxy.kt
@@ -1,9 +1,27 @@
 package com.sdk
 
+import android.content.Context
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter.SessionClient
+import java.util.concurrent.Executor
+
 public class MySecondInterfaceClientProxy(
     public val remote: IMySecondInterface,
+    public val sandboxedUiAdapter: SandboxedUiAdapter,
 ) : MySecondInterface {
     public override fun doStuff(): Unit {
         remote.doStuff()
     }
+
+    public override fun openSession(
+        context: Context,
+        initialWidth: Int,
+        initialHeight: Int,
+        isZOrderOnTop: Boolean,
+        clientExecutor: Executor,
+        client: SandboxedUiAdapter.SessionClient,
+    ): Unit {
+        sandboxedUiAdapter.openSession(context, initialWidth, initialHeight, isZOrderOnTop,
+                clientExecutor, client)
+    }
 }
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
index 43b9abe..49d0853 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
@@ -21,11 +21,13 @@
 import androidx.privacysandbox.tools.core.generator.SpecNames.suspendCancellableCoroutineMethod
 import androidx.privacysandbox.tools.core.model.AnnotatedInterface
 import androidx.privacysandbox.tools.core.model.Method
+import androidx.privacysandbox.tools.core.model.Types
 import com.squareup.kotlinpoet.ClassName
 import com.squareup.kotlinpoet.CodeBlock
 import com.squareup.kotlinpoet.FileSpec
 import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.KModifier
+import com.squareup.kotlinpoet.ParameterSpec
 import com.squareup.kotlinpoet.PropertySpec
 import com.squareup.kotlinpoet.TypeSpec
 import com.squareup.kotlinpoet.joinToCode
@@ -34,24 +36,34 @@
     private val basePackageName: String,
     private val binderCodeConverter: BinderCodeConverter
 ) {
-    private val cancellationSignalClassName =
-        ClassName(basePackageName, "ICancellationSignal")
+    private val cancellationSignalClassName = ClassName(basePackageName, "ICancellationSignal")
+    private val sandboxedUiAdapterPropertyName = "sandboxedUiAdapter"
 
     fun generate(annotatedInterface: AnnotatedInterface): FileSpec {
         val className = annotatedInterface.clientProxyNameSpec().simpleName
         val remoteBinderClassName = annotatedInterface.aidlType().innerType.poetTypeName()
+        val inheritsUiAdapter = annotatedInterface.superTypes.contains(Types.sandboxedUiAdapter)
 
         val classSpec = TypeSpec.classBuilder(className).build {
             addSuperinterface(annotatedInterface.type.poetTypeName())
 
-            primaryConstructor(
-                listOf(
+            primaryConstructor(buildList {
+                add(
                     PropertySpec.builder("remote", remoteBinderClassName)
                         .addModifiers(KModifier.PUBLIC).build()
                 )
-            )
+                if (inheritsUiAdapter) add(
+                    PropertySpec.builder(
+                        sandboxedUiAdapterPropertyName, Types.sandboxedUiAdapter.poetTypeName()
+                    ).addModifiers(KModifier.PUBLIC).build()
+                )
+            })
 
             addFunctions(annotatedInterface.methods.map(::toFunSpec))
+
+            if (inheritsUiAdapter) {
+                addFunction(generateOpenSession())
+            }
         }
 
         return FileSpec.builder(annotatedInterface.type.packageName, className).build {
@@ -94,6 +106,28 @@
             addCode(generateRemoteCall(method))
         }
 
+    private fun generateOpenSession() = FunSpec.builder("openSession").build {
+        addModifiers(KModifier.OVERRIDE)
+        addParameters(
+            listOf(
+                ParameterSpec("context", ClassName("android.content", "Context")),
+                ParameterSpec("initialWidth", Types.int.poetClassName()),
+                ParameterSpec("initialHeight", Types.int.poetClassName()),
+                ParameterSpec("isZOrderOnTop", Types.boolean.poetClassName()),
+                ParameterSpec("clientExecutor", ClassName("java.util.concurrent", "Executor")),
+                ParameterSpec(
+                    "client", ClassName(
+                        "androidx.privacysandbox.ui.core", "SandboxedUiAdapter.SessionClient"
+                    )
+                ),
+            )
+        )
+        addStatement(
+            "$sandboxedUiAdapterPropertyName.openSession(context, initialWidth, initialHeight, " +
+                "isZOrderOnTop, clientExecutor, client)"
+        )
+    }
+
     private fun generateTransactionCallbackObject(method: Method) = CodeBlock.builder().build {
         val transactionCallbackClassName = ClassName(
             basePackageName,
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/AnnotatedInterface.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/AnnotatedInterface.kt
index b70b515..070df87 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/AnnotatedInterface.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/AnnotatedInterface.kt
@@ -19,5 +19,11 @@
 /** Result of parsing a Kotlin interface. */
 data class AnnotatedInterface(
     val type: Type,
+    /**
+     * Direct super types of this interface.
+     *
+     * When there are no explicit parents, the list should be empty (ie. not containing kotlin.Any).
+     */
+    val superTypes: List<Type> = emptyList(),
     val methods: List<Method> = emptyList(),
 )
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
index a30e809..88245e9 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
@@ -41,6 +41,11 @@
     val char = Type(packageName = "kotlin", simpleName = "Char")
     val short = Type(packageName = "kotlin", simpleName = "Short")
     val primitiveTypes = setOf(unit, boolean, int, long, float, double, string, char, short)
+
+    val any = Type("kotlin", simpleName = "Any")
+    val sandboxedUiAdapter =
+        Type(packageName = "androidx.privacysandbox.ui.core", simpleName = "SandboxedUiAdapter")
+
     fun list(elementType: Type) = Type(
         packageName = "kotlin.collections",
         simpleName = "List",
diff --git a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/LibraryStubs.kt b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/LibraryStubs.kt
new file mode 100644
index 0000000..b51203a
--- /dev/null
+++ b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/LibraryStubs.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.tools.testing
+
+import androidx.room.compiler.processing.util.Source
+
+// SDK Runtime library is not available in AndroidX prebuilts, so while that's the case we use fake
+// stubs to run our compilation tests.
+val syntheticSdkRuntimeLibraryStubs = listOf(
+    Source.kotlin(
+        "androidx/privacysandbox/sdkruntime/core/SandboxedSdkCompat.kt", """
+        |package androidx.privacysandbox.sdkruntime.core
+        |
+        |import android.os.IBinder
+        |
+        |@Suppress("UNUSED_PARAMETER")
+        |class SandboxedSdkCompat(sdkInterface: IBinder) {
+        |    fun getInterface(): IBinder? = throw RuntimeException("Stub!")
+        |}
+        |""".trimMargin()
+    ),
+    Source.kotlin(
+        "androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderCompat.kt", """
+        |package androidx.privacysandbox.sdkruntime.core
+        |
+        |import android.content.Context
+        |import android.os.Bundle
+        |import android.view.View
+        |
+        |@Suppress("UNUSED_PARAMETER")
+        |abstract class SandboxedSdkProviderCompat {
+        |   var context: Context? = null
+        |       private set
+        |   fun attachContext(context: Context): Unit = throw RuntimeException("Stub!")
+        |
+        |   abstract fun onLoadSdk(params: Bundle): SandboxedSdkCompat
+        |
+        |   open fun beforeUnloadSdk() {}
+        |
+        |   abstract fun getView(
+        |       windowContext: Context,
+        |       params: Bundle,
+        |       width: Int,
+        |       height: Int
+        |   ): View
+        |}
+        |""".trimMargin()
+    ),
+)
+
+val syntheticUiLibraryStubs = listOf(
+    Source.kotlin(
+        "androidx/privacysandbox/ui/core/SandboxedUiAdapter.kt", """
+        |package androidx.privacysandbox.ui.core
+        |
+        |import android.content.Context
+        |import android.view.View
+        |import java.util.concurrent.Executor
+        |
+        |interface SandboxedUiAdapter {
+        |  fun openSession(
+        |      context: Context,
+        |      initialWidth: Int,
+        |      initialHeight: Int,
+        |      isZOrderOnTop: Boolean,
+        |      clientExecutor: Executor,
+        |      client: SessionClient
+        |  )
+        |
+        |
+        |  interface Session {
+        |    fun close()
+        |    val view: View
+        |  }
+        |
+        |  interface SessionClient {
+        |    fun onSessionError(throwable: Throwable);
+        |    fun onSessionOpened(session: Session);
+        |  }
+        |}
+        |""".trimMargin()
+    ),
+    Source.kotlin(
+        "androidx/privacysandbox/ui/core/SdkRuntimeUiLibVersions.kt", """
+        |package androidx.privacysandbox.ui.core
+        |
+        |import androidx.annotation.RestrictTo
+        |
+        |object SdkRuntimeUiLibVersions {
+        |    var clientVersion: Int = -1
+        |        /**
+        |         * @hide
+        |         */
+        |        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        |        set
+        |
+        |    const val apiVersion: Int = 1
+        |}
+        |""".trimMargin()
+    ),
+)
+
+val allTestLibraryStubs = syntheticSdkRuntimeLibraryStubs + syntheticUiLibraryStubs
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-client/api/current.txt b/privacysandbox/ui/ui-client/api/current.txt
index e6f50d0..e6beb91 100644
--- a/privacysandbox/ui/ui-client/api/current.txt
+++ b/privacysandbox/ui/ui-client/api/current.txt
@@ -1 +1,10 @@
 // Signature format: 4.0
+package androidx.privacysandbox.ui.client {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class SandboxedUiAdapterFactory {
+    method public androidx.privacysandbox.ui.core.SandboxedUiAdapter createFromCoreLibInfo(android.os.Bundle coreLibInfo);
+    field public static final androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory INSTANCE;
+  }
+
+}
+
diff --git a/privacysandbox/ui/ui-client/api/public_plus_experimental_current.txt b/privacysandbox/ui/ui-client/api/public_plus_experimental_current.txt
index e6f50d0..e6beb91 100644
--- a/privacysandbox/ui/ui-client/api/public_plus_experimental_current.txt
+++ b/privacysandbox/ui/ui-client/api/public_plus_experimental_current.txt
@@ -1 +1,10 @@
 // Signature format: 4.0
+package androidx.privacysandbox.ui.client {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class SandboxedUiAdapterFactory {
+    method public androidx.privacysandbox.ui.core.SandboxedUiAdapter createFromCoreLibInfo(android.os.Bundle coreLibInfo);
+    field public static final androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory INSTANCE;
+  }
+
+}
+
diff --git a/privacysandbox/ui/ui-client/api/restricted_current.txt b/privacysandbox/ui/ui-client/api/restricted_current.txt
index e6f50d0..e6beb91 100644
--- a/privacysandbox/ui/ui-client/api/restricted_current.txt
+++ b/privacysandbox/ui/ui-client/api/restricted_current.txt
@@ -1 +1,10 @@
 // Signature format: 4.0
+package androidx.privacysandbox.ui.client {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class SandboxedUiAdapterFactory {
+    method public androidx.privacysandbox.ui.core.SandboxedUiAdapter createFromCoreLibInfo(android.os.Bundle coreLibInfo);
+    field public static final androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory INSTANCE;
+  }
+
+}
+
diff --git a/privacysandbox/ui/ui-client/build.gradle b/privacysandbox/ui/ui-client/build.gradle
index 82beabb..918c043 100644
--- a/privacysandbox/ui/ui-client/build.gradle
+++ b/privacysandbox/ui/ui-client/build.gradle
@@ -24,7 +24,8 @@
 
 dependencies {
     api(libs.kotlinStdlib)
-    // Add dependencies here
+    implementation project(path: ':privacysandbox:ui:ui-core')
+    implementation project(path: ':annotation:annotation')
 }
 
 android {
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
new file mode 100644
index 0000000..f87ab3d
--- /dev/null
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.ui.client
+
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.os.Binder
+import android.os.Build
+import android.os.Bundle
+import android.view.Display
+import android.view.SurfaceControlViewHost
+import android.view.SurfaceView
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.privacysandbox.ui.core.IRemoteSessionClient
+import androidx.privacysandbox.ui.core.IRemoteSessionController
+import androidx.privacysandbox.ui.core.ISandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import java.util.concurrent.Executor
+
+/**
+ * Provides an adapter created from the supplied Bundle which acts as a proxy between the host app
+ * and Binder provided by the provider of content.
+ * @throws IllegalArgumentException if CoreLibInfo does not contain a Binder with the key
+ * uiAdapterBinder
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+object SandboxedUiAdapterFactory {
+    // Bundle key is a binary compatibility requirement
+    private const val UI_ADAPTER_BINDER = "uiAdapterBinder"
+    fun createFromCoreLibInfo(coreLibInfo: Bundle): SandboxedUiAdapter {
+        val uiAdapterBinder = requireNotNull(coreLibInfo.getBinder(UI_ADAPTER_BINDER)) {
+            "Invalid CoreLibInfo bundle, missing $UI_ADAPTER_BINDER."
+        }
+        val adapterInterface = ISandboxedUiAdapter.Stub.asInterface(
+            uiAdapterBinder
+        )
+        return RemoteAdapter(adapterInterface)
+    }
+
+    private class RemoteAdapter(private val adapterInterface: ISandboxedUiAdapter) :
+        SandboxedUiAdapter {
+
+        override fun openSession(
+            context: Context,
+            initialWidth: Int,
+            initialHeight: Int,
+            isZOrderOnTop: Boolean,
+            clientExecutor: Executor,
+            client: SandboxedUiAdapter.SessionClient
+        ) {
+            val mDisplayManager =
+                context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+            val displayId = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY).displayId
+
+            adapterInterface.openRemoteSession(
+                Binder(), // Host Token
+                displayId,
+                initialWidth,
+                initialHeight,
+                isZOrderOnTop,
+                RemoteSessionClient(context, client, clientExecutor)
+            )
+        }
+
+        class RemoteSessionClient(
+            val context: Context,
+            val client: SandboxedUiAdapter.SessionClient,
+            val clientExecutor: Executor
+        ) : IRemoteSessionClient.Stub() {
+
+            override fun onRemoteSessionOpened(
+                surfacePackage: SurfaceControlViewHost.SurfacePackage,
+                remoteSessionController: IRemoteSessionController,
+                isZOrderOnTop: Boolean
+            ) {
+                val surfaceView = SurfaceView(context)
+                surfaceView.setChildSurfacePackage(surfacePackage)
+                surfaceView.setZOrderOnTop(isZOrderOnTop)
+
+                clientExecutor.execute {
+                    client.onSessionOpened(SessionImpl(surfaceView, remoteSessionController))
+                }
+            }
+
+            override fun onRemoteSessionError(errorString: String) {
+                clientExecutor.execute {
+                    client.onSessionError(Throwable(errorString))
+                }
+            }
+        }
+
+        private class SessionImpl(
+            val surfaceView: SurfaceView,
+            val remoteSessionController: IRemoteSessionController
+        ) : SandboxedUiAdapter.Session {
+
+            override val view: View = surfaceView
+
+            override fun close() {
+                remoteSessionController.close()
+            }
+        }
+    }
+}
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-client-documentation.md b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/androidx-privacysandbox-ui-ui-client-documentation.md
similarity index 100%
rename from privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-client-documentation.md
rename to privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/androidx-privacysandbox-ui-ui-client-documentation.md
diff --git a/privacysandbox/ui/ui-core/api/current.txt b/privacysandbox/ui/ui-core/api/current.txt
index e6f50d0..3ab33d6 100644
--- a/privacysandbox/ui/ui-core/api/current.txt
+++ b/privacysandbox/ui/ui-core/api/current.txt
@@ -1 +1,27 @@
 // Signature format: 4.0
+package androidx.privacysandbox.ui.core {
+
+  public interface SandboxedUiAdapter {
+    method public void openSession(android.content.Context context, int initialWidth, int initialHeight, boolean isZOrderOnTop, java.util.concurrent.Executor clientExecutor, androidx.privacysandbox.ui.core.SandboxedUiAdapter.SessionClient client);
+  }
+
+  public static interface SandboxedUiAdapter.Session {
+    method public void close();
+    method public android.view.View getView();
+    property public abstract android.view.View view;
+  }
+
+  public static interface SandboxedUiAdapter.SessionClient {
+    method public void onSessionError(Throwable throwable);
+    method public void onSessionOpened(androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session session);
+  }
+
+  public final class SdkRuntimeUiLibVersions {
+    method public int getClientVersion();
+    property public final int clientVersion;
+    field public static final androidx.privacysandbox.ui.core.SdkRuntimeUiLibVersions INSTANCE;
+    field public static final int apiVersion = 1; // 0x1
+  }
+
+}
+
diff --git a/privacysandbox/ui/ui-core/api/public_plus_experimental_current.txt b/privacysandbox/ui/ui-core/api/public_plus_experimental_current.txt
index e6f50d0..3ab33d6 100644
--- a/privacysandbox/ui/ui-core/api/public_plus_experimental_current.txt
+++ b/privacysandbox/ui/ui-core/api/public_plus_experimental_current.txt
@@ -1 +1,27 @@
 // Signature format: 4.0
+package androidx.privacysandbox.ui.core {
+
+  public interface SandboxedUiAdapter {
+    method public void openSession(android.content.Context context, int initialWidth, int initialHeight, boolean isZOrderOnTop, java.util.concurrent.Executor clientExecutor, androidx.privacysandbox.ui.core.SandboxedUiAdapter.SessionClient client);
+  }
+
+  public static interface SandboxedUiAdapter.Session {
+    method public void close();
+    method public android.view.View getView();
+    property public abstract android.view.View view;
+  }
+
+  public static interface SandboxedUiAdapter.SessionClient {
+    method public void onSessionError(Throwable throwable);
+    method public void onSessionOpened(androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session session);
+  }
+
+  public final class SdkRuntimeUiLibVersions {
+    method public int getClientVersion();
+    property public final int clientVersion;
+    field public static final androidx.privacysandbox.ui.core.SdkRuntimeUiLibVersions INSTANCE;
+    field public static final int apiVersion = 1; // 0x1
+  }
+
+}
+
diff --git a/privacysandbox/ui/ui-core/api/restricted_current.txt b/privacysandbox/ui/ui-core/api/restricted_current.txt
index e6f50d0..3ab33d6 100644
--- a/privacysandbox/ui/ui-core/api/restricted_current.txt
+++ b/privacysandbox/ui/ui-core/api/restricted_current.txt
@@ -1 +1,27 @@
 // Signature format: 4.0
+package androidx.privacysandbox.ui.core {
+
+  public interface SandboxedUiAdapter {
+    method public void openSession(android.content.Context context, int initialWidth, int initialHeight, boolean isZOrderOnTop, java.util.concurrent.Executor clientExecutor, androidx.privacysandbox.ui.core.SandboxedUiAdapter.SessionClient client);
+  }
+
+  public static interface SandboxedUiAdapter.Session {
+    method public void close();
+    method public android.view.View getView();
+    property public abstract android.view.View view;
+  }
+
+  public static interface SandboxedUiAdapter.SessionClient {
+    method public void onSessionError(Throwable throwable);
+    method public void onSessionOpened(androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session session);
+  }
+
+  public final class SdkRuntimeUiLibVersions {
+    method public int getClientVersion();
+    property public final int clientVersion;
+    field public static final androidx.privacysandbox.ui.core.SdkRuntimeUiLibVersions INSTANCE;
+    field public static final int apiVersion = 1; // 0x1
+  }
+
+}
+
diff --git a/privacysandbox/ui/ui-core/build.gradle b/privacysandbox/ui/ui-core/build.gradle
index 4e0e9bd..f0b88cd 100644
--- a/privacysandbox/ui/ui-core/build.gradle
+++ b/privacysandbox/ui/ui-core/build.gradle
@@ -24,11 +24,21 @@
 
 dependencies {
     api(libs.kotlinStdlib)
-    // Add dependencies here
+    implementation 'androidx.annotation:annotation:1.5.0'
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(libs.kotlinStdlib)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.truth)
 }
 
 android {
     namespace "androidx.privacysandbox.ui.core"
+    buildFeatures {
+        aidl = true
+    }
 }
 
 androidx {
diff --git a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionClient.aidl b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionClient.aidl
new file mode 100644
index 0000000..63a66c1
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionClient.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.core;
+
+import androidx.privacysandbox.ui.core.IRemoteSessionController;
+import android.view.SurfaceControlViewHost.SurfacePackage;
+
+/** @hide */
+oneway interface IRemoteSessionClient {
+    void onRemoteSessionOpened(in SurfacePackage surfacePackage,
+        IRemoteSessionController remoteSessionController, boolean isZOrderOnTop);
+    void onRemoteSessionError(String exception);
+}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl
new file mode 100644
index 0000000..b64a992
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.core;
+
+/** @hide */
+oneway interface IRemoteSessionController {
+    void close();
+}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/ISandboxedUiAdapter.aidl b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/ISandboxedUiAdapter.aidl
new file mode 100644
index 0000000..a452da6
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/ISandboxedUiAdapter.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.core;
+
+import androidx.privacysandbox.ui.core.IRemoteSessionClient;
+import android.content.Context;
+
+/** @hide */
+oneway interface ISandboxedUiAdapter {
+    void openRemoteSession(
+        IBinder hostToken, int displayId, int initialWidth, int initialHeight, boolean isZOrderOnTop,
+        IRemoteSessionClient remoteSessionClient);
+}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-core-documentation.md b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-core-documentation.md
deleted file mode 100644
index aa56a66..0000000
--- a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-core-documentation.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Module root
-
-Privacy Sandbox Ui Core
-
-# Package androidx.privacysandbox.ui.core
-This package contains interface and class definitions shared between Privacy
-Sandbox Ui Client and Privacy Sandbox Ui Provider.
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedUiAdapter.kt b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedUiAdapter.kt
new file mode 100644
index 0000000..c6751e3
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedUiAdapter.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.core
+
+import android.content.Context
+import android.view.View
+import java.util.concurrent.Executor
+
+/**
+ * An Adapter that provides content from a SandboxedSdk to be displayed as part of a host app's UI.
+ */
+
+interface SandboxedUiAdapter {
+
+    /**
+     * Open a new session for displaying content with an initial size of
+     * [initialWidth]x[initialHeight] pixels. [client] will receive all incoming communication from
+     * the provider of content. All incoming calls to [client] will be made through the provided
+     * [clientExecutor]. [isZOrderOnTop] tracks if the content surface will be placed on top of its
+     * window
+     */
+    fun openSession(
+        context: Context,
+        initialWidth: Int,
+        initialHeight: Int,
+        isZOrderOnTop: Boolean,
+        clientExecutor: Executor,
+        client: SessionClient
+    )
+
+    /**
+     * A single session with the provider of remote content.
+     */
+    interface Session {
+
+        /**
+         * Return the [View] that presents content for this session. The same view will be returned
+         * for the life of the session object. Accessing [view] after [close] may throw an
+         * [IllegalStateException].
+         */
+        val view: View
+
+        /**
+         * Close this session, indicating that the remote provider of content should
+         * dispose of associated resources and that the [SessionClient] should not
+         * receive further callback events.
+         */
+        fun close()
+    }
+
+    /**
+     * The client of a single session that will receive callback events from an active session.
+     */
+    interface SessionClient {
+        /**
+         * Called to report that the session was opened successfully, delivering the [Session]
+         * handle that should be used to notify the session of UI events.
+         */
+        fun onSessionOpened(session: Session)
+
+        /**
+         * Called to report a terminal error in the session. No further events will be reported
+         * to this [SessionClient] and any further or currently pending calls to the [Session]
+         * that may have been in flight may be ignored.
+         */
+        fun onSessionError(throwable: Throwable)
+    }
+}
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SdkRuntimeUiLibVersions.kt b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SdkRuntimeUiLibVersions.kt
new file mode 100644
index 0000000..ce0f622
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SdkRuntimeUiLibVersions.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.ui.core
+
+import androidx.annotation.RestrictTo
+
+object SdkRuntimeUiLibVersions {
+    var clientVersion: Int = -1
+        /**
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        set
+
+    const val apiVersion: Int = 1
+}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/androidx-privacysandbox-ui-ui-core-documentation.md b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/androidx-privacysandbox-ui-ui-core-documentation.md
new file mode 100644
index 0000000..df34ca3
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/androidx-privacysandbox-ui-ui-core-documentation.md
@@ -0,0 +1,8 @@
+# Module root
+
+Privacy Sandbox Ui Core
+
+# Package androidx.privacysandbox.ui.core
+
+This package contains interface and class definitions shared between Privacy
+Sandbox Ui Client and Privacy Sandbox Ui Provider.
diff --git a/privacysandbox/ui/ui-provider/api/current.txt b/privacysandbox/ui/ui-provider/api/current.txt
index e6f50d0..20170b4 100644
--- a/privacysandbox/ui/ui-provider/api/current.txt
+++ b/privacysandbox/ui/ui-provider/api/current.txt
@@ -1 +1,9 @@
 // Signature format: 4.0
+package androidx.privacysandbox.ui.provider {
+
+  @RequiresApi(33) public final class SandboxedUiAdapterProxy {
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static android.os.Bundle toCoreLibInfo(androidx.privacysandbox.ui.core.SandboxedUiAdapter, android.content.Context context);
+  }
+
+}
+
diff --git a/privacysandbox/ui/ui-provider/api/public_plus_experimental_current.txt b/privacysandbox/ui/ui-provider/api/public_plus_experimental_current.txt
index e6f50d0..20170b4 100644
--- a/privacysandbox/ui/ui-provider/api/public_plus_experimental_current.txt
+++ b/privacysandbox/ui/ui-provider/api/public_plus_experimental_current.txt
@@ -1 +1,9 @@
 // Signature format: 4.0
+package androidx.privacysandbox.ui.provider {
+
+  @RequiresApi(33) public final class SandboxedUiAdapterProxy {
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static android.os.Bundle toCoreLibInfo(androidx.privacysandbox.ui.core.SandboxedUiAdapter, android.content.Context context);
+  }
+
+}
+
diff --git a/privacysandbox/ui/ui-provider/api/restricted_current.txt b/privacysandbox/ui/ui-provider/api/restricted_current.txt
index e6f50d0..20170b4 100644
--- a/privacysandbox/ui/ui-provider/api/restricted_current.txt
+++ b/privacysandbox/ui/ui-provider/api/restricted_current.txt
@@ -1 +1,9 @@
 // Signature format: 4.0
+package androidx.privacysandbox.ui.provider {
+
+  @RequiresApi(33) public final class SandboxedUiAdapterProxy {
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static android.os.Bundle toCoreLibInfo(androidx.privacysandbox.ui.core.SandboxedUiAdapter, android.content.Context context);
+  }
+
+}
+
diff --git a/privacysandbox/ui/ui-provider/build.gradle b/privacysandbox/ui/ui-provider/build.gradle
index 6b2b1324..bcbd719 100644
--- a/privacysandbox/ui/ui-provider/build.gradle
+++ b/privacysandbox/ui/ui-provider/build.gradle
@@ -24,7 +24,15 @@
 
 dependencies {
     api(libs.kotlinStdlib)
-    // Add dependencies here
+    implementation project(path: ':privacysandbox:ui:ui-core')
+    implementation project(path: ':annotation:annotation')
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(libs.kotlinStdlib)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.truth)
 }
 
 android {
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
new file mode 100644
index 0000000..84149c0
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.
+ */
+@file:RequiresApi(Build.VERSION_CODES.TIRAMISU)
+@file:JvmName("SandboxedUiAdapterProxy")
+
+package androidx.privacysandbox.ui.provider
+
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.IBinder
+import android.os.Looper
+import android.view.SurfaceControlViewHost
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.privacysandbox.ui.core.IRemoteSessionClient
+import androidx.privacysandbox.ui.core.IRemoteSessionController
+import androidx.privacysandbox.ui.core.ISandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import java.util.concurrent.Executor
+
+/**
+ * Provides a [Bundle] containing a Binder which represents a [SandboxedUiAdapter]. The Bundle
+ * is shuttled to the host app in order for the [SandboxedUiAdapter] to be used to retrieve
+ * content.
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+fun SandboxedUiAdapter.toCoreLibInfo(@Suppress("ContextFirst") context: Context): Bundle {
+    val binderAdapter = BinderAdapterDelegate(context, this)
+    // TODO: Add version info
+    val bundle = Bundle()
+    // Bundle key is a binary compatibility requirement
+    bundle.putBinder("uiAdapterBinder", binderAdapter)
+    return bundle
+}
+
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+private class BinderAdapterDelegate(
+    private val sandboxContext: Context,
+    private val adapter: SandboxedUiAdapter
+) : ISandboxedUiAdapter.Stub() {
+
+    fun openSession(
+        context: Context,
+        initialWidth: Int,
+        initialHeight: Int,
+        isZOrderOnTop: Boolean,
+        clientExecutor: Executor,
+        client: SandboxedUiAdapter.SessionClient
+    ) {
+        adapter.openSession(
+            context, initialWidth, initialHeight, isZOrderOnTop, clientExecutor,
+            client
+        )
+    }
+
+    override fun openRemoteSession(
+        hostToken: IBinder,
+        displayId: Int,
+        initialWidth: Int,
+        initialHeight: Int,
+        isZOrderOnTop: Boolean,
+        remoteSessionClient: IRemoteSessionClient
+    ) {
+        val mHandler = Handler(Looper.getMainLooper())
+        mHandler.post {
+            try {
+                val mDisplayManager: DisplayManager =
+                    sandboxContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+                val windowContext =
+                    sandboxContext.createDisplayContext(mDisplayManager.getDisplay(displayId))
+                val surfaceControlViewHost = SurfaceControlViewHost(
+                    windowContext,
+                    mDisplayManager.getDisplay(displayId), hostToken
+                )
+                val sessionClient = SessionClientProxy(
+                    surfaceControlViewHost, initialWidth, initialHeight, remoteSessionClient
+                )
+                openSession(
+                    windowContext, initialWidth, initialHeight, isZOrderOnTop,
+                    Runnable::run, sessionClient
+                )
+            } catch (exception: Throwable) {
+                remoteSessionClient.onRemoteSessionError(exception.message)
+            }
+        }
+    }
+
+    private inner class SessionClientProxy(
+        private val surfaceControlViewHost: SurfaceControlViewHost,
+        private val initialWidth: Int,
+        private val initialHeight: Int,
+        private val remoteSessionClient: IRemoteSessionClient
+    ) : SandboxedUiAdapter.SessionClient {
+
+        override fun onSessionOpened(session: SandboxedUiAdapter.Session) {
+            val view = session.view
+            surfaceControlViewHost.setView(view, initialWidth, initialHeight)
+            val surfacePackage = surfaceControlViewHost.surfacePackage
+            val remoteSessionController =
+                RemoteSessionController(surfaceControlViewHost, session)
+            remoteSessionClient.onRemoteSessionOpened(
+                surfacePackage, remoteSessionController,
+                /* isZOrderOnTop= */ true
+            )
+        }
+
+        override fun onSessionError(throwable: Throwable) {
+            remoteSessionClient.onRemoteSessionError(throwable.message)
+        }
+
+        @VisibleForTesting
+        private inner class RemoteSessionController(
+            val surfaceControlViewHost: SurfaceControlViewHost,
+            val session: SandboxedUiAdapter.Session
+        ) : IRemoteSessionController.Stub() {
+            override fun close() {
+                session.close()
+                surfaceControlViewHost.release()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-provider-documentation.md b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/androidx-privacysandbox-ui-ui-provider-documentation.md
similarity index 100%
rename from privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-provider-documentation.md
rename to privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/androidx-privacysandbox-ui-ui-provider-documentation.md
diff --git a/profileinstaller/profileinstaller/api/1.3.0-beta01.txt b/profileinstaller/profileinstaller/api/1.3.0-beta01.txt
new file mode 100644
index 0000000..e7da088
--- /dev/null
+++ b/profileinstaller/profileinstaller/api/1.3.0-beta01.txt
@@ -0,0 +1,74 @@
+// Signature format: 4.0
+package androidx.profileinstaller {
+
+  public class ProfileInstallReceiver extends android.content.BroadcastReceiver {
+    ctor public ProfileInstallReceiver();
+    method public void onReceive(android.content.Context, android.content.Intent?);
+    field public static final String ACTION_BENCHMARK_OPERATION = "androidx.profileinstaller.action.BENCHMARK_OPERATION";
+    field public static final String ACTION_INSTALL_PROFILE = "androidx.profileinstaller.action.INSTALL_PROFILE";
+    field public static final String ACTION_SAVE_PROFILE = "androidx.profileinstaller.action.SAVE_PROFILE";
+    field public static final String ACTION_SKIP_FILE = "androidx.profileinstaller.action.SKIP_FILE";
+  }
+
+  public class ProfileInstaller {
+    method @WorkerThread public static void writeProfile(android.content.Context);
+    method @WorkerThread public static void writeProfile(android.content.Context, java.util.concurrent.Executor, androidx.profileinstaller.ProfileInstaller.DiagnosticsCallback);
+    field public static final int DIAGNOSTIC_CURRENT_PROFILE_DOES_NOT_EXIST = 2; // 0x2
+    field public static final int DIAGNOSTIC_CURRENT_PROFILE_EXISTS = 1; // 0x1
+    field public static final int DIAGNOSTIC_PROFILE_IS_COMPRESSED = 5; // 0x5
+    field public static final int DIAGNOSTIC_REF_PROFILE_DOES_NOT_EXIST = 4; // 0x4
+    field public static final int DIAGNOSTIC_REF_PROFILE_EXISTS = 3; // 0x3
+    field public static final int RESULT_ALREADY_INSTALLED = 2; // 0x2
+    field public static final int RESULT_BASELINE_PROFILE_NOT_FOUND = 6; // 0x6
+    field public static final int RESULT_BENCHMARK_OPERATION_FAILURE = 15; // 0xf
+    field public static final int RESULT_BENCHMARK_OPERATION_SUCCESS = 14; // 0xe
+    field public static final int RESULT_BENCHMARK_OPERATION_UNKNOWN = 16; // 0x10
+    field public static final int RESULT_DELETE_SKIP_FILE_SUCCESS = 11; // 0xb
+    field public static final int RESULT_DESIRED_FORMAT_UNSUPPORTED = 5; // 0x5
+    field public static final int RESULT_INSTALL_SKIP_FILE_SUCCESS = 10; // 0xa
+    field public static final int RESULT_INSTALL_SUCCESS = 1; // 0x1
+    field public static final int RESULT_IO_EXCEPTION = 7; // 0x7
+    field public static final int RESULT_META_FILE_REQUIRED_BUT_NOT_FOUND = 9; // 0x9
+    field public static final int RESULT_NOT_WRITABLE = 4; // 0x4
+    field public static final int RESULT_PARSE_EXCEPTION = 8; // 0x8
+    field public static final int RESULT_SAVE_PROFILE_SIGNALLED = 12; // 0xc
+    field public static final int RESULT_SAVE_PROFILE_SKIPPED = 13; // 0xd
+    field public static final int RESULT_UNSUPPORTED_ART_VERSION = 3; // 0x3
+  }
+
+  public static interface ProfileInstaller.DiagnosticsCallback {
+    method public void onDiagnosticReceived(int, Object?);
+    method public void onResultReceived(int, Object?);
+  }
+
+  public class ProfileInstallerInitializer implements androidx.startup.Initializer<androidx.profileinstaller.ProfileInstallerInitializer.Result> {
+    ctor public ProfileInstallerInitializer();
+    method public androidx.profileinstaller.ProfileInstallerInitializer.Result create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
+  public static class ProfileInstallerInitializer.Result {
+    ctor public ProfileInstallerInitializer.Result();
+  }
+
+  public final class ProfileVerifier {
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.profileinstaller.ProfileVerifier.CompilationStatus!> getCompilationStatusAsync();
+    method @WorkerThread public static androidx.profileinstaller.ProfileVerifier.CompilationStatus writeProfileVerification(android.content.Context);
+  }
+
+  public static class ProfileVerifier.CompilationStatus {
+    method public int getProfileInstallResultCode();
+    method public boolean hasProfileEnqueuedForCompilation();
+    method public boolean isCompiledWithProfile();
+    field public static final int RESULT_CODE_COMPILED_WITH_PROFILE = 1; // 0x1
+    field public static final int RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING = 3; // 0x3
+    field public static final int RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ = 131072; // 0x20000
+    field public static final int RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE = 196608; // 0x30000
+    field public static final int RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST = 65536; // 0x10000
+    field public static final int RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION = 262144; // 0x40000
+    field public static final int RESULT_CODE_NO_PROFILE = 0; // 0x0
+    field public static final int RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION = 2; // 0x2
+  }
+
+}
+
diff --git a/profileinstaller/profileinstaller/api/current.txt b/profileinstaller/profileinstaller/api/current.txt
index 83b7dce..e7da088 100644
--- a/profileinstaller/profileinstaller/api/current.txt
+++ b/profileinstaller/profileinstaller/api/current.txt
@@ -15,6 +15,7 @@
     method @WorkerThread public static void writeProfile(android.content.Context, java.util.concurrent.Executor, androidx.profileinstaller.ProfileInstaller.DiagnosticsCallback);
     field public static final int DIAGNOSTIC_CURRENT_PROFILE_DOES_NOT_EXIST = 2; // 0x2
     field public static final int DIAGNOSTIC_CURRENT_PROFILE_EXISTS = 1; // 0x1
+    field public static final int DIAGNOSTIC_PROFILE_IS_COMPRESSED = 5; // 0x5
     field public static final int DIAGNOSTIC_REF_PROFILE_DOES_NOT_EXIST = 4; // 0x4
     field public static final int DIAGNOSTIC_REF_PROFILE_EXISTS = 3; // 0x3
     field public static final int RESULT_ALREADY_INSTALLED = 2; // 0x2
diff --git a/profileinstaller/profileinstaller/api/public_plus_experimental_1.3.0-beta01.txt b/profileinstaller/profileinstaller/api/public_plus_experimental_1.3.0-beta01.txt
new file mode 100644
index 0000000..e7da088
--- /dev/null
+++ b/profileinstaller/profileinstaller/api/public_plus_experimental_1.3.0-beta01.txt
@@ -0,0 +1,74 @@
+// Signature format: 4.0
+package androidx.profileinstaller {
+
+  public class ProfileInstallReceiver extends android.content.BroadcastReceiver {
+    ctor public ProfileInstallReceiver();
+    method public void onReceive(android.content.Context, android.content.Intent?);
+    field public static final String ACTION_BENCHMARK_OPERATION = "androidx.profileinstaller.action.BENCHMARK_OPERATION";
+    field public static final String ACTION_INSTALL_PROFILE = "androidx.profileinstaller.action.INSTALL_PROFILE";
+    field public static final String ACTION_SAVE_PROFILE = "androidx.profileinstaller.action.SAVE_PROFILE";
+    field public static final String ACTION_SKIP_FILE = "androidx.profileinstaller.action.SKIP_FILE";
+  }
+
+  public class ProfileInstaller {
+    method @WorkerThread public static void writeProfile(android.content.Context);
+    method @WorkerThread public static void writeProfile(android.content.Context, java.util.concurrent.Executor, androidx.profileinstaller.ProfileInstaller.DiagnosticsCallback);
+    field public static final int DIAGNOSTIC_CURRENT_PROFILE_DOES_NOT_EXIST = 2; // 0x2
+    field public static final int DIAGNOSTIC_CURRENT_PROFILE_EXISTS = 1; // 0x1
+    field public static final int DIAGNOSTIC_PROFILE_IS_COMPRESSED = 5; // 0x5
+    field public static final int DIAGNOSTIC_REF_PROFILE_DOES_NOT_EXIST = 4; // 0x4
+    field public static final int DIAGNOSTIC_REF_PROFILE_EXISTS = 3; // 0x3
+    field public static final int RESULT_ALREADY_INSTALLED = 2; // 0x2
+    field public static final int RESULT_BASELINE_PROFILE_NOT_FOUND = 6; // 0x6
+    field public static final int RESULT_BENCHMARK_OPERATION_FAILURE = 15; // 0xf
+    field public static final int RESULT_BENCHMARK_OPERATION_SUCCESS = 14; // 0xe
+    field public static final int RESULT_BENCHMARK_OPERATION_UNKNOWN = 16; // 0x10
+    field public static final int RESULT_DELETE_SKIP_FILE_SUCCESS = 11; // 0xb
+    field public static final int RESULT_DESIRED_FORMAT_UNSUPPORTED = 5; // 0x5
+    field public static final int RESULT_INSTALL_SKIP_FILE_SUCCESS = 10; // 0xa
+    field public static final int RESULT_INSTALL_SUCCESS = 1; // 0x1
+    field public static final int RESULT_IO_EXCEPTION = 7; // 0x7
+    field public static final int RESULT_META_FILE_REQUIRED_BUT_NOT_FOUND = 9; // 0x9
+    field public static final int RESULT_NOT_WRITABLE = 4; // 0x4
+    field public static final int RESULT_PARSE_EXCEPTION = 8; // 0x8
+    field public static final int RESULT_SAVE_PROFILE_SIGNALLED = 12; // 0xc
+    field public static final int RESULT_SAVE_PROFILE_SKIPPED = 13; // 0xd
+    field public static final int RESULT_UNSUPPORTED_ART_VERSION = 3; // 0x3
+  }
+
+  public static interface ProfileInstaller.DiagnosticsCallback {
+    method public void onDiagnosticReceived(int, Object?);
+    method public void onResultReceived(int, Object?);
+  }
+
+  public class ProfileInstallerInitializer implements androidx.startup.Initializer<androidx.profileinstaller.ProfileInstallerInitializer.Result> {
+    ctor public ProfileInstallerInitializer();
+    method public androidx.profileinstaller.ProfileInstallerInitializer.Result create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
+  public static class ProfileInstallerInitializer.Result {
+    ctor public ProfileInstallerInitializer.Result();
+  }
+
+  public final class ProfileVerifier {
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.profileinstaller.ProfileVerifier.CompilationStatus!> getCompilationStatusAsync();
+    method @WorkerThread public static androidx.profileinstaller.ProfileVerifier.CompilationStatus writeProfileVerification(android.content.Context);
+  }
+
+  public static class ProfileVerifier.CompilationStatus {
+    method public int getProfileInstallResultCode();
+    method public boolean hasProfileEnqueuedForCompilation();
+    method public boolean isCompiledWithProfile();
+    field public static final int RESULT_CODE_COMPILED_WITH_PROFILE = 1; // 0x1
+    field public static final int RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING = 3; // 0x3
+    field public static final int RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ = 131072; // 0x20000
+    field public static final int RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE = 196608; // 0x30000
+    field public static final int RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST = 65536; // 0x10000
+    field public static final int RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION = 262144; // 0x40000
+    field public static final int RESULT_CODE_NO_PROFILE = 0; // 0x0
+    field public static final int RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION = 2; // 0x2
+  }
+
+}
+
diff --git a/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt b/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
index 83b7dce..e7da088 100644
--- a/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
+++ b/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
@@ -15,6 +15,7 @@
     method @WorkerThread public static void writeProfile(android.content.Context, java.util.concurrent.Executor, androidx.profileinstaller.ProfileInstaller.DiagnosticsCallback);
     field public static final int DIAGNOSTIC_CURRENT_PROFILE_DOES_NOT_EXIST = 2; // 0x2
     field public static final int DIAGNOSTIC_CURRENT_PROFILE_EXISTS = 1; // 0x1
+    field public static final int DIAGNOSTIC_PROFILE_IS_COMPRESSED = 5; // 0x5
     field public static final int DIAGNOSTIC_REF_PROFILE_DOES_NOT_EXIST = 4; // 0x4
     field public static final int DIAGNOSTIC_REF_PROFILE_EXISTS = 3; // 0x3
     field public static final int RESULT_ALREADY_INSTALLED = 2; // 0x2
diff --git a/camera/camera-viewfinder/api/res-1.2.0-beta01.txt b/profileinstaller/profileinstaller/api/res-1.3.0-beta01.txt
similarity index 100%
copy from camera/camera-viewfinder/api/res-1.2.0-beta01.txt
copy to profileinstaller/profileinstaller/api/res-1.3.0-beta01.txt
diff --git a/profileinstaller/profileinstaller/api/restricted_1.3.0-beta01.txt b/profileinstaller/profileinstaller/api/restricted_1.3.0-beta01.txt
new file mode 100644
index 0000000..e7da088
--- /dev/null
+++ b/profileinstaller/profileinstaller/api/restricted_1.3.0-beta01.txt
@@ -0,0 +1,74 @@
+// Signature format: 4.0
+package androidx.profileinstaller {
+
+  public class ProfileInstallReceiver extends android.content.BroadcastReceiver {
+    ctor public ProfileInstallReceiver();
+    method public void onReceive(android.content.Context, android.content.Intent?);
+    field public static final String ACTION_BENCHMARK_OPERATION = "androidx.profileinstaller.action.BENCHMARK_OPERATION";
+    field public static final String ACTION_INSTALL_PROFILE = "androidx.profileinstaller.action.INSTALL_PROFILE";
+    field public static final String ACTION_SAVE_PROFILE = "androidx.profileinstaller.action.SAVE_PROFILE";
+    field public static final String ACTION_SKIP_FILE = "androidx.profileinstaller.action.SKIP_FILE";
+  }
+
+  public class ProfileInstaller {
+    method @WorkerThread public static void writeProfile(android.content.Context);
+    method @WorkerThread public static void writeProfile(android.content.Context, java.util.concurrent.Executor, androidx.profileinstaller.ProfileInstaller.DiagnosticsCallback);
+    field public static final int DIAGNOSTIC_CURRENT_PROFILE_DOES_NOT_EXIST = 2; // 0x2
+    field public static final int DIAGNOSTIC_CURRENT_PROFILE_EXISTS = 1; // 0x1
+    field public static final int DIAGNOSTIC_PROFILE_IS_COMPRESSED = 5; // 0x5
+    field public static final int DIAGNOSTIC_REF_PROFILE_DOES_NOT_EXIST = 4; // 0x4
+    field public static final int DIAGNOSTIC_REF_PROFILE_EXISTS = 3; // 0x3
+    field public static final int RESULT_ALREADY_INSTALLED = 2; // 0x2
+    field public static final int RESULT_BASELINE_PROFILE_NOT_FOUND = 6; // 0x6
+    field public static final int RESULT_BENCHMARK_OPERATION_FAILURE = 15; // 0xf
+    field public static final int RESULT_BENCHMARK_OPERATION_SUCCESS = 14; // 0xe
+    field public static final int RESULT_BENCHMARK_OPERATION_UNKNOWN = 16; // 0x10
+    field public static final int RESULT_DELETE_SKIP_FILE_SUCCESS = 11; // 0xb
+    field public static final int RESULT_DESIRED_FORMAT_UNSUPPORTED = 5; // 0x5
+    field public static final int RESULT_INSTALL_SKIP_FILE_SUCCESS = 10; // 0xa
+    field public static final int RESULT_INSTALL_SUCCESS = 1; // 0x1
+    field public static final int RESULT_IO_EXCEPTION = 7; // 0x7
+    field public static final int RESULT_META_FILE_REQUIRED_BUT_NOT_FOUND = 9; // 0x9
+    field public static final int RESULT_NOT_WRITABLE = 4; // 0x4
+    field public static final int RESULT_PARSE_EXCEPTION = 8; // 0x8
+    field public static final int RESULT_SAVE_PROFILE_SIGNALLED = 12; // 0xc
+    field public static final int RESULT_SAVE_PROFILE_SKIPPED = 13; // 0xd
+    field public static final int RESULT_UNSUPPORTED_ART_VERSION = 3; // 0x3
+  }
+
+  public static interface ProfileInstaller.DiagnosticsCallback {
+    method public void onDiagnosticReceived(int, Object?);
+    method public void onResultReceived(int, Object?);
+  }
+
+  public class ProfileInstallerInitializer implements androidx.startup.Initializer<androidx.profileinstaller.ProfileInstallerInitializer.Result> {
+    ctor public ProfileInstallerInitializer();
+    method public androidx.profileinstaller.ProfileInstallerInitializer.Result create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
+  public static class ProfileInstallerInitializer.Result {
+    ctor public ProfileInstallerInitializer.Result();
+  }
+
+  public final class ProfileVerifier {
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.profileinstaller.ProfileVerifier.CompilationStatus!> getCompilationStatusAsync();
+    method @WorkerThread public static androidx.profileinstaller.ProfileVerifier.CompilationStatus writeProfileVerification(android.content.Context);
+  }
+
+  public static class ProfileVerifier.CompilationStatus {
+    method public int getProfileInstallResultCode();
+    method public boolean hasProfileEnqueuedForCompilation();
+    method public boolean isCompiledWithProfile();
+    field public static final int RESULT_CODE_COMPILED_WITH_PROFILE = 1; // 0x1
+    field public static final int RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING = 3; // 0x3
+    field public static final int RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ = 131072; // 0x20000
+    field public static final int RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE = 196608; // 0x30000
+    field public static final int RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST = 65536; // 0x10000
+    field public static final int RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION = 262144; // 0x40000
+    field public static final int RESULT_CODE_NO_PROFILE = 0; // 0x0
+    field public static final int RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION = 2; // 0x2
+  }
+
+}
+
diff --git a/profileinstaller/profileinstaller/api/restricted_current.txt b/profileinstaller/profileinstaller/api/restricted_current.txt
index 83b7dce..e7da088 100644
--- a/profileinstaller/profileinstaller/api/restricted_current.txt
+++ b/profileinstaller/profileinstaller/api/restricted_current.txt
@@ -15,6 +15,7 @@
     method @WorkerThread public static void writeProfile(android.content.Context, java.util.concurrent.Executor, androidx.profileinstaller.ProfileInstaller.DiagnosticsCallback);
     field public static final int DIAGNOSTIC_CURRENT_PROFILE_DOES_NOT_EXIST = 2; // 0x2
     field public static final int DIAGNOSTIC_CURRENT_PROFILE_EXISTS = 1; // 0x1
+    field public static final int DIAGNOSTIC_PROFILE_IS_COMPRESSED = 5; // 0x5
     field public static final int DIAGNOSTIC_REF_PROFILE_DOES_NOT_EXIST = 4; // 0x4
     field public static final int DIAGNOSTIC_REF_PROFILE_EXISTS = 3; // 0x3
     field public static final int RESULT_ALREADY_INSTALLED = 2; // 0x2
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
index 2696f2b..068b838 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
@@ -160,42 +160,117 @@
         if (mDesiredVersion == null) {
             return this;
         }
-        try (InputStream is = mAssetManager.open(mProfileSourceLocation)) {
-                byte[] baselineVersion = ProfileTranscoder.readHeader(is, MAGIC_PROF);
-                mProfile = ProfileTranscoder.readProfile(is, baselineVersion, mApkName);
+
+        InputStream profileStream = getProfileInputStream(mAssetManager);
+        if (profileStream != null) {
+            mProfile = readProfileInternal(profileStream);
+        }
+        if (mProfile != null) {
+            DexProfileData[] profile = mProfile;
+            if (requiresMetadata()) {
+                DeviceProfileWriter profileWriter = addMetadata(profile, mDesiredVersion);
+                if (profileWriter != null) return profileWriter;
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Loads an {@link InputStream} from assets whether the underlying file is compressed or not.
+     *
+     * @param assetManager The {@link AssetManager} to use.
+     * @param location The source file's location.
+     * @return An InputStream in case the profile was successfully read.
+     * @throws IOException If anything goes wrong while opening or reading the file.
+     */
+    private @Nullable InputStream openStreamFromAssets(AssetManager assetManager, String location)
+            throws IOException {
+        InputStream profileStream = null;
+        try {
+            AssetFileDescriptor descriptor = assetManager.openFd(location);
+            profileStream = descriptor.createInputStream();
+        } catch (FileNotFoundException e) {
+            String message = e.getMessage();
+            if (message != null && message.contains("compressed")) {
+                mDiagnostics.onDiagnosticReceived(
+                        ProfileInstaller.DIAGNOSTIC_PROFILE_IS_COMPRESSED, null);
+            }
+        }
+        return profileStream;
+    }
+
+    /**
+     * Load the baseline profile file from assets.
+     * @param assetManager The {@link AssetManager} to use.
+     * @return The opened stream or null if the stream was unable to be opened.
+     */
+    private @Nullable InputStream getProfileInputStream(AssetManager assetManager) {
+        InputStream profileStream = null;
+        try {
+            profileStream = openStreamFromAssets(assetManager, mProfileSourceLocation);
         } catch (FileNotFoundException e) {
             mDiagnostics.onResultReceived(ProfileInstaller.RESULT_BASELINE_PROFILE_NOT_FOUND, e);
         } catch (IOException e) {
             mDiagnostics.onResultReceived(ProfileInstaller.RESULT_IO_EXCEPTION, e);
+        }
+        return profileStream;
+    }
+
+    /**
+     * Reads a baseline profile from a given {@link InputStream} and transcodes it along the way
+     * if needed.
+     *
+     * @param profileStream The {@link InputStream} containing the baseline profile data.
+     */
+    private @Nullable DexProfileData[] readProfileInternal(InputStream profileStream) {
+        DexProfileData[] profile = null;
+        try {
+            byte[] baselineVersion = ProfileTranscoder.readHeader(profileStream, MAGIC_PROF);
+            profile = ProfileTranscoder.readProfile(profileStream, baselineVersion, mApkName);
+        } catch (IOException e) {
+            mDiagnostics.onResultReceived(ProfileInstaller.RESULT_IO_EXCEPTION, e);
         } catch (IllegalStateException e) {
             mDiagnostics.onResultReceived(ProfileInstaller.RESULT_PARSE_EXCEPTION, e);
-        }
-        DexProfileData[] profile = mProfile;
-        if (profile != null && requiresMetadata()) {
-            try (AssetFileDescriptor fd = mAssetManager.openFd(mProfileMetaSourceLocation)) {
-                try (InputStream is = fd.createInputStream()) {
-                    byte[] metaVersion = ProfileTranscoder.readHeader(is, MAGIC_PROFM);
-                    mProfile = ProfileTranscoder.readMeta(
-                            is,
-                            metaVersion,
-                            mDesiredVersion,
-                            profile
-                    );
-                    return this;
-                }
-            } catch (FileNotFoundException e) {
-                mDiagnostics.onResultReceived(
-                        ProfileInstaller.RESULT_META_FILE_REQUIRED_BUT_NOT_FOUND,
-                        e
-                );
+        } finally {
+            try {
+                profileStream.close();
             } catch (IOException e) {
                 mDiagnostics.onResultReceived(ProfileInstaller.RESULT_IO_EXCEPTION, e);
-            } catch (IllegalStateException e) {
-                mProfile = null;
-                mDiagnostics.onResultReceived(ProfileInstaller.RESULT_PARSE_EXCEPTION, e);
             }
         }
-        return this;
+        return profile;
+    }
+
+    /**
+     * Add Metadata from an existing baseline profile metadata file.
+     * @param profile The profile which needs adding of metadata.
+     *
+     * @return Baseline profile with metaadata.
+     */
+    @Nullable
+    private DeviceProfileWriter addMetadata(DexProfileData[] profile, byte[] desiredVersion) {
+
+        try (InputStream is = openStreamFromAssets(mAssetManager, mProfileMetaSourceLocation)) {
+            if (is != null) {
+                byte[] metaVersion = ProfileTranscoder.readHeader(is, MAGIC_PROFM);
+                mProfile = ProfileTranscoder.readMeta(
+                        is,
+                        metaVersion,
+                        desiredVersion,
+                        profile
+                );
+                return this;
+            }
+        } catch (FileNotFoundException e) {
+            mDiagnostics.onResultReceived(
+                    ProfileInstaller.RESULT_META_FILE_REQUIRED_BUT_NOT_FOUND, e);
+        } catch (IOException e) {
+            mDiagnostics.onResultReceived(ProfileInstaller.RESULT_IO_EXCEPTION, e);
+        } catch (IllegalStateException e) {
+            mProfile = null;
+            mDiagnostics.onResultReceived(ProfileInstaller.RESULT_PARSE_EXCEPTION, e);
+        }
+        return null;
     }
 
     /**
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
index f9c63c5..c0b0036 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
@@ -137,6 +137,9 @@
                 case DIAGNOSTIC_REF_PROFILE_DOES_NOT_EXIST:
                     msg = "DIAGNOSTIC_REF_PROFILE_DOES_NOT_EXIST";
                     break;
+                case DIAGNOSTIC_PROFILE_IS_COMPRESSED:
+                    msg = "DIAGNOSTIC_PROFILE_IS_COMPRESSED";
+                    break;
             }
             Log.d(TAG, msg);
         }
@@ -189,7 +192,8 @@
             DIAGNOSTIC_CURRENT_PROFILE_EXISTS,
             DIAGNOSTIC_CURRENT_PROFILE_DOES_NOT_EXIST,
             DIAGNOSTIC_REF_PROFILE_EXISTS,
-            DIAGNOSTIC_REF_PROFILE_DOES_NOT_EXIST
+            DIAGNOSTIC_REF_PROFILE_DOES_NOT_EXIST,
+            DIAGNOSTIC_PROFILE_IS_COMPRESSED
     })
     public @interface DiagnosticCode {}
 
@@ -220,6 +224,12 @@
     @DiagnosticCode public static final int DIAGNOSTIC_REF_PROFILE_DOES_NOT_EXIST = 4;
 
     /**
+     * Indicates that the profile is compressed and a newer version of bundletool needs to be used
+     * to build the app.
+     */
+    @DiagnosticCode public static final int DIAGNOSTIC_PROFILE_IS_COMPRESSED = 5;
+
+    /**
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY)
diff --git a/recyclerview/OWNERS b/recyclerview/OWNERS
index bcc9f1e..33d6a94 100644
--- a/recyclerview/OWNERS
+++ b/recyclerview/OWNERS
@@ -1,2 +1,3 @@
+# Bug component: 461174
 ryanmentley@google.com
 yboyar@google.com
\ No newline at end of file
diff --git a/room/integration-tests/incremental-annotation-processing/src/test/kotlin/androidx/room/gradle/RoomIncrementalAnnotationProcessingTest.kt b/room/integration-tests/incremental-annotation-processing/src/test/kotlin/androidx/room/gradle/RoomIncrementalAnnotationProcessingTest.kt
index d6096c6..8720b6e 100644
--- a/room/integration-tests/incremental-annotation-processing/src/test/kotlin/androidx/room/gradle/RoomIncrementalAnnotationProcessingTest.kt
+++ b/room/integration-tests/incremental-annotation-processing/src/test/kotlin/androidx/room/gradle/RoomIncrementalAnnotationProcessingTest.kt
@@ -212,6 +212,14 @@
                 $processorConfiguration "androidx.room:room-compiler:$roomVersion"
             }
 
+            tasks.withType(
+                org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+            ).configureEach {
+                kotlinOptions {
+                    jvmTarget = "1.8"
+                }
+            }
+
             class SchemaLocationArgumentProvider implements CommandLineArgumentProvider {
 
                 @OutputDirectory
@@ -237,6 +245,11 @@
                         }
                     }
                 }
+
+                compileOptions {
+                  sourceCompatibility = JavaVersion.VERSION_1_8
+                  targetCompatibility = JavaVersion.VERSION_1_8
+                }
             }
             $kspArgumentsBlock
         """
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
index b0b500b..29f861c7 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
@@ -134,8 +134,14 @@
     fun getBook(bookId: String): Book
 
     @Query("SELECT * FROM book WHERE bookId = :bookId")
+    fun getBookNullable(bookId: String): Book?
+
+    @Query("SELECT * FROM book WHERE bookId = :bookId")
     suspend fun getBookSuspend(bookId: String): Book
 
+    @Query("SELECT * FROM book WHERE bookId = :bookId")
+    suspend fun getBookNullableSuspend(bookId: String): Book?
+
     @Query("SELECT * FROM book")
     suspend fun getBooksSuspend(): List<Book>
 
@@ -366,6 +372,9 @@
     @Query("SELECT * FROM Publisher WHERE publisherId = :publisherId")
     fun getPublisher(publisherId: String): Publisher
 
+    @Query("SELECT * FROM Publisher WHERE publisherId = :publisherId")
+    fun getPublisherNullable(publisherId: String): Publisher?
+
     @Query("SELECT * FROM Publisher WHERE _rowid_ = :rowid")
     fun getPublisher(rowid: Long): Publisher
 
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/AlteredTableColumnOrderTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/AlteredTableColumnOrderTest.kt
index d873f1c..99740cc 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/AlteredTableColumnOrderTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/AlteredTableColumnOrderTest.kt
@@ -116,7 +116,7 @@
     @Dao
     internal interface FooDao {
         @Insert
-        fun insertFoo(f: Foo?)
+        fun insertFoo(f: Foo)
 
         @Query("SELECT * FROM Foo LIMIT 1")
         fun getOneFoo(): Foo
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BooksDaoTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BooksDaoTest.kt
index 49b2b5f..370e1f7 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BooksDaoTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BooksDaoTest.kt
@@ -30,11 +30,12 @@
 import io.reactivex.Flowable
 import io.reactivex.schedulers.Schedulers
 import io.reactivex.subscribers.TestSubscriber
+import java.util.Date
 import kotlinx.coroutines.runBlocking
 import org.hamcrest.CoreMatchers
-import org.hamcrest.CoreMatchers.`is`
 import org.hamcrest.CoreMatchers.equalTo
 import org.hamcrest.CoreMatchers.instanceOf
+import org.hamcrest.CoreMatchers.`is`
 import org.hamcrest.CoreMatchers.notNullValue
 import org.hamcrest.MatcherAssert.assertThat
 import org.junit.Assert.assertEquals
@@ -42,7 +43,6 @@
 import org.junit.Assert.assertNull
 import org.junit.Assert.fail
 import org.junit.Test
-import java.util.Date
 
 @MediumTest
 class BooksDaoTest : TestDatabaseTest() {
@@ -431,7 +431,7 @@
     @Test
     fun kotlinDefaultFunction() {
         booksDao.addAndRemovePublisher(TestUtil.PUBLISHER)
-        assertNull(booksDao.getPublisher(TestUtil.PUBLISHER.publisherId))
+        assertNull(booksDao.getPublisherNullable(TestUtil.PUBLISHER.publisherId))
 
         assertEquals("", booksDao.concreteFunction())
         assertEquals("1 - hello", booksDao.concreteFunctionWithParams(1, "hello"))
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DeferredBooksDaoTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DeferredBooksDaoTest.kt
index 1ad5028..24ddfac 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DeferredBooksDaoTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DeferredBooksDaoTest.kt
@@ -142,14 +142,14 @@
     @Test
     fun deleteBookWithIds() {
         booksDao.deleteBookWithIds(TestUtil.BOOK_1.bookId)
-        assertThat(booksDao.getBook(TestUtil.BOOK_1.bookId), nullValue())
+        assertThat(booksDao.getBookNullable(TestUtil.BOOK_1.bookId), nullValue())
     }
 
     @Test
     fun deleteBookWithIdsSuspend() {
         runBlocking {
             booksDao.deleteBookWithIdsSuspend(TestUtil.BOOK_1.bookId)
-            assertThat(booksDao.getBookSuspend(TestUtil.BOOK_1.bookId), nullValue())
+            assertThat(booksDao.getBookNullableSuspend(TestUtil.BOOK_1.bookId), nullValue())
         }
     }
 
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DependencyDaoTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DependencyDaoTest.kt
index 7e9e88b..f5bd5fd 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DependencyDaoTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DependencyDaoTest.kt
@@ -92,19 +92,19 @@
         )
         dao.insert(foo1, foo2, bar)
         val fooList = dao.relation("foo")
-        assertThat(fooList.sharedName, `is`("foo"))
         assertThat(fooList, `is`(notNullValue()))
-        assertThat(fooList.dataItems, `is`(listOf(foo1, foo2)))
+        assertThat(fooList?.sharedName, `is`("foo"))
+        assertThat(fooList?.dataItems, `is`(listOf(foo1, foo2)))
 
         val barList = dao.relation("bar")
-        assertThat(barList.sharedName, `is`("bar"))
         assertThat(barList, `is`(notNullValue()))
-        assertThat(barList.dataItems, `is`(listOf(bar)))
+        assertThat(barList?.sharedName, `is`("bar"))
+        assertThat(barList?.dataItems, `is`(listOf(bar)))
 
         val bazList = dao.relation("baz")
-        assertThat(bazList.sharedName, `is`("baz"))
         assertThat(bazList, `is`(notNullValue()))
-        assertThat(bazList.dataItems, `is`(emptyList()))
+        assertThat(bazList?.sharedName, `is`("baz"))
+        assertThat(bazList?.dataItems, `is`(emptyList()))
     }
 
     private fun insertSample(id: Int): DataClassFromDependency {
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/test/MultiTypedPagingSourceTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt
index 866789b..43badcf 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt
@@ -48,6 +48,7 @@
 import kotlinx.coroutines.withContext
 import org.junit.After
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -340,6 +341,7 @@
         }
     }
 
+    @Ignore // b/260592924
     @Test
     fun prependWithBlockingObserver() {
         val items = createItems(startId = 0, count = 90)
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/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/dao/DependencyDao.kt b/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/dao/DependencyDao.kt
index 5365d47..487f747 100644
--- a/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/dao/DependencyDao.kt
+++ b/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/dao/DependencyDao.kt
@@ -33,18 +33,18 @@
     fun selectAll(): List<DataClassFromDependency>
 
     @Query("select * from DataClassFromDependency where id = :id LIMIT 1")
-    fun findEmbedded(id: Int): EmbeddedFromDependency
+    fun findEmbedded(id: Int): EmbeddedFromDependency?
 
     @Query("select * from DataClassFromDependency where id = :id LIMIT 1")
-    fun findPojo(id: Int): PojoFromDependency
+    fun findPojo(id: Int): PojoFromDependency?
 
     @Query("select * from DataClassFromDependency where id = :id LIMIT 1")
-    fun findById(id: Int): DataClassFromDependency
+    fun findById(id: Int): DataClassFromDependency?
 
     @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
     @Transaction
     @Query("WITH nameTable( sharedName ) AS ( SELECT :name ) SELECT * from nameTable")
-    fun relation(name: String): RelationFromDependency
+    fun relation(name: String): RelationFromDependency?
 
     @Insert(onConflict = OnConflictStrategy.REPLACE)
     fun insert(vararg input: DataClassFromDependency)
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-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/DelegatingTestRegistrar.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/DelegatingTestRegistrar.kt
index ae10928..2cc1938 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/DelegatingTestRegistrar.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/DelegatingTestRegistrar.kt
@@ -17,24 +17,27 @@
 package androidx.room.compiler.processing.util.compiler
 
 import androidx.room.compiler.processing.util.compiler.DelegatingTestRegistrar.Companion.runCompilation
+import java.net.URI
+import java.nio.file.Paths
 import org.jetbrains.kotlin.cli.common.ExitCode
 import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
 import org.jetbrains.kotlin.cli.common.messages.MessageCollector
 import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
-import org.jetbrains.kotlin.cli.jvm.plugins.ServiceLoaderLite
 import org.jetbrains.kotlin.com.intellij.mock.MockProject
-import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
+import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
 import org.jetbrains.kotlin.config.CompilerConfiguration
 import org.jetbrains.kotlin.config.Services
-import java.net.URI
-import java.nio.file.Paths
+import org.jetbrains.kotlin.util.ServiceLoaderLite
 
 /**
  * A component registrar for Kotlin Compiler that delegates to a list of thread local delegates.
  *
  * see [runCompilation] for usages.
  */
-internal class DelegatingTestRegistrar : ComponentRegistrar {
+@Suppress("DEPRECATION") // TODO: Migrate ComponentRegistrar to CompilerPluginRegistrar
+@OptIn(ExperimentalCompilerApi::class)
+internal class DelegatingTestRegistrar :
+    @Suppress("DEPRECATION") org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar {
     override fun registerProjectComponents(
         project: MockProject,
         configuration: CompilerConfiguration
@@ -63,7 +66,8 @@
                 }
                 .find { resourcesPath ->
                     ServiceLoaderLite.findImplementations(
-                        ComponentRegistrar::class.java,
+                        @Suppress("DEPRECATION")
+                        org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar::class.java,
                         listOf(resourcesPath.toFile())
                     ).any { implementation ->
                         implementation == DelegatingTestRegistrar::class.java.name
@@ -76,12 +80,15 @@
                     """.trimIndent()
                 )
         }
-        private val delegates = ThreadLocal<List<ComponentRegistrar>>()
+        @Suppress("DEPRECATION")
+        private val delegates =
+            ThreadLocal<List<org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar>>()
         fun runCompilation(
             compiler: K2JVMCompiler,
             messageCollector: MessageCollector,
             arguments: K2JVMCompilerArguments,
-            pluginRegistrars: List<ComponentRegistrar>
+            @Suppress("DEPRECATION")
+            pluginRegistrars: List<org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar>
         ): ExitCode {
             try {
                 arguments.addDelegatingTestRegistrar()
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/KotlinCliRunner.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/KotlinCliRunner.kt
index 9b1f573..5b591f0 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/KotlinCliRunner.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/KotlinCliRunner.kt
@@ -24,7 +24,7 @@
 import org.jetbrains.kotlin.cli.common.ExitCode
 import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
 import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
-import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
+import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
 import org.jetbrains.kotlin.config.JvmDefaultMode
 import org.jetbrains.kotlin.config.JvmTarget
 
@@ -77,6 +77,7 @@
     /**
      * Runs the kotlin cli API with the given arguments.
      */
+    @OptIn(ExperimentalCompilerApi::class)
     fun runKotlinCli(
         /**
          * Compilation arguments (sources, classpaths etc)
@@ -89,7 +90,8 @@
         /**
          * List of component registrars for the compilation.
          */
-        pluginRegistrars: List<ComponentRegistrar>
+        @Suppress("DEPRECATION")
+        pluginRegistrars: List<org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar>
     ): KotlinCliResult {
         val cliArguments = compiler.createArguments()
         destinationDir.mkdirs()
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKapt3Registrar.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKapt3Registrar.kt
index f38d3ad..8dca55a 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKapt3Registrar.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKapt3Registrar.kt
@@ -17,6 +17,8 @@
 
 package androidx.room.compiler.processing.util.compiler
 
+import java.io.File
+import javax.annotation.processing.Processor
 import org.jetbrains.kotlin.base.kapt3.KaptFlag
 import org.jetbrains.kotlin.base.kapt3.KaptOptions
 import org.jetbrains.kotlin.base.kapt3.logString
@@ -25,7 +27,7 @@
 import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot
 import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot
 import org.jetbrains.kotlin.com.intellij.mock.MockProject
-import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
+import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
 import org.jetbrains.kotlin.config.CompilerConfiguration
 import org.jetbrains.kotlin.config.JVMConfigurationKeys
 import org.jetbrains.kotlin.container.StorageComponentContainer
@@ -38,13 +40,11 @@
 import org.jetbrains.kotlin.kapt3.base.incremental.DeclaredProcType
 import org.jetbrains.kotlin.kapt3.base.incremental.IncrementalProcessor
 import org.jetbrains.kotlin.kapt3.util.MessageCollectorBackedKaptLogger
+import org.jetbrains.kotlin.kapt3.util.doOpenInternalPackagesIfRequired
 import org.jetbrains.kotlin.platform.TargetPlatform
 import org.jetbrains.kotlin.platform.jvm.isJvm
 import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension
 import org.jetbrains.kotlin.resolve.jvm.extensions.PartialAnalysisHandlerExtension
-import java.io.File
-import javax.annotation.processing.Processor
-import org.jetbrains.kotlin.kapt3.util.doOpenInternalPackagesIfRequired
 
 /**
  * Registers the KAPT component for the kotlin compilation.
@@ -53,11 +53,13 @@
  * https://github.com/JetBrains/kotlin/blob/master/plugins/kapt3/kapt3-compiler/src/
  *  org/jetbrains/kotlin/kapt3/Kapt3Plugin.kt
  */
+@Suppress("DEPRECATION") // TODO: Migrate ComponentRegistrar to CompilerPluginRegistrar
+@OptIn(ExperimentalCompilerApi::class)
 internal class TestKapt3Registrar(
     val processors: List<Processor>,
     val baseOptions: KaptOptions.Builder,
     val messageCollector: MessageCollector
-) : ComponentRegistrar {
+) : @Suppress("DEPRECATION") org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar {
     override fun registerProjectComponents(
         project: MockProject,
         configuration: CompilerConfiguration
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKspRegistrar.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKspRegistrar.kt
index b4104d3..9ab12a4 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKspRegistrar.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKspRegistrar.kt
@@ -22,6 +22,7 @@
 import com.google.devtools.ksp.processing.KSPLogger
 import com.google.devtools.ksp.processing.SymbolProcessorProvider
 import com.google.devtools.ksp.processing.impl.MessageCollectorBasedKSPLogger
+import java.io.File
 import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
 import org.jetbrains.kotlin.cli.common.messages.MessageCollector
 import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot
@@ -30,21 +31,22 @@
 import org.jetbrains.kotlin.com.intellij.mock.MockProject
 import org.jetbrains.kotlin.com.intellij.psi.PsiTreeChangeAdapter
 import org.jetbrains.kotlin.com.intellij.psi.PsiTreeChangeListener
-import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
+import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
 import org.jetbrains.kotlin.config.CompilerConfiguration
 import org.jetbrains.kotlin.resolve.extensions.AnalysisHandlerExtension
-import java.io.File
 
 /**
  * Registers the KSP component for the kotlin compilation.
  */
+@Suppress("DEPRECATION") // TODO: Migrate ComponentRegistrar to CompilerPluginRegistrar
+@OptIn(ExperimentalCompilerApi::class)
 internal class TestKspRegistrar(
     val kspWorkingDir: File,
     val baseOptions: KspOptions.Builder,
 
     val processorProviders: List<SymbolProcessorProvider>,
     val messageCollector: MessageCollector
-) : ComponentRegistrar {
+) : @Suppress("DEPRECATION") org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar {
     override fun registerProjectComponents(
         project: MockProject,
         configuration: CompilerConfiguration
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KaptCompilationStep.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KaptCompilationStep.kt
index e968381..2b8bc91 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KaptCompilationStep.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KaptCompilationStep.kt
@@ -26,7 +26,8 @@
 import org.jetbrains.kotlin.base.kapt3.KaptFlag
 import org.jetbrains.kotlin.base.kapt3.KaptOptions
 import org.jetbrains.kotlin.cli.common.ExitCode
-import org.jetbrains.kotlin.compiler.plugin.parsePluginOption
+import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
+import org.jetbrains.kotlin.compiler.plugin.parseLegacyPluginOption
 
 /**
  * Runs KAPT to run annotation processors.
@@ -72,6 +73,7 @@
         }
     }
 
+    @OptIn(ExperimentalCompilerApi::class)
     override fun execute(
         workingDir: File,
         arguments: CompilationStepArguments
@@ -131,7 +133,7 @@
             val options = kotlincArguments.dropLast(1).zip(kotlincArguments.drop(1))
                 .filter { it.first == "-P" }
                 .mapNotNull {
-                    parsePluginOption(it.second)
+                    parseLegacyPluginOption(it.second)
                 }
             val filteredOptionsMap = options
                 .filter { it.pluginId == pluginId }
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KotlinSourceCompilationStep.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KotlinSourceCompilationStep.kt
index dcfe22b..aa80eaf 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KotlinSourceCompilationStep.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KotlinSourceCompilationStep.kt
@@ -17,6 +17,7 @@
 package androidx.room.compiler.processing.util.compiler.steps
 
 import androidx.room.compiler.processing.util.compiler.KotlinCliRunner
+import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
 import org.jetbrains.kotlin.cli.common.ExitCode
 import java.io.File
 
@@ -28,6 +29,7 @@
 internal object KotlinSourceCompilationStep : KotlinCompilationStep {
     override val name = "kotlinSourceCompilation"
 
+    @OptIn(ExperimentalCompilerApi::class)
     override fun execute(
         workingDir: File,
         arguments: CompilationStepArguments
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KspCompilationStep.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KspCompilationStep.kt
index 392de38..5a9069a 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KspCompilationStep.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/steps/KspCompilationStep.kt
@@ -24,6 +24,7 @@
 import com.google.devtools.ksp.processing.SymbolProcessorProvider
 import java.io.File
 import org.jetbrains.kotlin.cli.common.ExitCode
+import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
 
 /**
  * Runs the Symbol Processors
@@ -44,6 +45,7 @@
         }
     }
 
+    @OptIn(ExperimentalCompilerApi::class)
     override fun execute(
         workingDir: File,
         arguments: CompilationStepArguments
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XFunSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XFunSpec.kt
index 56cc91c..3a1a2ec 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XFunSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XFunSpec.kt
@@ -36,7 +36,9 @@
 
         val name: String
 
-        fun addAnnotation(annotation: XAnnotationSpec)
+        fun addAnnotation(annotation: XAnnotationSpec): Builder
+
+        fun addAbstractModifier(): Builder
 
         // TODO(b/247247442): Maybe make a XParameterSpec ?
         fun addParameter(
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XPropertySpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XPropertySpec.kt
index a66f12e..50ecd53 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XPropertySpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XPropertySpec.kt
@@ -90,11 +90,4 @@
             }
         }
     }
-}
-
-// TODO(b/127483380): Temporary API for XPoet migration.
-// @Deprecated("Temporary API for XPoet migration.")
-fun XPropertySpec.toJavaPoet(): FieldSpec {
-    check(this is JavaPropertySpec)
-    return this.actual
 }
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
index 7e7dc7b..df2a0b3 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
@@ -279,6 +279,7 @@
     val packageName: String = java.packageName()
     val simpleNames: List<String> = java.simpleNames()
     val canonicalName: String = java.canonicalName()
+    val reflectionName: String = java.reflectionName()
 
     /**
      * Returns a parameterized type, applying the `typeArguments` to `this`.
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeSpec.kt
index c4608e9..344481a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeSpec.kt
@@ -22,6 +22,7 @@
 import androidx.room.compiler.codegen.kotlin.KotlinTypeSpec
 import androidx.room.compiler.processing.XElement
 import androidx.room.compiler.processing.addOriginatingElement
+import com.squareup.kotlinpoet.KModifier
 import com.squareup.kotlinpoet.javapoet.JTypeSpec
 import com.squareup.kotlinpoet.javapoet.KTypeSpec
 import javax.lang.model.element.Modifier
@@ -39,6 +40,7 @@
         fun addType(typeSpec: XTypeSpec): Builder
         fun setPrimaryConstructor(functionSpec: XFunSpec): Builder
         fun setVisibility(visibility: VisibilityModifier)
+        fun addAbstractModifier(): Builder
         fun build(): XTypeSpec
 
         companion object {
@@ -89,16 +91,28 @@
     }
 
     companion object {
-        fun classBuilder(language: CodeLanguage, className: XClassName): Builder {
+        fun classBuilder(
+            language: CodeLanguage,
+            className: XClassName,
+            isOpen: Boolean = false
+        ): Builder {
             return when (language) {
                 CodeLanguage.JAVA -> JavaTypeSpec.Builder(
                     className = className,
-                    actual = JTypeSpec.classBuilder(className.java)
-                        .addModifiers(Modifier.FINAL)
+                    actual = JTypeSpec.classBuilder(className.java).apply {
+                        if (!isOpen) {
+                            addModifiers(Modifier.FINAL)
+                        }
+                    }
                 )
+
                 CodeLanguage.KOTLIN -> KotlinTypeSpec.Builder(
                     className = className,
-                    actual = KTypeSpec.classBuilder(className.kotlin)
+                    actual = KTypeSpec.classBuilder(className.kotlin).apply {
+                        if (isOpen) {
+                            addModifiers(KModifier.OPEN)
+                        }
+                    }
                 )
             }
         }
@@ -148,11 +162,4 @@
             }
         }
     }
-}
-
-// TODO(b/127483380): Temporary API for XPoet migration.
-// @Deprecated("Temporary API for XPoet migration.")
-fun XTypeSpec.toJavaPoet(): JTypeSpec {
-    check(this is JavaTypeSpec)
-    return this.actual
 }
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt
index 1774512e..453ad85 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt
@@ -33,17 +33,22 @@
     override val name: String,
     internal val actual: MethodSpec
 ) : JavaLang(), XFunSpec {
+    override fun toString() = actual.toString()
 
     internal class Builder(
         override val name: String,
         internal val actual: MethodSpec.Builder
     ) : JavaLang(), XFunSpec.Builder {
 
-        override fun addAnnotation(annotation: XAnnotationSpec) {
+        override fun addAnnotation(annotation: XAnnotationSpec) = apply {
             require(annotation is JavaAnnotationSpec)
             actual.addAnnotation(annotation.actual)
         }
 
+        override fun addAbstractModifier() = apply {
+            actual.addModifiers(Modifier.ABSTRACT)
+        }
+
         override fun addCode(code: XCodeBlock) = apply {
             require(code is JavaCodeBlock)
             actual.addCode(code.actual)
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaTypeSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaTypeSpec.kt
index 8a50dae..37d576c 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaTypeSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaTypeSpec.kt
@@ -25,11 +25,13 @@
 import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.codegen.XTypeSpec
 import com.squareup.kotlinpoet.javapoet.JTypeSpec
+import javax.lang.model.element.Modifier
 
 internal class JavaTypeSpec(
     private val _className: XClassName?,
     internal val actual: JTypeSpec
 ) : JavaLang(), XTypeSpec {
+    override fun toString() = actual.toString()
 
     override val className: XClassName
         get() {
@@ -78,5 +80,9 @@
         override fun build(): XTypeSpec {
             return JavaTypeSpec(className, actual.build())
         }
+
+        override fun addAbstractModifier(): XTypeSpec.Builder = apply {
+            actual.addModifiers(Modifier.ABSTRACT)
+        }
     }
 }
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinFunSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinFunSpec.kt
index 68b015a..1a3c0c4 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinFunSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinFunSpec.kt
@@ -29,17 +29,22 @@
     override val name: String,
     internal val actual: FunSpec
 ) : KotlinLang(), XFunSpec {
+    override fun toString() = actual.toString()
 
     internal class Builder(
         override val name: String,
         internal val actual: FunSpec.Builder
     ) : KotlinLang(), XFunSpec.Builder {
 
-        override fun addAnnotation(annotation: XAnnotationSpec) {
+        override fun addAnnotation(annotation: XAnnotationSpec) = apply {
             require(annotation is KotlinAnnotationSpec)
             actual.addAnnotation(annotation.actual)
         }
 
+        override fun addAbstractModifier() = apply {
+            actual.addModifiers(KModifier.ABSTRACT)
+        }
+
         override fun addCode(code: XCodeBlock) = apply {
             require(code is KotlinCodeBlock)
             actual.addCode(code.actual)
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinTypeSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinTypeSpec.kt
index 3151ff0..a819dd9 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinTypeSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinTypeSpec.kt
@@ -24,12 +24,14 @@
 import androidx.room.compiler.codegen.XPropertySpec
 import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.codegen.XTypeSpec
+import com.squareup.kotlinpoet.KModifier
 import com.squareup.kotlinpoet.javapoet.KTypeSpec
 
 internal class KotlinTypeSpec(
     private val _className: XClassName?,
     internal val actual: KTypeSpec
 ) : KotlinLang(), XTypeSpec {
+    override fun toString() = actual.toString()
 
     override val className: XClassName
         get() {
@@ -81,6 +83,10 @@
             actual.addModifiers(visibility.toKotlinVisibilityModifier())
         }
 
+        override fun addAbstractModifier(): XTypeSpec.Builder = apply {
+            actual.addModifiers(KModifier.ABSTRACT)
+        }
+
         override fun build(): XTypeSpec {
             return KotlinTypeSpec(className, actual.build())
         }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
index 6d87e42..7b781ec 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
@@ -191,7 +191,9 @@
                 addModifiers(Modifier.PROTECTED)
             }
             addAnnotation(Override::class.java)
-            varargs(executableElement.isVarArgs())
+            // In Java, only the last argument can be a vararg so for suspend functions, it is never
+            // a vararg function.
+            varargs(!executableElement.isSuspendFunction() && executableElement.isVarArgs())
             executableElement.thrownTypes.forEach {
                 addException(it.asTypeName().java)
             }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/KotlinPoetExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/KotlinPoetExt.kt
index 87ab8c2..d8258a7 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/KotlinPoetExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/KotlinPoetExt.kt
@@ -76,16 +76,17 @@
             }
             // TODO(b/251316420): Add type variable names
             val isVarArgs = executableElement.isVarArgs()
-            resolvedType.parameterTypes.let {
+            val parameterTypes = resolvedType.parameterTypes.let {
                 // Drop the synthetic Continuation param of suspend functions, always at the last
                 // position.
                 // TODO(b/254135327): Revisit with the introduction of a target language.
                 if (resolvedType.isSuspendFunction()) it.dropLast(1) else it
-            }.forEachIndexed { index, paramType ->
+            }
+            parameterTypes.forEachIndexed { index, paramType ->
                 val typeName: XTypeName
                 val modifiers: Array<KModifier>
                 // TODO(b/253268357): In Kotlin the vararg is not always the last param
-                if (isVarArgs && index == resolvedType.parameterTypes.size - 1) {
+                if (isVarArgs && index == parameterTypes.size - 1) {
                     typeName = (paramType as XArrayType).componentType.asTypeName()
                     modifiers = arrayOf(KModifier.VARARG)
                 } else {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt
index 9256458..b1a65c0 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt
@@ -158,11 +158,16 @@
      * already [XNullability.NONNULL].
      */
     fun makeNonNullable(): XType
+}
 
-    /**
-     * Returns true if this type is a type variable.
-     */
-    fun isTypeVariable(): Boolean
+/**
+ * Returns true if this type is a [XTypeVariableType].
+ */
+fun XType.isTypeVariable(): Boolean {
+    contract {
+        returns(true) implies (this@isTypeVariable is XTypeVariableType)
+    }
+    return this is XTypeVariableType
 }
 
 /**
@@ -209,16 +214,16 @@
 }
 
 /**
- * Returns `true` if this is a primitive or boxed it
+ * Returns `true` if this is a primitive or boxed int
  */
 fun XType.isInt(): Boolean = asTypeName() == XTypeName.PRIMITIVE_INT ||
-    asTypeName().equalsIgnoreNullability(KnownTypeNames.BOXED_INT)
+    asTypeName().equalsIgnoreNullability(XTypeName.BOXED_INT)
 
 /**
  * Returns `true` if this is a primitive or boxed long
  */
 fun XType.isLong(): Boolean = asTypeName() == XTypeName.PRIMITIVE_LONG ||
-    asTypeName().equalsIgnoreNullability(KnownTypeNames.BOXED_LONG)
+    asTypeName().equalsIgnoreNullability(XTypeName.BOXED_LONG)
 
 /**
  * Returns `true` if this is `void`
@@ -239,12 +244,39 @@
  * Returns `true` if this represents a `byte`.
  */
 fun XType.isByte(): Boolean = asTypeName() == XTypeName.PRIMITIVE_BYTE ||
-    asTypeName().equalsIgnoreNullability(KnownTypeNames.BOXED_BYTE)
+    asTypeName().equalsIgnoreNullability(XTypeName.BOXED_BYTE)
+
+/**
+ * Returns `true` if this represents a `short`.
+ */
+fun XType.isShort(): Boolean = asTypeName() == XTypeName.PRIMITIVE_SHORT ||
+    asTypeName().equalsIgnoreNullability(XTypeName.BOXED_SHORT)
+
+/**
+ * Returns `true` if this represents a `float`.
+ */
+fun XType.isFloat(): Boolean = asTypeName() == XTypeName.PRIMITIVE_FLOAT ||
+    asTypeName().equalsIgnoreNullability(XTypeName.BOXED_FLOAT)
+
+/**
+ * Returns `true` if this represents a `double`.
+ */
+fun XType.isDouble(): Boolean = asTypeName() == XTypeName.PRIMITIVE_DOUBLE ||
+    asTypeName().equalsIgnoreNullability(XTypeName.BOXED_DOUBLE)
+
+/**
+ * Returns `true` if this represents a `boolean`.
+ */
+fun XType.isBoolean(): Boolean = asTypeName() == XTypeName.PRIMITIVE_BOOLEAN ||
+    asTypeName().equalsIgnoreNullability(XTypeName.BOXED_BOOLEAN)
+
+/**
+ * Returns `true` if this represents a `char`.
+ */
+fun XType.isChar(): Boolean = asTypeName() == XTypeName.PRIMITIVE_CHAR ||
+    asTypeName().equalsIgnoreNullability(XTypeName.BOXED_CHAR)
 
 internal object KnownTypeNames {
     val BOXED_VOID = Void::class.asClassName()
-    val BOXED_INT = Int::class.asClassName()
-    val BOXED_LONG = Long::class.asClassName()
-    val BOXED_BYTE = Byte::class.asClassName()
     val KOTLIN_UNIT = XClassName.get("kotlin", "Unit")
 }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XTypeVariableType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XTypeVariableType.kt
new file mode 100644
index 0000000..d061e3f
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XTypeVariableType.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.room.compiler.processing
+
+/**
+ * Represents a type variable.
+ *
+ * @see [javax.lang.model.type.TypeVariable]
+ * @see [com.google.devtools.ksp.symbol.KSTypeParameter]
+ */
+interface XTypeVariableType : XType {
+    /**
+     * The upper bounds of the type variable.
+     *
+     * Note that this model differs a bit from the Javac model where
+     * [javax.lang.model.type.TypeVariable] always has a single `upperBound` which may end up
+     * resolving to an [javax.lang.model.type.IntersectionType]. Instead, this model is closer to
+     * the KSP model where the type variable may return multiple upper bounds when an intersection
+     * type exists rather than representing the intersection type as a unique type in the model.
+     */
+    val upperBounds: List<XType>
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt
index 681fa6e..1427653 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt
@@ -18,7 +18,8 @@
 
 import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XType
-import androidx.room.compiler.processing.javac.kotlin.KmType
+import androidx.room.compiler.processing.javac.kotlin.KmTypeContainer
+import androidx.room.compiler.processing.javac.kotlin.nullability
 import javax.lang.model.type.TypeMirror
 
 /**
@@ -28,7 +29,7 @@
     env: JavacProcessingEnv,
     typeMirror: TypeMirror,
     nullability: XNullability?,
-    override val kotlinType: KmType?
+    override val kotlinType: KmTypeContainer?
 ) : JavacType(
     env, typeMirror, nullability
 ) {
@@ -45,7 +46,7 @@
     constructor(
         env: JavacProcessingEnv,
         typeMirror: TypeMirror,
-        kotlinType: KmType
+        kotlinType: KmTypeContainer
     ) : this(
         env = env,
         typeMirror = typeMirror,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt
index b9c3ce7..5d0ba49 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt
@@ -21,7 +21,8 @@
 import androidx.room.compiler.processing.XArrayType
 import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XType
-import androidx.room.compiler.processing.javac.kotlin.KmType
+import androidx.room.compiler.processing.javac.kotlin.KmTypeContainer
+import androidx.room.compiler.processing.javac.kotlin.nullability
 import javax.lang.model.type.ArrayType
 
 internal class JavacArrayType private constructor(
@@ -29,7 +30,7 @@
     override val typeMirror: ArrayType,
     nullability: XNullability?,
     private val knownComponentNullability: XNullability?,
-    override val kotlinType: KmType?
+    override val kotlinType: KmTypeContainer?
 ) : JavacType(
     env, typeMirror, nullability
 ), XArrayType {
@@ -48,7 +49,7 @@
     constructor(
         env: JavacProcessingEnv,
         typeMirror: ArrayType,
-        kotlinType: KmType
+        kotlinType: KmTypeContainer
     ) : this(
         env = env,
         typeMirror = typeMirror,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt
index 135970e..e3dda74 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt
@@ -20,7 +20,7 @@
 import androidx.room.compiler.processing.XConstructorType
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeParameterElement
-import androidx.room.compiler.processing.javac.kotlin.KmConstructor
+import androidx.room.compiler.processing.javac.kotlin.KmConstructorContainer
 import com.google.auto.common.MoreTypes
 import javax.lang.model.element.ElementKind
 import javax.lang.model.element.ExecutableElement
@@ -79,7 +79,7 @@
         }
     }
 
-    override val kotlinMetadata: KmConstructor? by lazy {
+    override val kotlinMetadata: KmConstructorContainer? by lazy {
         (enclosingElement as? JavacTypeElement)?.kotlinMetadata?.getConstructorMetadata(element)
     }
 }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt
index b760dc2..a00819f 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt
@@ -17,7 +17,8 @@
 package androidx.room.compiler.processing.javac
 
 import androidx.room.compiler.processing.XNullability
-import androidx.room.compiler.processing.javac.kotlin.KmType
+import androidx.room.compiler.processing.javac.kotlin.KmTypeContainer
+import androidx.room.compiler.processing.javac.kotlin.nullability
 import javax.lang.model.type.DeclaredType
 
 /**
@@ -29,7 +30,7 @@
     env: JavacProcessingEnv,
     override val typeMirror: DeclaredType,
     nullability: XNullability?,
-    override val kotlinType: KmType?
+    override val kotlinType: KmTypeContainer?
 ) : JavacType(
     env, typeMirror, nullability
 ) {
@@ -46,7 +47,7 @@
     constructor(
         env: JavacProcessingEnv,
         typeMirror: DeclaredType,
-        kotlinType: KmType
+        kotlinType: KmTypeContainer
     ) : this(
         env = env,
         typeMirror = typeMirror,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
index b66283f..7d8bbe1 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
@@ -22,7 +22,7 @@
 import androidx.room.compiler.processing.XElement
 import androidx.room.compiler.processing.XEquality
 import androidx.room.compiler.processing.XHasModifiers
-import androidx.room.compiler.processing.javac.kotlin.KmElement
+import androidx.room.compiler.processing.javac.kotlin.KmFlags
 import androidx.room.compiler.processing.unwrapRepeatedAnnotationsFromContainer
 import com.google.auto.common.MoreElements
 import com.google.auto.common.MoreElements.isAnnotationPresent
@@ -39,7 +39,7 @@
     open val element: Element
 ) : XElement, XEquality, InternalXAnnotated, XHasModifiers {
 
-    abstract val kotlinMetadata: KmElement?
+    abstract val kotlinMetadata: KmFlags?
 
     override fun <T : Annotation> getAnnotations(
         annotation: KClass<T>,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt
index 7c62191..20e38a3 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt
@@ -18,7 +18,7 @@
 
 import androidx.room.compiler.processing.XExecutableElement
 import androidx.room.compiler.processing.XNullability
-import androidx.room.compiler.processing.javac.kotlin.KmExecutable
+import androidx.room.compiler.processing.javac.kotlin.KmFunctionContainer
 import androidx.room.compiler.processing.javac.kotlin.descriptor
 import javax.lang.model.element.ExecutableElement
 
@@ -26,7 +26,7 @@
     env: JavacProcessingEnv,
     override val element: ExecutableElement
 ) : JavacElement(env, element), XExecutableElement {
-    abstract override val kotlinMetadata: KmExecutable?
+    abstract override val kotlinMetadata: KmFunctionContainer?
 
     val descriptor by lazy {
         element.descriptor()
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFieldElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFieldElement.kt
index a79e0fa..88f7332 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFieldElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFieldElement.kt
@@ -17,8 +17,8 @@
 package androidx.room.compiler.processing.javac
 
 import androidx.room.compiler.processing.XFieldElement
-import androidx.room.compiler.processing.javac.kotlin.KmProperty
-import androidx.room.compiler.processing.javac.kotlin.KmType
+import androidx.room.compiler.processing.javac.kotlin.KmPropertyContainer
+import androidx.room.compiler.processing.javac.kotlin.KmTypeContainer
 import javax.lang.model.element.VariableElement
 
 internal class JavacFieldElement(
@@ -26,11 +26,11 @@
     element: VariableElement
 ) : JavacVariableElement(env, element), XFieldElement {
 
-    override val kotlinMetadata: KmProperty? by lazy {
+    override val kotlinMetadata: KmPropertyContainer? by lazy {
         (enclosingElement as? JavacTypeElement)?.kotlinMetadata?.getPropertyMetadata(name)
     }
 
-    override val kotlinType: KmType?
+    override val kotlinType: KmTypeContainer?
         get() = kotlinMetadata?.type
 
     override val enclosingElement: JavacTypeElement by lazy {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt
index 4af85f3..3a3fcfa 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt
@@ -22,7 +22,7 @@
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeElement
 import androidx.room.compiler.processing.XTypeParameterElement
-import androidx.room.compiler.processing.javac.kotlin.KmFunction
+import androidx.room.compiler.processing.javac.kotlin.KmFunctionContainer
 import com.google.auto.common.MoreElements
 import com.google.auto.common.MoreTypes
 import javax.lang.model.element.ElementKind
@@ -51,8 +51,8 @@
 
     override val typeParameters: List<XTypeParameterElement> by lazy {
         element.typeParameters.mapIndexed { index, typeParameter ->
-            val typeArgument = kotlinMetadata?.typeArguments?.get(index)
-            JavacTypeParameterElement(env, this, typeParameter, typeArgument)
+            val typeParameterMetadata = kotlinMetadata?.typeParameters?.get(index)
+            JavacTypeParameterElement(env, this, typeParameter, typeParameterMetadata)
         }
     }
 
@@ -71,7 +71,7 @@
         }
     }
 
-    override val kotlinMetadata: KmFunction? by lazy {
+    override val kotlinMetadata: KmFunctionContainer? by lazy {
         (enclosingElement as? JavacTypeElement)?.kotlinMetadata?.getFunctionMetadata(element)
     }
 
@@ -178,5 +178,5 @@
         }
     }
 
-    override fun isKotlinPropertyMethod() = kotlinMetadata?.isPropertyFunction ?: false
+    override fun isKotlinPropertyMethod() = kotlinMetadata?.isPropertyFunction() ?: false
 }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodParameter.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodParameter.kt
index ec44f2f..27aeaa5 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodParameter.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodParameter.kt
@@ -18,8 +18,8 @@
 
 import androidx.room.compiler.processing.XExecutableParameterElement
 import androidx.room.compiler.processing.XMemberContainer
-import androidx.room.compiler.processing.javac.kotlin.KmType
-import androidx.room.compiler.processing.javac.kotlin.KmValueParameter
+import androidx.room.compiler.processing.javac.kotlin.KmTypeContainer
+import androidx.room.compiler.processing.javac.kotlin.KmValueParameterContainer
 import androidx.room.compiler.processing.util.sanitizeAsJavaParameterName
 import javax.lang.model.element.VariableElement
 
@@ -27,7 +27,7 @@
     env: JavacProcessingEnv,
     override val enclosingElement: JavacExecutableElement,
     element: VariableElement,
-    kotlinMetadataFactory: () -> KmValueParameter?,
+    kotlinMetadataFactory: () -> KmValueParameterContainer?,
     val argIndex: Int
 ) : JavacVariableElement(env, element), XExecutableParameterElement {
 
@@ -38,7 +38,7 @@
             argIndex = argIndex
         )
 
-    override val kotlinType: KmType?
+    override val kotlinType: KmTypeContainer?
         get() = kotlinMetadata?.type
 
     override val hasDefaultValue: Boolean
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
index c9c5393..cec6636 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
@@ -23,7 +23,7 @@
 import androidx.room.compiler.processing.XProcessingEnvConfig
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeElement
-import androidx.room.compiler.processing.javac.kotlin.KmType
+import androidx.room.compiler.processing.javac.kotlin.KmTypeContainer
 import com.google.auto.common.GeneratedAnnotations
 import com.google.auto.common.MoreTypes
 import java.util.Locale
@@ -174,7 +174,7 @@
      */
     inline fun <reified T : JavacType> wrap(
         typeMirror: TypeMirror,
-        kotlinType: KmType?,
+        kotlinType: KmTypeContainer?,
         elementNullability: XNullability?
     ): T {
         return when (typeMirror.kind) {
@@ -225,6 +225,29 @@
                         )
                     }
                 }
+            TypeKind.TYPEVAR ->
+                when {
+                    kotlinType != null -> {
+                        JavacTypeVariableType(
+                            env = this,
+                            typeMirror = MoreTypes.asTypeVariable(typeMirror),
+                            kotlinType = kotlinType
+                        )
+                    }
+                    elementNullability != null -> {
+                        JavacTypeVariableType(
+                            env = this,
+                            typeMirror = MoreTypes.asTypeVariable(typeMirror),
+                            nullability = elementNullability
+                        )
+                    }
+                    else -> {
+                        JavacTypeVariableType(
+                            env = this,
+                            typeMirror = MoreTypes.asTypeVariable(typeMirror)
+                        )
+                    }
+                }
             else ->
                 when {
                     kotlinType != null -> {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
index e5463e1..3dd55ea 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
@@ -21,8 +21,8 @@
 import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XRawType
 import androidx.room.compiler.processing.XType
-import androidx.room.compiler.processing.javac.kotlin.KmType
-import androidx.room.compiler.processing.javac.kotlin.KotlinMetadataElement
+import androidx.room.compiler.processing.javac.kotlin.KmClassContainer
+import androidx.room.compiler.processing.javac.kotlin.KmTypeContainer
 import androidx.room.compiler.processing.ksp.ERROR_JTYPE_NAME
 import androidx.room.compiler.processing.safeTypeName
 import com.google.auto.common.MoreTypes
@@ -35,8 +35,8 @@
     open val typeMirror: TypeMirror,
     internal val maybeNullability: XNullability?,
 ) : XType, XEquality {
-    // Kotlin type information about the type if this type is driven from kotlin code.
-    abstract val kotlinType: KmType?
+    // Kotlin type information about the type if this type is driven from Kotlin code.
+    abstract val kotlinType: KmTypeContainer?
 
     override val rawType: XRawType by lazy {
         JavacRawType(env, this)
@@ -48,7 +48,7 @@
             val element = MoreTypes.asTypeElement(it)
             env.wrap<JavacType>(
                 typeMirror = it,
-                kotlinType = KotlinMetadataElement.createFor(element)?.kmType,
+                kotlinType = KmClassContainer.createFor(env, element)?.type,
                 elementNullability = element.nullability
             )
         }
@@ -200,6 +200,4 @@
                 "TypeMirror#toXProcessing(XProcessingEnv)?"
         )
     }
-
-    override fun isTypeVariable() = typeMirror.kind == TypeKind.TYPEVAR
 }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
index dc868ae..5b09016 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
@@ -29,7 +29,7 @@
 import androidx.room.compiler.processing.collectAllMethods
 import androidx.room.compiler.processing.collectFieldsIncludingPrivateSupers
 import androidx.room.compiler.processing.filterMethodsByConfig
-import androidx.room.compiler.processing.javac.kotlin.KotlinMetadataElement
+import androidx.room.compiler.processing.javac.kotlin.KmClassContainer
 import androidx.room.compiler.processing.util.MemoizedSequence
 import com.google.auto.common.MoreElements
 import com.google.auto.common.MoreTypes
@@ -53,7 +53,7 @@
         get() = MoreElements.getPackage(element).qualifiedName.toString()
 
     override val kotlinMetadata by lazy {
-        KotlinMetadataElement.createFor(element)
+        KmClassContainer.createFor(env, element)
     }
 
     override val qualifiedName by lazy {
@@ -86,8 +86,8 @@
 
     override val typeParameters: List<XTypeParameterElement> by lazy {
         element.typeParameters.mapIndexed { index, typeParameter ->
-            val typeArgument = kotlinMetadata?.kmType?.typeArguments?.get(index)
-            JavacTypeParameterElement(env, this, typeParameter, typeArgument)
+            val typeParameterMetadata = kotlinMetadata?.typeParameters?.get(index)
+            JavacTypeParameterElement(env, this, typeParameter, typeParameterMetadata)
         }
     }
 
@@ -151,7 +151,7 @@
     }
 
     override fun findPrimaryConstructor(): JavacConstructorElement? {
-        val primarySignature = kotlinMetadata?.findPrimaryConstructorSignature() ?: return null
+        val primarySignature = kotlinMetadata?.primaryConstructorSignature ?: return null
         return getConstructors().firstOrNull {
             primarySignature == it.descriptor
         }
@@ -194,7 +194,7 @@
     override val type: JavacDeclaredType by lazy {
         env.wrap(
             typeMirror = element.asType(),
-            kotlinType = kotlinMetadata?.kmType,
+            kotlinType = kotlinMetadata?.type,
             elementNullability = element.nullability
         )
     }
@@ -222,7 +222,7 @@
             val element = MoreTypes.asTypeElement(it)
             env.wrap<JavacType>(
                 typeMirror = it,
-                kotlinType = KotlinMetadataElement.createFor(element)?.kmType,
+                kotlinType = KmClassContainer.createFor(env, element)?.type,
                 elementNullability = element.nullability
             )
         }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt
index 5f15f95..ad42e32 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt
@@ -21,7 +21,7 @@
 import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeParameterElement
-import androidx.room.compiler.processing.javac.kotlin.KmType
+import androidx.room.compiler.processing.javac.kotlin.KmTypeParameterContainer
 import com.squareup.javapoet.TypeVariableName
 import javax.lang.model.element.TypeParameterElement
 
@@ -29,9 +29,8 @@
     env: JavacProcessingEnv,
     override val enclosingElement: XElement,
     override val element: TypeParameterElement,
-    private val kotlinType: KmType?,
+    override val kotlinMetadata: KmTypeParameterContainer?,
 ) : JavacElement(env, element), XTypeParameterElement {
-    override val kotlinMetadata = null
 
     override val name: String
         get() = element.simpleName.toString()
@@ -41,7 +40,9 @@
     }
 
     override val bounds: List<XType> by lazy {
-        element.bounds.map { env.wrap(it, kotlinType?.extendsBound, XNullability.UNKNOWN) }
+        element.bounds.mapIndexed { i, bound ->
+            env.wrap(bound, kotlinMetadata?.upperBounds?.getOrNull(i), XNullability.UNKNOWN)
+        }
     }
 
     override val fallbackLocationText: String
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeVariableType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeVariableType.kt
new file mode 100644
index 0000000..f8c3f78
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeVariableType.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.room.compiler.processing.javac
+
+import androidx.room.compiler.processing.XNullability
+import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XTypeVariableType
+import androidx.room.compiler.processing.javac.kotlin.KmTypeContainer
+import androidx.room.compiler.processing.javac.kotlin.nullability
+import com.google.auto.common.MoreTypes.asIntersection
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeVariable
+
+internal class JavacTypeVariableType(
+    env: JavacProcessingEnv,
+    override val typeMirror: TypeVariable,
+    nullability: XNullability?,
+    override val kotlinType: KmTypeContainer?
+) : JavacType(env, typeMirror, nullability), XTypeVariableType {
+    constructor(
+        env: JavacProcessingEnv,
+        typeMirror: TypeVariable
+    ) : this(
+        env = env,
+        typeMirror = typeMirror,
+        nullability = null,
+        kotlinType = null
+    )
+
+    constructor(
+        env: JavacProcessingEnv,
+        typeMirror: TypeVariable,
+        kotlinType: KmTypeContainer
+    ) : this(
+        env = env,
+        typeMirror = typeMirror,
+        nullability = kotlinType.nullability,
+        kotlinType = kotlinType
+    )
+
+    constructor(
+        env: JavacProcessingEnv,
+        typeMirror: TypeVariable,
+        nullability: XNullability
+    ) : this(
+        env = env,
+        typeMirror = typeMirror,
+        nullability = nullability,
+        kotlinType = null
+    )
+
+    override val equalityItems by lazy {
+        arrayOf(typeMirror)
+    }
+
+    override val typeArguments: List<XType>
+        get() = emptyList()
+
+    override val upperBounds: List<XType>
+        get() {
+            return if (typeMirror.upperBound.kind == TypeKind.INTERSECTION) {
+                asIntersection(typeMirror.upperBound).bounds.mapIndexed { i, bound ->
+                    env.wrap(
+                        typeMirror = bound,
+                        kotlinType = kotlinType?.upperBounds?.getOrNull(i),
+                        elementNullability = maybeNullability
+                    )
+                }
+            } else {
+                listOf(
+                    env.wrap(
+                        typeMirror = typeMirror.upperBound,
+                        // If this isn't an intersection type then there is only 1 upper bound
+                        kotlinType = kotlinType?.upperBounds?.singleOrNull(),
+                        elementNullability = maybeNullability
+                    )
+                )
+            }
+        }
+
+    override fun copyWithNullability(nullability: XNullability): JavacTypeVariableType {
+        return JavacTypeVariableType(
+            env = env,
+            typeMirror = typeMirror,
+            kotlinType = kotlinType,
+            nullability = nullability
+        )
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacVariableElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacVariableElement.kt
index daec9e9..58e2973 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacVariableElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacVariableElement.kt
@@ -18,7 +18,7 @@
 
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XVariableElement
-import androidx.room.compiler.processing.javac.kotlin.KmType
+import androidx.room.compiler.processing.javac.kotlin.KmTypeContainer
 import com.google.auto.common.MoreTypes
 import javax.lang.model.element.VariableElement
 
@@ -27,7 +27,7 @@
     override val element: VariableElement
 ) : JavacElement(env, element), XVariableElement {
 
-    abstract val kotlinType: KmType?
+    abstract val kotlinType: KmTypeContainer?
 
     override val name: String
         get() = element.simpleName.toString()
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/KmTypeExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/KmTypeExt.kt
deleted file mode 100644
index 8725cbe..0000000
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/KmTypeExt.kt
+++ /dev/null
@@ -1,28 +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.room.compiler.processing.javac
-
-import androidx.room.compiler.processing.XNullability
-import androidx.room.compiler.processing.javac.kotlin.KmType
-
-internal val KmType.nullability: XNullability
-    get() = if (isNullable()) {
-        XNullability.NULLABLE
-    } else {
-        // if there is an upper bound information, use its nullability (e.g. it might be T : Foo?)
-        extendsBound?.nullability ?: XNullability.NONNULL
-    }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/JvmDescriptorUtils.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/JvmDescriptorUtils.kt
index dbfc124..4453eb7 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/JvmDescriptorUtils.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/JvmDescriptorUtils.kt
@@ -16,7 +16,6 @@
 
 package androidx.room.compiler.processing.javac.kotlin
 
-import com.google.auto.common.MoreTypes
 import com.squareup.javapoet.ArrayTypeName
 import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.TypeName
@@ -53,51 +52,9 @@
  *
  * For reference, see the [JVM specification, section 4.3.3](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3.3)
  */
-internal fun ExecutableElement.descriptor() =
-    "$simpleName${MoreTypes.asExecutable(asType()).descriptor()}"
+internal fun ExecutableElement.descriptor() = "$simpleName${asType().descriptor()}"
 
-/**
- * Returns the name of this [TypeElement] in its "internal form".
- *
- * For reference, see the [JVM specification, section 4.2](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.2).
- */
-internal val Element.internalName: String
-    get() = when (this) {
-        is TypeElement ->
-            when (nestingKind) {
-                NestingKind.TOP_LEVEL ->
-                    qualifiedName.toString().replace('.', '/')
-                NestingKind.MEMBER, NestingKind.LOCAL ->
-                    enclosingElement.internalName + "$" + simpleName
-                NestingKind.ANONYMOUS ->
-                    error("Unsupported nesting $nestingKind")
-                else ->
-                    error("Unsupported, nestingKind == null")
-            }
-        is ExecutableElement -> enclosingElement.internalName
-        is QualifiedNameable -> qualifiedName.toString().replace('.', '/')
-        else -> simpleName.toString()
-    }
-
-@Suppress("unused")
-internal val NoType.descriptor: String
-    get() = "V"
-
-internal val DeclaredType.descriptor: String
-    get() = "L" + asElement().internalName + ";"
-
-internal val PrimitiveType.descriptor: String
-    get() = when (this.kind) {
-        TypeKind.BYTE -> "B"
-        TypeKind.CHAR -> "C"
-        TypeKind.DOUBLE -> "D"
-        TypeKind.FLOAT -> "F"
-        TypeKind.INT -> "I"
-        TypeKind.LONG -> "J"
-        TypeKind.SHORT -> "S"
-        TypeKind.BOOLEAN -> "Z"
-        else -> error("Unknown primitive type $this")
-    }
+private fun TypeMirror.descriptor() = JvmDescriptorTypeVisitor.visit(this)
 
 // see https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3.2-200
 internal fun String.typeNameFromJvmSignature(): TypeName {
@@ -146,61 +103,78 @@
     }
 }
 
-internal fun TypeMirror.descriptor(): String = accept(JvmDescriptorTypeVisitor, Unit)
-
-@Suppress("unused")
-internal fun WildcardType.descriptor(): String = ""
-
-// The erasure of a type variable is the erasure of its leftmost bound. - JVM Spec Sec 4.6
-internal fun TypeVariable.descriptor(): String = this.upperBound.descriptor()
-
-// For a type variable with multiple bounds: "the erasure of a type variable is determined by
-// the first type in its bound" - JVM Spec Sec 4.4
-internal fun IntersectionType.descriptor(): String =
-    this.bounds[0].descriptor()
-
-internal fun ArrayType.descriptor(): String =
-    "[" + componentType.descriptor()
-
-internal fun ExecutableType.descriptor(): String {
-    val parameterDescriptors =
-        parameterTypes.joinToString(separator = "") { it.descriptor() }
-    val returnDescriptor = returnType.descriptor()
-    return "($parameterDescriptors)$returnDescriptor"
-}
-
 /**
  * When applied over a type, it returns either:
  * + a "field descriptor", for example: `Ljava/lang/Object;`
  * + a "method descriptor", for example: `(Ljava/lang/Object;)Z`
  *
- * The easiest way to use this is through [TypeMirror.descriptor]
- *
  * For reference, see the [JVM specification, section 4.3](http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3).
  */
-internal object JvmDescriptorTypeVisitor : AbstractTypeVisitor8<String, Unit>() {
+private object JvmDescriptorTypeVisitor : AbstractTypeVisitor8<String, Any?>() {
 
-    override fun visitNoType(t: NoType, u: Unit): String = t.descriptor
+    override fun visitNoType(t: NoType, u: Any?): String = "V"
 
-    override fun visitDeclared(t: DeclaredType, u: Unit): String = t.descriptor
+    override fun visitDeclared(t: DeclaredType, u: Any?): String = "L${t.asElement().internalName};"
 
-    override fun visitPrimitive(t: PrimitiveType, u: Unit): String = t.descriptor
+    override fun visitPrimitive(t: PrimitiveType, u: Any?): String {
+        return when (t.kind) {
+            TypeKind.BYTE -> "B"
+            TypeKind.CHAR -> "C"
+            TypeKind.DOUBLE -> "D"
+            TypeKind.FLOAT -> "F"
+            TypeKind.INT -> "I"
+            TypeKind.LONG -> "J"
+            TypeKind.SHORT -> "S"
+            TypeKind.BOOLEAN -> "Z"
+            else -> error("Unknown primitive type $this")
+        }
+    }
 
-    override fun visitArray(t: ArrayType, u: Unit): String = t.descriptor()
+    override fun visitArray(t: ArrayType, u: Any?): String = "[" + visit(t.componentType)
 
-    override fun visitWildcard(t: WildcardType, u: Unit): String = t.descriptor()
+    override fun visitWildcard(t: WildcardType, u: Any?): String = visitUnknown(t, u)
 
-    override fun visitExecutable(t: ExecutableType, u: Unit): String = t.descriptor()
+    override fun visitExecutable(t: ExecutableType, u: Any?): String {
+        val parameterDescriptors = t.parameterTypes.joinToString("") { visit(it) }
+        val returnDescriptor = visit(t.returnType)
+        return "($parameterDescriptors)$returnDescriptor"
+    }
 
-    override fun visitTypeVariable(t: TypeVariable, u: Unit): String = t.descriptor()
+    override fun visitTypeVariable(t: TypeVariable, u: Any?): String = visit(t.upperBound)
 
-    override fun visitNull(t: NullType, u: Unit): String = visitUnknown(t, u)
+    override fun visitNull(t: NullType, u: Any?): String = visitUnknown(t, u)
 
-    override fun visitError(t: ErrorType, u: Unit): String = visitDeclared(t, u)
+    override fun visitError(t: ErrorType, u: Any?): String = visitDeclared(t, u)
 
-    override fun visitIntersection(t: IntersectionType, u: Unit) = t.descriptor()
+    // For a type variable with multiple bounds: "the erasure of a type variable is determined
+    // by the first type in its bound" - JLS Sec 4.4
+    // See https://docs.oracle.com/javase/specs/jls/se16/html/jls-4.html#jls-4.4
+    override fun visitIntersection(t: IntersectionType, u: Any?): String = visit(t.bounds[0])
 
-    override fun visitUnion(t: UnionType, u: Unit) = visitUnknown(t, u)
+    override fun visitUnion(t: UnionType, u: Any?): String = visitUnknown(t, u)
 
-    override fun visitUnknown(t: TypeMirror, u: Unit): String = error("Unsupported type $t")
+    override fun visitUnknown(t: TypeMirror, u: Any?): String = error("Unsupported type $t")
+
+    /**
+     * Returns the name of this [TypeElement] in its "internal form".
+     *
+     * For reference, see the [JVM specification, section 4.2](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.2).
+     */
+    private val Element.internalName: String
+        get() = when (this) {
+            is TypeElement ->
+                when (nestingKind) {
+                    NestingKind.TOP_LEVEL ->
+                        qualifiedName.toString().replace('.', '/')
+                    NestingKind.MEMBER, NestingKind.LOCAL ->
+                        enclosingElement.internalName + "$" + simpleName
+                    NestingKind.ANONYMOUS ->
+                        error("Unsupported nesting $nestingKind")
+                    else ->
+                        error("Unsupported, nestingKind == null")
+                }
+            is ExecutableElement -> enclosingElement.internalName
+            is QualifiedNameable -> qualifiedName.toString().replace('.', '/')
+            else -> simpleName.toString()
+        }
 }
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt
index f6e76c6..fdf221a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt
@@ -16,523 +16,361 @@
 
 package androidx.room.compiler.processing.javac.kotlin
 
+import androidx.room.compiler.processing.XNullability
+import androidx.room.compiler.processing.javac.JavacProcessingEnv
 import androidx.room.compiler.processing.util.sanitizeAsJavaParameterName
-import kotlinx.metadata.ClassName
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.ExecutableElement
+import javax.tools.Diagnostic
 import kotlinx.metadata.Flag
 import kotlinx.metadata.Flags
-import kotlinx.metadata.KmAnnotation
-import kotlinx.metadata.KmClassVisitor
-import kotlinx.metadata.KmConstructorExtensionVisitor
-import kotlinx.metadata.KmConstructorVisitor
-import kotlinx.metadata.KmExtensionType
-import kotlinx.metadata.KmFunctionExtensionVisitor
-import kotlinx.metadata.KmFunctionVisitor
-import kotlinx.metadata.KmPropertyExtensionVisitor
-import kotlinx.metadata.KmPropertyVisitor
-import kotlinx.metadata.KmTypeExtensionVisitor
-import kotlinx.metadata.KmTypeParameterVisitor
-import kotlinx.metadata.KmTypeVisitor
-import kotlinx.metadata.KmValueParameterVisitor
-import kotlinx.metadata.KmVariance
-import kotlinx.metadata.jvm.JvmConstructorExtensionVisitor
-import kotlinx.metadata.jvm.JvmFieldSignature
-import kotlinx.metadata.jvm.JvmFunctionExtensionVisitor
-import kotlinx.metadata.jvm.JvmMethodSignature
-import kotlinx.metadata.jvm.JvmPropertyExtensionVisitor
-import kotlinx.metadata.jvm.JvmTypeExtensionVisitor
+import kotlinx.metadata.KmClass
+import kotlinx.metadata.KmConstructor
+import kotlinx.metadata.KmFunction
+import kotlinx.metadata.KmProperty
+import kotlinx.metadata.KmType
+import kotlinx.metadata.KmTypeParameter
+import kotlinx.metadata.KmValueParameter
 import kotlinx.metadata.jvm.KotlinClassMetadata
+import kotlinx.metadata.jvm.annotations
+import kotlinx.metadata.jvm.getterSignature
+import kotlinx.metadata.jvm.setterSignature
+import kotlinx.metadata.jvm.signature
 
-/** Represents the kotlin metadata for a given element. */
-internal interface KmElement {
+internal interface KmFlags {
     val flags: Flags
 }
 
-/** Represents a function or constructor. */
-internal interface KmExecutable : KmElement {
-    val parameters: List<KmValueParameter>
+internal class KmClassContainer(
+    private val kmClass: KmClass
+) : KmFlags {
+    override val flags: Flags
+        get() = kmClass.flags
+
+    val type: KmTypeContainer by lazy {
+        KmTypeContainer(
+            kmType = KmType(flags),
+            typeArguments = kmClass.typeParameters.map { kmTypeParameter ->
+                KmTypeContainer(
+                    kmType = KmType(kmTypeParameter.flags),
+                    typeArguments = emptyList(),
+                    upperBounds = kmTypeParameter.upperBounds.map { it.asContainer() }
+                )
+            }
+        )
+    }
+
+    val superType: KmTypeContainer? by lazy {
+        kmClass.supertypes.firstOrNull()?.asContainer()
+    }
+
+    val typeParameters: List<KmTypeParameterContainer> by lazy {
+        kmClass.typeParameters.map { it.asContainer() }
+    }
+
+    private val functionList: List<KmFunctionContainer> by lazy {
+        kmClass.functions.map { it.asContainer() }
+    }
+
+    private val constructorList: List<KmConstructorContainer> by lazy {
+        kmClass.constructors.map { it.asContainer(type) }
+    }
+
+    private val propertyList: List<KmPropertyContainer> by lazy {
+        kmClass.properties.map { it.asContainer() }
+    }
+
+    val primaryConstructorSignature: String? by lazy {
+        constructorList.firstOrNull { it.isPrimary() }?.descriptor
+    }
+
+    fun isObject() = Flag.Class.IS_OBJECT(flags)
+    fun isCompanionObject() = Flag.Class.IS_COMPANION_OBJECT(flags)
+    fun isAnnotationClass() = Flag.Class.IS_ANNOTATION_CLASS(flags)
+    fun isClass() = Flag.Class.IS_CLASS(flags)
+    fun isInterface() = Flag.Class.IS_INTERFACE(flags)
+    fun isDataClass() = Flag.Class.IS_DATA(flags)
+    fun isValueClass() = Flag.Class.IS_VALUE(flags)
+    fun isFunctionalInterface() = Flag.Class.IS_FUN(flags)
+    fun isExpect() = Flag.Class.IS_EXPECT(flags)
+
+    fun getFunctionMetadata(method: ExecutableElement): KmFunctionContainer? {
+        check(method.kind == ElementKind.METHOD) {
+            "must pass an element type of method"
+        }
+        val methodSignature = method.descriptor()
+        functionList.firstOrNull { it.descriptor == methodSignature }?.let {
+            return it
+        }
+        // might be a property getter or setter
+        return propertyList.firstNotNullOfOrNull { property ->
+            when {
+                property.getter?.descriptor == methodSignature -> {
+                    property.getter
+                }
+
+                property.setter?.descriptor == methodSignature -> {
+                    property.setter
+                }
+
+                else -> {
+                    null
+                }
+            }
+        }
+    }
+
+    fun getConstructorMetadata(method: ExecutableElement): KmConstructorContainer? {
+        check(method.kind == ElementKind.CONSTRUCTOR) {
+            "must pass an element type of constructor"
+        }
+        val methodSignature = method.descriptor()
+        return constructorList.firstOrNull { it.descriptor == methodSignature }
+    }
+
+    fun getPropertyMetadata(propertyName: String): KmPropertyContainer? =
+        propertyList.firstOrNull { it.name == propertyName }
+
+    companion object {
+        /**
+         * Creates a [KmClassContainer] for the given element if it contains Kotlin metadata,
+         * otherwise this method returns null.
+         *
+         * Usually the [element] passed must represent a class. For example, if Kotlin metadata is
+         * desired for a method, then the containing class should be used as parameter.
+         */
+        fun createFor(env: JavacProcessingEnv, element: Element): KmClassContainer? {
+            val metadataAnnotation = getMetadataAnnotation(element) ?: return null
+            val classMetadata = KotlinClassMetadata.read(metadataAnnotation)
+            if (classMetadata == null) {
+                env.delegate.messager.printMessage(
+                    Diagnostic.Kind.WARNING,
+                    "Unable to read Kotlin metadata due to unsupported metadata version.",
+                    element
+                )
+            }
+            // TODO: Support more metadata kind (file facade, synthetic class, etc...)
+            return when (classMetadata) {
+                is KotlinClassMetadata.Class -> KmClassContainer(classMetadata.toKmClass())
+                else -> {
+                    env.delegate.messager.printMessage(
+                        Diagnostic.Kind.WARNING,
+                        "Unable to read Kotlin metadata due to unsupported metadata " +
+                            "kind: $classMetadata.",
+                        element
+                    )
+                    null
+                }
+            }
+        }
+
+        /**
+         * Search for Kotlin's Metadata annotation across the element's hierarchy.
+         */
+        private fun getMetadataAnnotation(element: Element?): Metadata? =
+            if (element != null) {
+                element.getAnnotation(Metadata::class.java)
+                    ?: getMetadataAnnotation(element.enclosingElement)
+            } else {
+                null
+            }
+    }
 }
 
-/**
- * Represents the kotlin metadata of a function
- */
-internal data class KmFunction(
-    /**
-     * Name of the function in byte code
-     */
-    val jvmName: String,
-    /**
-     * Name of the function in source code
-     */
-    val name: String,
-    val descriptor: String,
-    override val flags: Flags,
-    val typeArguments: List<KmType>,
-    override val parameters: List<KmValueParameter>,
-    val returnType: KmType,
-    val receiverType: KmType?,
-    val isPropertyFunction: Boolean = false
-) : KmExecutable {
+internal interface KmFunctionContainer : KmFlags {
+    /** Name of the function in source code **/
+    val name: String
+    /** Name of the function in byte code **/
+    val jvmName: String
+    val descriptor: String
+    val typeParameters: List<KmTypeParameterContainer>
+    val parameters: List<KmValueParameterContainer>
+    val returnType: KmTypeContainer
+
+    fun isPropertyFunction(): Boolean = this is KmPropertyFunctionContainerImpl
     fun isSuspend() = Flag.Function.IS_SUSPEND(flags)
-    fun isExtension() = receiverType != null
+    fun isExtension() =
+        this is KmFunctionContainerImpl && this.kmFunction.receiverParameterType != null
 }
 
-/**
- * Represents the kotlin metadata of a constructor
- */
-internal data class KmConstructor(
-    val descriptor: String,
+private class KmFunctionContainerImpl(
+    val kmFunction: KmFunction,
+    override val returnType: KmTypeContainer,
+) : KmFunctionContainer {
+    override val flags: Flags
+        get() = kmFunction.flags
+    override val name: String
+        get() = kmFunction.name
+    override val jvmName: String
+        get() = kmFunction.signature!!.name
+    override val descriptor: String
+        get() = kmFunction.signature!!.asString()
+    override val typeParameters: List<KmTypeParameterContainer>
+        get() = kmFunction.typeParameters.map { it.asContainer() }
+    override val parameters: List<KmValueParameterContainer>
+        get() = kmFunction.valueParameters.map { it.asContainer() }
+}
+
+private class KmPropertyFunctionContainerImpl(
     override val flags: Flags,
-    override val parameters: List<KmValueParameter>
-) : KmExecutable {
+    override val name: String,
+    override val jvmName: String,
+    override val descriptor: String,
+    override val parameters: List<KmValueParameterContainer>,
+    override val returnType: KmTypeContainer,
+) : KmFunctionContainer {
+    override val typeParameters: List<KmTypeParameterContainer> = emptyList()
+}
+
+internal class KmConstructorContainer(
+    private val kmConstructor: KmConstructor,
+    override val returnType: KmTypeContainer
+) : KmFunctionContainer {
+    override val flags: Flags
+        get() = kmConstructor.flags
+    override val name: String = "<init>"
+    override val jvmName: String = name
+    override val descriptor: String
+        get() = checkNotNull(kmConstructor.signature).asString()
+    override val typeParameters: List<KmTypeParameterContainer> = emptyList()
+    override val parameters: List<KmValueParameterContainer> by lazy {
+        kmConstructor.valueParameters.map { it.asContainer() }
+    }
     fun isPrimary() = !Flag.Constructor.IS_SECONDARY(flags)
 }
 
-internal data class KmProperty(
-    val name: String,
-    override val flags: Flags,
-    val type: KmType,
-    val getter: KmFunction?,
-    val setter: KmFunction?
-) : KmElement {
-    val typeParameters
+internal class KmPropertyContainer(
+    private val kmProperty: KmProperty,
+    val type: KmTypeContainer,
+    val getter: KmFunctionContainer?,
+    val setter: KmFunctionContainer?
+) : KmFlags {
+    override val flags: Flags
+        get() = kmProperty.flags
+    val name: String
+        get() = kmProperty.name
+    val typeParameters: List<KmTypeContainer>
         get() = type.typeArguments
-
-    fun isNullable() = Flag.Type.IS_NULLABLE(type.flags)
+    fun isNullable() = type.isNullable()
 }
 
-internal data class KmType(
-    override val flags: Flags,
-    val typeArguments: List<KmType>,
-    val extendsBound: KmType?,
-    val isExtensionType: Boolean
-) : KmElement {
+internal class KmTypeContainer(
+    private val kmType: KmType,
+    val typeArguments: List<KmTypeContainer>,
+    /** The extends bounds are only non-null for wildcard (i.e. in/out variant) types. */
+    val extendsBound: KmTypeContainer? = null,
+    /** The upper bounds are only non-null for type variable types with upper bounds. */
+    val upperBounds: List<KmTypeContainer>? = null
+) : KmFlags {
+    override val flags: Flags
+        get() = kmType.flags
+    fun isExtensionType() =
+        kmType.annotations.any { it.className == "kotlin/ExtensionFunctionType" }
     fun isNullable() = Flag.Type.IS_NULLABLE(flags)
-    fun erasure(): KmType = KmType(flags, emptyList(), extendsBound?.erasure(), isExtensionType)
-}
 
-private data class KmTypeParameter(
-    val name: String,
-    override val flags: Flags,
-    val extendsBound: KmType?
-) : KmElement {
-    fun asKmType() = KmType(
-        flags = flags,
+    fun erasure(): KmTypeContainer = KmTypeContainer(
+        kmType = kmType,
         typeArguments = emptyList(),
-        extendsBound = extendsBound,
-        isExtensionType = false
+        extendsBound = extendsBound?.erasure(),
+        // The erasure of a type variable is equal to the erasure of the first upper bound.
+        upperBounds = upperBounds?.firstOrNull()?.erasure()?.let { listOf(it) },
     )
 }
 
-/**
- * Represents the kotlin metadata of a parameter
- */
-internal data class KmValueParameter(
-    val name: String,
-    val type: KmType,
+internal val KmTypeContainer.nullability: XNullability
+    get() = if (isNullable()) {
+        XNullability.NULLABLE
+    } else {
+        // if there is an upper bound information, use its nullability (e.g. it might be T : Foo?)
+        if (upperBounds?.all { it.nullability == XNullability.NULLABLE } == true) {
+            XNullability.NULLABLE
+        } else {
+            extendsBound?.nullability ?: XNullability.NONNULL
+        }
+    }
+
+internal class KmTypeParameterContainer(
+    private val kmTypeParameter: KmTypeParameter,
+    val upperBounds: List<KmTypeContainer>
+) : KmFlags {
     override val flags: Flags
-) : KmElement {
+        get() = kmTypeParameter.flags
+    val name: String
+        get() = kmTypeParameter.name
+}
+
+internal class KmValueParameterContainer(
+    private val kmValueParameter: KmValueParameter,
+    val type: KmTypeContainer
+) : KmFlags {
+    override val flags: Flags
+        get() = kmValueParameter.flags
+    val name: String
+        get() = kmValueParameter.name
     fun isNullable() = type.isNullable()
     fun hasDefault() = Flag.ValueParameter.DECLARES_DEFAULT_VALUE(flags)
 }
 
-internal data class KmClassTypeInfo(
-    val kmType: KmType,
-    val superType: KmType?
-)
+private fun KmFunction.asContainer(): KmFunctionContainer =
+    KmFunctionContainerImpl(
+        kmFunction = this,
+        returnType = this.returnType.asContainer()
+    )
 
-internal fun KotlinClassMetadata.Class.readFunctions(): List<KmFunction> =
-    mutableListOf<KmFunction>().apply { accept(FunctionReader(this)) }
+private fun KmConstructor.asContainer(returnType: KmTypeContainer): KmConstructorContainer =
+    KmConstructorContainer(
+        kmConstructor = this,
+        returnType = returnType
+    )
 
-private class FunctionReader(val result: MutableList<KmFunction>) : KmClassVisitor() {
-    override fun visitFunction(flags: Flags, name: String): KmFunctionVisitor {
-        return object : KmFunctionVisitor() {
-
-            lateinit var methodSignature: JvmMethodSignature
-            private val typeParameters = mutableListOf<KmTypeParameter>()
-            val parameters = mutableListOf<KmValueParameter>()
-            lateinit var returnType: KmType
-            var receiverType: KmType? = null
-
-            override fun visitTypeParameter(
-                flags: Flags,
-                name: String,
-                id: Int,
-                variance: KmVariance
-            ): KmTypeParameterVisitor {
-                return TypeParameterReader(name, flags) {
-                    typeParameters.add(it)
-                }
-            }
-
-            override fun visitValueParameter(
-                flags: Flags,
-                name: String
-            ): KmValueParameterVisitor {
-                return ValueParameterReader(name, flags) {
-                    parameters.add(it)
-                }
-            }
-
-            override fun visitReceiverParameterType(flags: Flags): KmTypeVisitor? {
-                return TypeReader(flags) {
-                    receiverType = it
-                }
-            }
-
-            override fun visitExtensions(type: KmExtensionType): KmFunctionExtensionVisitor {
-                if (type != JvmFunctionExtensionVisitor.TYPE) {
-                    error("Unsupported extension type: $type")
-                }
-                return object : JvmFunctionExtensionVisitor() {
-                    override fun visit(signature: JvmMethodSignature?) {
-                        methodSignature = signature!!
-                    }
-                }
-            }
-
-            override fun visitReturnType(flags: Flags): KmTypeVisitor {
-                return TypeReader(flags) {
-                    returnType = it
-                }
-            }
-
-            override fun visitEnd() {
-                result.add(
-                    KmFunction(
-                        name = name,
-                        jvmName = methodSignature.name,
-                        descriptor = methodSignature.asString(),
-                        flags = flags,
-                        typeArguments = typeParameters.map { it.asKmType() },
-                        parameters = parameters,
-                        returnType = returnType,
-                        receiverType = receiverType
-                    )
-                )
-            }
-        }
-    }
-}
-
-internal fun KotlinClassMetadata.Class.readConstructors(): List<KmConstructor> =
-    mutableListOf<KmConstructor>().apply { accept(ConstructorReader(this)) }
-
-private class ConstructorReader(val result: MutableList<KmConstructor>) : KmClassVisitor() {
-    override fun visitConstructor(flags: Flags): KmConstructorVisitor {
-        return object : KmConstructorVisitor() {
-
-            lateinit var descriptor: String
-            val parameters = mutableListOf<KmValueParameter>()
-
-            override fun visitValueParameter(
-                flags: Flags,
-                name: String
-            ): KmValueParameterVisitor {
-                return ValueParameterReader(name, flags) {
-                    parameters.add(it)
-                }
-            }
-
-            override fun visitExtensions(type: KmExtensionType): KmConstructorExtensionVisitor {
-                if (type != JvmConstructorExtensionVisitor.TYPE) {
-                    error("Unsupported extension type: $type")
-                }
-                return object : JvmConstructorExtensionVisitor() {
-                    override fun visit(signature: JvmMethodSignature?) {
-                        descriptor = signature!!.asString()
-                    }
-                }
-            }
-
-            override fun visitEnd() {
-                result.add(KmConstructor(descriptor, flags, parameters))
-            }
-        }
-    }
-}
-
-internal class KotlinMetadataClassFlags(val classMetadata: KotlinClassMetadata.Class) {
-
-    private val flags: Flags by lazy {
-        var theFlags: Flags = 0
-        classMetadata.accept(object : KmClassVisitor() {
-            override fun visit(flags: Flags, name: ClassName) {
-                theFlags = flags
-                super.visit(flags, name)
-            }
-        })
-        return@lazy theFlags
-    }
-
-    fun isObject(): Boolean = Flag.Class.IS_OBJECT(flags)
-
-    fun isCompanionObject(): Boolean = Flag.Class.IS_COMPANION_OBJECT(flags)
-
-    fun isAnnotationClass(): Boolean = Flag.Class.IS_ANNOTATION_CLASS(flags)
-
-    fun isInterface(): Boolean = Flag.Class.IS_INTERFACE(flags)
-
-    fun isClass(): Boolean = Flag.Class.IS_CLASS(flags)
-
-    fun isDataClass(): Boolean = Flag.Class.IS_DATA(flags)
-
-    fun isValueClass(): Boolean = Flag.Class.IS_VALUE(flags)
-
-    fun isFunctionalInterface(): Boolean = Flag.Class.IS_FUN(flags)
-
-    fun isExpect(): Boolean = Flag.Class.IS_EXPECT(flags)
-}
-
-internal fun KotlinClassMetadata.Class.readProperties(): List<KmProperty> =
-    mutableListOf<KmProperty>().apply { accept(PropertyReader(this)) }
-
-/**
- * Reads the properties of a class declaration
- */
-private class PropertyReader(
-    val result: MutableList<KmProperty>
-) : KmClassVisitor() {
-    override fun visitProperty(
-        flags: Flags,
-        name: String,
-        getterFlags: Flags,
-        setterFlags: Flags
-    ): KmPropertyVisitor {
-        var setterParam: KmValueParameter? = null
-        var getter: JvmMethodSignature? = null
-        var setter: JvmMethodSignature? = null
-        return object : KmPropertyVisitor() {
-            lateinit var returnType: KmType
-            override fun visitEnd() {
-                result.add(
-                    KmProperty(
-                        type = returnType,
-                        name = name,
-                        flags = flags,
-                        setter = setter?.let { setterSignature ->
-                            // setter parameter visitor may not be invoked when not declared
-                            // explicitly
-                            val param = setterParam ?: KmValueParameter(
-                                // kotlinc will set this to set-? but it is better to not expose
-                                // it here since it is not valid name
-                                name = "set-?".sanitizeAsJavaParameterName(0),
-                                type = returnType,
-                                flags = 0
-                            )
-                            KmFunction(
-                                jvmName = setterSignature.name,
-                                name = JvmAbi.computeSetterName(name),
-                                descriptor = setterSignature.asString(),
-                                flags = setterFlags,
-                                typeArguments = emptyList(),
-                                parameters = listOf(param),
-                                returnType = KM_VOID_TYPE,
-                                receiverType = null,
-                                isPropertyFunction = true
-                            )
-                        },
-                        getter = getter?.let { getterSignature ->
-                            KmFunction(
-                                jvmName = getterSignature.name,
-                                name = JvmAbi.computeGetterName(name),
-                                descriptor = getterSignature.asString(),
-                                flags = getterFlags,
-                                typeArguments = emptyList(),
-                                parameters = emptyList(),
-                                returnType = returnType,
-                                receiverType = null,
-                                isPropertyFunction = true
-                            )
-                        }
-                    )
-                )
-            }
-
-            override fun visitReturnType(flags: Flags): KmTypeVisitor {
-                return TypeReader(flags) {
-                    returnType = it
-                }
-            }
-
-            override fun visitSetterParameter(
-                flags: Flags,
-                name: String
-            ): KmValueParameterVisitor {
-                return ValueParameterReader(
-                    name = name,
-                    flags = flags
-                ) {
-                    setterParam = it
-                }
-            }
-
-            override fun visitExtensions(type: KmExtensionType): KmPropertyExtensionVisitor? {
-                if (type != JvmPropertyExtensionVisitor.TYPE) {
-                    return null
-                }
-                return object : JvmPropertyExtensionVisitor() {
-                    override fun visit(
-                        jvmFlags: Flags,
-                        fieldSignature: JvmFieldSignature?,
-                        getterSignature: JvmMethodSignature?,
-                        setterSignature: JvmMethodSignature?
-                    ) {
-                        getter = getterSignature
-                        setter = setterSignature
-                    }
-                }
-            }
-        }
-    }
-}
-
-/**
- * Reads a type description and calls the output with the read value
- */
-private class TypeReader(
-    private val flags: Flags,
-    private val output: (KmType) -> Unit
-) : KmTypeVisitor() {
-    private val typeArguments = mutableListOf<KmType>()
-    private var extendsBound: KmType? = null
-    private var isExtensionType = false
-    override fun visitArgument(flags: Flags, variance: KmVariance): KmTypeVisitor {
-        return TypeReader(flags) {
-            typeArguments.add(it)
-        }
-    }
-
-    override fun visitFlexibleTypeUpperBound(
-        flags: Flags,
-        typeFlexibilityId: String?
-    ): KmTypeVisitor {
-        return TypeReader(flags) {
-            extendsBound = it
-        }
-    }
-
-    override fun visitExtensions(type: KmExtensionType): KmTypeExtensionVisitor? {
-        if (type != JvmTypeExtensionVisitor.TYPE) return null
-        return object : JvmTypeExtensionVisitor() {
-            override fun visitAnnotation(annotation: KmAnnotation) {
-                if (annotation.className == "kotlin/ExtensionFunctionType") {
-                    isExtensionType = true
-                }
-            }
-        }
-    }
-
-    override fun visitEnd() {
-        output(
-            KmType(
-                flags = flags,
-                typeArguments = typeArguments,
-                extendsBound = extendsBound,
-                isExtensionType = isExtensionType
+private fun KmProperty.asContainer(): KmPropertyContainer =
+    KmPropertyContainer(
+        kmProperty = this,
+        type = this.returnType.asContainer(),
+        getter = getterSignature?.let {
+            KmPropertyFunctionContainerImpl(
+                flags = this.getterFlags,
+                name = JvmAbi.computeGetterName(this.name),
+                jvmName = it.name,
+                descriptor = it.asString(),
+                parameters = emptyList(),
+                returnType = this.returnType.asContainer(),
             )
-        )
-    }
-}
-
-/**
- * Reads the value parameter of a function or constructor and calls the output with the read value
- */
-private class ValueParameterReader(
-    val name: String,
-    val flags: Flags,
-    val output: (KmValueParameter) -> Unit
-) : KmValueParameterVisitor() {
-    lateinit var type: KmType
-    override fun visitType(flags: Flags): KmTypeVisitor {
-        return TypeReader(flags) {
-            type = it
-        }
-    }
-
-    override fun visitEnd() {
-        output(
-            KmValueParameter(
-                name = name,
-                type = type,
-                flags = flags
+        },
+        setter = setterSignature?.let {
+            // setter parameter visitor may not be available when not declared explicitly
+            val param = this.setterParameter ?: KmValueParameter(
+                flags = 0,
+                // kotlinc will set this to set-? but it is better to not expose
+                // it here since it is not valid name
+                name = "set-?".sanitizeAsJavaParameterName(0)
+            ).apply { type = this@asContainer.returnType }
+            KmPropertyFunctionContainerImpl(
+                flags = this.setterFlags,
+                name = JvmAbi.computeSetterName(this.name),
+                jvmName = it.name,
+                descriptor = it.asString(),
+                parameters = listOf(param.asContainer()),
+                returnType = KmType(0).asContainer(),
             )
-        )
-    }
-}
+        },
+    )
 
-/**
- * Reads a class declaration and turns it into a KmType for both itself and its super type
- */
-internal class ClassAsKmTypeReader(
-    val output: (KmClassTypeInfo) -> Unit
-) : KmClassVisitor() {
-    private var flags: Flags = 0
-    private val typeParameters = mutableListOf<KmTypeParameter>()
-    private var superType: KmType? = null
-    override fun visit(flags: Flags, name: ClassName) {
-        this.flags = flags
-    }
+private fun KmType.asContainer(): KmTypeContainer =
+    KmTypeContainer(
+        kmType = this,
+        typeArguments = this.arguments.mapNotNull { it.type?.asContainer() }
+    )
 
-    override fun visitTypeParameter(
-        flags: Flags,
-        name: String,
-        id: Int,
-        variance: KmVariance
-    ): KmTypeParameterVisitor {
-        return TypeParameterReader(name, flags) {
-            typeParameters.add(it)
-        }
-    }
+private fun KmTypeParameter.asContainer(): KmTypeParameterContainer =
+    KmTypeParameterContainer(
+        kmTypeParameter = this,
+        upperBounds = this.upperBounds.map { it.asContainer() }
+    )
 
-    override fun visitSupertype(flags: Flags): KmTypeVisitor {
-        return TypeReader(flags) {
-            superType = it
-        }
-    }
-
-    override fun visitEnd() {
-        output(
-            KmClassTypeInfo(
-                kmType = KmType(
-                    flags = flags,
-                    typeArguments = typeParameters.map {
-                        it.asKmType()
-                    },
-                    extendsBound = null,
-                    isExtensionType = false
-                ),
-                superType = superType
-            )
-        )
-    }
-}
-
-private class TypeParameterReader(
-    private val name: String,
-    private val flags: Flags,
-    private val output: (KmTypeParameter) -> Unit
-) : KmTypeParameterVisitor() {
-    private var upperBound: KmType? = null
-    override fun visitEnd() {
-        output(
-            KmTypeParameter(
-                name = name,
-                flags = flags,
-                extendsBound = upperBound
-            )
-        )
-    }
-
-    override fun visitUpperBound(flags: Flags): KmTypeVisitor {
-        return TypeReader(flags) {
-            upperBound = it
-        }
-    }
-}
-
-private val KM_VOID_TYPE = KmType(
-    flags = 0,
-    typeArguments = emptyList(),
-    extendsBound = null,
-    isExtensionType = false
-)
\ No newline at end of file
+private fun KmValueParameter.asContainer(): KmValueParameterContainer =
+    KmValueParameterContainer(
+        kmValueParameter = this,
+        type = this.type.asContainer()
+    )
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElement.kt
deleted file mode 100644
index 2d1ae4b..0000000
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElement.kt
+++ /dev/null
@@ -1,148 +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.room.compiler.processing.javac.kotlin
-
-import kotlinx.metadata.jvm.KotlinClassHeader
-import kotlinx.metadata.jvm.KotlinClassMetadata
-import javax.lang.model.element.Element
-import javax.lang.model.element.ElementKind
-import javax.lang.model.element.ExecutableElement
-
-/**
- * Utility class for processors that wants to run kotlin specific code.
- */
-internal class KotlinMetadataElement(
-    val element: Element,
-    private val classMetadata: KotlinClassMetadata.Class
-) : KmElement {
-    private val typeInfo: KmClassTypeInfo by lazy {
-        lateinit var result: KmClassTypeInfo
-        classMetadata.accept(
-            ClassAsKmTypeReader {
-                result = it
-            }
-        )
-        result
-    }
-    val kmType
-        get() = typeInfo.kmType
-    override val flags
-        get() = kmType.flags
-    val superType
-        get() = typeInfo.superType
-    private val functionList: List<KmFunction> by lazy { classMetadata.readFunctions() }
-    private val constructorList: List<KmConstructor> by lazy { classMetadata.readConstructors() }
-    private val propertyList: List<KmProperty> by lazy { classMetadata.readProperties() }
-    private val classFlags: KotlinMetadataClassFlags by lazy {
-        KotlinMetadataClassFlags(classMetadata)
-    }
-
-    private val ExecutableElement.descriptor: String
-        get() = descriptor()
-
-    fun findPrimaryConstructorSignature() = constructorList.firstOrNull {
-        it.isPrimary()
-    }?.descriptor
-
-    fun isObject(): Boolean = classFlags.isObject()
-    fun isCompanionObject(): Boolean = classFlags.isCompanionObject()
-    fun isAnnotationClass(): Boolean = classFlags.isAnnotationClass()
-    fun isClass(): Boolean = classFlags.isClass()
-    fun isInterface(): Boolean = classFlags.isInterface()
-    fun isDataClass(): Boolean = classFlags.isDataClass()
-    fun isValueClass(): Boolean = classFlags.isValueClass()
-    fun isFunctionalInterface(): Boolean = classFlags.isFunctionalInterface()
-    fun isExpect(): Boolean = classFlags.isExpect()
-
-    fun getFunctionMetadata(method: ExecutableElement): KmFunction? {
-        check(method.kind == ElementKind.METHOD) {
-            "must pass an element type of method"
-        }
-        val methodSignature = method.descriptor
-        functionList.firstOrNull { it.descriptor == methodSignature }?.let {
-            return it
-        }
-        // might be a property getter or setter
-        return propertyList.firstNotNullOfOrNull { property ->
-            when {
-                property.getter?.descriptor == methodSignature -> {
-                    property.getter
-                }
-                property.setter?.descriptor == methodSignature -> {
-                    property.setter
-                }
-                else -> {
-                    null
-                }
-            }
-        }
-    }
-
-    fun getConstructorMetadata(method: ExecutableElement): KmConstructor? {
-        check(method.kind == ElementKind.CONSTRUCTOR) {
-            "must pass an element type of constructor"
-        }
-        val methodSignature = method.descriptor
-        return constructorList.firstOrNull { it.descriptor == methodSignature }
-    }
-
-    fun getPropertyMetadata(propertyName: String) = propertyList.firstOrNull {
-        it.name == propertyName
-    }
-
-    companion object {
-        /**
-         * Creates a [KotlinMetadataElement] for the given element if it contains Kotlin metadata,
-         * otherwise this method returns null.
-         *
-         * Usually the [element] passed must represent a class. For example, if kotlin metadata is
-         * desired for a method, then the containing class should be used as parameter.
-         */
-        fun createFor(element: Element): KotlinMetadataElement? {
-            val metadata = getMetadataAnnotation(element)?.run {
-                KotlinClassHeader(
-                    kind = kind,
-                    metadataVersion = metadataVersion,
-                    data1 = data1,
-                    data2 = data2,
-                    extraString = extraString,
-                    packageName = packageName,
-                    extraInt = extraInt
-                ).let {
-                    // TODO: Support more metadata kind (file facade, synthetic class, etc...)
-                    KotlinClassMetadata.read(it) as? KotlinClassMetadata.Class
-                }
-            }
-            return if (metadata != null) {
-                KotlinMetadataElement(element, metadata)
-            } else {
-                null
-            }
-        }
-
-        /**
-         * Search for Kotlin's Metadata annotation across the element's hierarchy.
-         */
-        private fun getMetadataAnnotation(element: Element?): Metadata? =
-            if (element != null) {
-                element.getAnnotation(Metadata::class.java)
-                    ?: getMetadataAnnotation(element.enclosingElement)
-            } else {
-                null
-            }
-    }
-}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspClassFileUtility.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspClassFileUtility.kt
index 4dc85b8..f01c0fd 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspClassFileUtility.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspClassFileUtility.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.compiler.processing.ksp
 
+import androidx.room.compiler.processing.XMethodElement
 import androidx.room.compiler.processing.XProcessingConfig
 import com.google.devtools.ksp.symbol.ClassKind
 import com.google.devtools.ksp.symbol.KSClassDeclaration
@@ -103,8 +104,6 @@
 
     /**
      * Sorts the given methods in the order they are declared in the backing class declaration.
-     * Note that this does not check signatures so ordering might break if there are multiple
-     * methods with the same name.
      */
     fun orderMethods(
         owner: KSClassDeclaration,
@@ -112,14 +111,14 @@
     ): List<KspMethodElement> {
         // no reason to try to load .class if we don't have any fields to sort
         if (methods.isEmpty()) return methods
-        val comparator = getNamesComparator(owner, Type.METHOD, KspMethodElement::jvmName)
+        val comparator = getNamesComparator(owner, Type.METHOD, XMethodElement::jvmDescriptor)
         return if (comparator == null) {
             methods
         } else {
             methods.forEach {
-                // make sure each name gets registered so that if we didn't find it in .class for
-                // whatever reason, we keep the order given from KSP.
-                comparator.register(it.jvmName)
+                // make sure each descriptor gets registered so that if we didn't find it in .class
+                // for whatever reason, we keep the order given from KSP.
+                comparator.register(it.jvmDescriptor())
             }
             methods.sortedWith(comparator)
         }
@@ -157,7 +156,7 @@
             val typeReferences = ReflectionReferences.getInstance(classDeclaration) ?: return null
             val binarySource = getBinarySource(typeReferences, classDeclaration) ?: return null
 
-            val fieldNameComparator = MemberNameComparator(
+            val memberNameComparator = MemberNameComparator(
                 getName = getName,
                 // we can do strict mode only in classes. For Interfaces, private methods won't
                 // show up in the binary.
@@ -168,7 +167,13 @@
                 if (method.name == type.visitorName) {
                     val nameAsString = typeReferences.asStringMethod.invoke(args[0])
                     if (nameAsString is String) {
-                        fieldNameComparator.register(nameAsString)
+                        when (type) {
+                            Type.FIELD -> memberNameComparator.register(nameAsString)
+                            Type.METHOD -> {
+                                val methodTypeDescriptor = args[1]
+                                memberNameComparator.register(nameAsString + methodTypeDescriptor)
+                            }
+                        }
                     }
                 }
                 null
@@ -181,8 +186,8 @@
             )
 
             typeReferences.visitMembersMethod.invoke(binarySource, proxy, null)
-            fieldNameComparator.seal()
-            fieldNameComparator
+            memberNameComparator.seal()
+            memberNameComparator
         } catch (ignored: Throwable) {
             // this is best effort, if it failed, just ignore
             if (XProcessingConfig.STRICT_MODE) {
@@ -306,7 +311,7 @@
          */
         private fun getOrder(name: String) = orders.getOrPut(name) {
             if (sealed && strictMode) {
-                error("expected to find field/method $name but it is non-existent")
+                error("expected to find field/method $name but it is non-existent: $orders")
             }
             nextOrder++
         }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
index 818856d..9e74ffb 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
@@ -27,7 +27,6 @@
 import com.google.devtools.ksp.KspExperimental
 import com.google.devtools.ksp.isConstructor
 import com.google.devtools.ksp.symbol.KSFunctionDeclaration
-import com.google.devtools.ksp.symbol.Modifier
 
 internal abstract class KspExecutableElement(
     env: KspProcessingEnv,
@@ -63,12 +62,7 @@
     }
 
     override fun isVarArgs(): Boolean {
-        // in java, only the last argument can be a vararg so for suspend functions, it is never
-        // a vararg function. this would change if room generated kotlin code
-        return !declaration.modifiers.contains(Modifier.SUSPEND) &&
-            declaration.parameters.any {
-                it.isVararg
-            }
+        return declaration.parameters.any { it.isVararg }
     }
 
     companion object {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspJvmDescriptorUtils.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspJvmDescriptorUtils.kt
new file mode 100644
index 0000000..0ff7763
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspJvmDescriptorUtils.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.room.compiler.processing.ksp
+
+import androidx.room.compiler.processing.XConstructorElement
+import androidx.room.compiler.processing.XConstructorType
+import androidx.room.compiler.processing.XExecutableType
+import androidx.room.compiler.processing.XFieldElement
+import androidx.room.compiler.processing.XMethodElement
+import androidx.room.compiler.processing.XMethodType
+import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.isArray
+import androidx.room.compiler.processing.isBoolean
+import androidx.room.compiler.processing.isByte
+import androidx.room.compiler.processing.isChar
+import androidx.room.compiler.processing.isDouble
+import androidx.room.compiler.processing.isFloat
+import androidx.room.compiler.processing.isInt
+import androidx.room.compiler.processing.isKotlinUnit
+import androidx.room.compiler.processing.isLong
+import androidx.room.compiler.processing.isShort
+import androidx.room.compiler.processing.isTypeVariable
+import androidx.room.compiler.processing.isVoid
+import androidx.room.compiler.processing.isVoidObject
+
+/**
+ * Returns the method descriptor of this field element.
+ *
+ * For reference, see the [JVM
+ * specification, section 4.3.2](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2).
+ */
+internal fun XFieldElement.jvmDescriptor() = name + ":" + type.jvmDescriptor()
+
+/**
+ * Returns the method descriptor of this method element.
+ *
+ * For reference, see the [JVM
+ * specification, section 4.3.3](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.3).
+ */
+internal fun XMethodElement.jvmDescriptor() = jvmName + executableType.jvmDescriptor()
+
+/**
+ * Returns the method descriptor of this constructor element.
+ *
+ * For reference, see the [JVM
+ * specification, section 4.3.3](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.3).
+ */
+internal fun XConstructorElement.jvmDescriptor() = name + executableType.jvmDescriptor()
+
+private fun XExecutableType.jvmDescriptor(): String {
+    val parameterTypeDescriptors = parameterTypes.joinToString("") { it.jvmDescriptor() }
+    val returnTypeDescriptor = when (this) {
+        is XMethodType -> returnType.jvmDescriptor()
+        is XConstructorType -> "V"
+        else -> error("Unexpected executable type: $javaClass")
+    }
+    return "($parameterTypeDescriptors)$returnTypeDescriptor"
+}
+
+private fun XType.jvmDescriptor(): String {
+    return when {
+        isKotlinUnit() || isNone() || isVoid() || isVoidObject() -> "V"
+        isArray() -> "[${componentType.jvmDescriptor()}"
+        // See https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.2
+        typeElement != null -> "L${typeElement!!.asClassName().reflectionName.replace('.', '/')};"
+        // For a type variable with multiple bounds: "the erasure of a type variable is determined
+        // by the first type in its bound" - JLS Sec 4.4
+        // See https://docs.oracle.com/javase/specs/jls/se16/html/jls-4.html#jls-4.4
+        isTypeVariable() -> upperBounds.first().jvmDescriptor()
+        isInt() -> "I"
+        isLong() -> "J"
+        isByte() -> "B"
+        isShort() -> "S"
+        isDouble() -> "D"
+        isFloat() -> "F"
+        isBoolean() -> "Z"
+        isChar() -> "C"
+        else -> error("Unexpected type: $javaClass")
+    }
+}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt
index af44a4d..c452578 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt
@@ -253,12 +253,9 @@
         }
         val qName = ksType.declaration.qualifiedName?.asString()
         if (declaration is KSTypeParameter) {
-            return KspTypeArgumentType(
+            return KspTypeVariableType(
                 env = this,
-                typeArg = resolver.getTypeArgument(
-                    ksType.createTypeReference(),
-                    declaration.variance
-                ),
+                ksType = ksType,
                 jvmTypeResolver = null
             )
         }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
index 413bc62..b0d03e1 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
@@ -298,6 +298,4 @@
         }
         return copyWithNullability(XNullability.NONNULL)
     }
-
-    override fun isTypeVariable() = false
 }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
index a9a4c03..ffacdbd 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
@@ -70,8 +70,6 @@
         return _extendsBound
     }
 
-    override fun isTypeVariable() = true
-
     override fun copyWithNullability(nullability: XNullability): KspTypeArgumentType {
         return KspTypeArgumentType(
             env = env,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeVariableType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeVariableType.kt
new file mode 100644
index 0000000..9d91f0c4
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeVariableType.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.room.compiler.processing.ksp
+
+import androidx.room.compiler.processing.XNullability
+import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XTypeVariableType
+import com.google.devtools.ksp.symbol.KSType
+import com.google.devtools.ksp.symbol.KSTypeParameter
+import com.squareup.kotlinpoet.javapoet.JTypeName
+import com.squareup.kotlinpoet.javapoet.KTypeName
+
+internal class KspTypeVariableType(
+    env: KspProcessingEnv,
+    ksType: KSType,
+    jvmTypeResolver: KspJvmTypeResolver?
+) : KspType(
+    env = env,
+    ksType = ksType,
+    jvmTypeResolver = jvmTypeResolver
+), XTypeVariableType {
+    private val typeVariable: KSTypeParameter by lazy {
+        // Note: This is a workaround for a bug in KSP where we may get ERROR_TYPE in the bounds
+        // (https://github.com/google/ksp/issues/1250). To work around it we get the matching
+        // KSTypeParameter from the parent declaration instead.
+        ksType.declaration.parentDeclaration!!.typeParameters
+            .filter { it.name == (ksType.declaration as KSTypeParameter).name }
+            .single()
+    }
+
+    override fun resolveJTypeName(): JTypeName {
+        return typeVariable.asJTypeName(env.resolver)
+    }
+
+    override fun resolveKTypeName(): KTypeName {
+        return typeVariable.asKTypeName(env.resolver)
+    }
+
+    override val upperBounds: List<XType> = typeVariable.bounds.map(env::wrap).toList()
+
+    override fun boxed(): KspTypeVariableType {
+        return this
+    }
+
+    override fun copyWithNullability(nullability: XNullability): KspTypeVariableType {
+        return KspTypeVariableType(
+            env = env,
+            ksType = ksType,
+            jvmTypeResolver = jvmTypeResolver
+        )
+    }
+
+    override fun copyWithJvmTypeResolver(jvmTypeResolver: KspJvmTypeResolver): KspType {
+        return KspTypeVariableType(
+            env = env,
+            ksType = ksType,
+            jvmTypeResolver = jvmTypeResolver
+        )
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
index 705d9f0..73c87b1 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
@@ -135,7 +135,11 @@
         ) {
             val element = it.processingEnv.requireTypeElement("Subject")
             assertThat(element.getMethodByJvmName("method").isVarArgs()).isTrue()
-            assertThat(element.getMethodByJvmName("suspendMethod").isVarArgs()).isFalse()
+            if (it.isKsp) {
+                assertThat(element.getMethodByJvmName("suspendMethod").isVarArgs()).isTrue()
+            } else {
+                assertThat(element.getMethodByJvmName("suspendMethod").isVarArgs()).isFalse()
+            }
         }
     }
 
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
index be70bab..da53f87 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
@@ -19,6 +19,7 @@
 import androidx.room.compiler.codegen.XClassName
 import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.codegen.asClassName
+import androidx.room.compiler.processing.ksp.jvmDescriptor
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.XTestInvocation
 import androidx.room.compiler.processing.util.asKClassName
@@ -1922,6 +1923,43 @@
         }
     }
 
+    @Test
+    fun sameMethodNameOrder() {
+        runTest(
+            sources = listOf(
+                Source.kotlin(
+                    "test.Foo.kt",
+                    """
+                    package test
+                    class Foo<T1: Bar, T2: Baz> {
+                        fun method(): String = TODO()
+                        fun method(param: String): String = TODO()
+                        fun method(param: Any): String = TODO()
+                        fun method(param: T1): T2 = TODO()
+                        fun method(param: T2): T1 = TODO()
+                        fun <U1: Baz, U2> method(param1: U1, param2: U2) {}
+                        fun <U1: Baz, U2: U1> method(param1: U1, param2: U2): T1 = TODO()
+                    }
+                    interface Bar
+                    interface Baz
+                    """.trimIndent()
+                )
+            )
+        ) { invocation ->
+            val foo = invocation.processingEnv.requireTypeElement("test.Foo")
+            assertThat(foo.getDeclaredMethods().map { it.jvmDescriptor() }.toList())
+                .containsExactly(
+                    "method()Ljava/lang/String;",
+                    "method(Ljava/lang/String;)Ljava/lang/String;",
+                    "method(Ljava/lang/Object;)Ljava/lang/String;",
+                    "method(Ltest/Bar;)Ltest/Baz;",
+                    "method(Ltest/Baz;)Ltest/Bar;",
+                    "method(Ltest/Baz;Ljava/lang/Object;)V",
+                    "method(Ltest/Baz;Ltest/Baz;)Ltest/Bar;"
+                ).inOrder()
+        }
+    }
+
     /**
      * it is good to exclude methods coming from Object when testing as they differ between KSP
      * and KAPT but irrelevant for Room.
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElementTest.kt
index f451bd8..5915327 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElementTest.kt
@@ -17,8 +17,8 @@
 package androidx.room.compiler.processing.javac.kotlin
 
 import androidx.room.compiler.processing.XNullability
+import androidx.room.compiler.processing.XProcessingEnvConfig
 import androidx.room.compiler.processing.javac.JavacProcessingEnv
-import androidx.room.compiler.processing.javac.nullability
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.compileFiles
 import androidx.room.compiler.processing.util.runJavaProcessorTest
@@ -26,14 +26,14 @@
 import androidx.room.compiler.processing.util.sanitizeAsJavaParameterName
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
-import org.junit.AssumptionViolatedException
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
 import javax.annotation.processing.ProcessingEnvironment
 import javax.lang.model.element.ExecutableElement
 import javax.lang.model.element.TypeElement
 import javax.lang.model.util.ElementFilter
+import org.junit.AssumptionViolatedException
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
 
 @RunWith(Parameterized::class)
 class KotlinMetadataElementTest(
@@ -164,7 +164,7 @@
             assertThat(
                 testClassElement.getConstructors().map {
                     val desc = it.descriptor()
-                    desc to (desc == metadataElement.findPrimaryConstructorSignature())
+                    desc to (desc == metadataElement.primaryConstructorSignature)
                 }
             ).containsExactly(
                 "<init>(Ljava/lang/String;)V" to true,
@@ -347,7 +347,7 @@
             )
 
             fun assertSetter(
-                kmFunction: KmFunction?,
+                kmFunction: KmFunctionContainer?,
                 name: String,
                 jvmName: String,
                 paramNullable: Boolean
@@ -366,7 +366,7 @@
             }
 
             fun assertSetter(
-                metadata: KotlinMetadataElement,
+                metadata: KmClassContainer,
                 method: ExecutableElement,
                 name: String,
                 jvmName: String,
@@ -391,7 +391,7 @@
             }
 
             fun assertGetter(
-                kmFunction: KmFunction?,
+                kmFunction: KmFunctionContainer?,
                 name: String,
                 jvmName: String,
                 returnsNullable: Boolean
@@ -582,7 +582,7 @@
                 invocation,
                 "Subject"
             )
-            fun assertParams(params: List<KmValueParameter>?) {
+            fun assertParams(params: List<KmValueParameterContainer>?) {
                 assertThat(
                     params?.map {
                         Triple(
@@ -670,19 +670,23 @@
         )
         simpleRun(listOf(src)) { invocation ->
             val (_, simple) = getMetadataElement(invocation, "Simple")
-            assertThat(simple.kmType.isNullable()).isFalse()
-            assertThat(simple.kmType.typeArguments).isEmpty()
+            assertThat(simple.type.isNullable()).isFalse()
+            assertThat(simple.type.typeArguments).isEmpty()
 
             val (_, twoArgGeneric) = getMetadataElement(invocation, "TwoArgGeneric")
-            assertThat(twoArgGeneric.kmType.isNullable()).isFalse()
-            assertThat(twoArgGeneric.kmType.typeArguments).hasSize(2)
-            assertThat(twoArgGeneric.kmType.typeArguments[0].isNullable()).isFalse()
-            assertThat(twoArgGeneric.kmType.typeArguments[1].isNullable()).isFalse()
+            assertThat(twoArgGeneric.type.isNullable()).isFalse()
+            assertThat(twoArgGeneric.type.typeArguments).hasSize(2)
+            assertThat(twoArgGeneric.type.typeArguments[0].isNullable()).isFalse()
+            assertThat(twoArgGeneric.type.typeArguments[1].isNullable()).isFalse()
 
             val (_, withUpperBounds) = getMetadataElement(invocation, "WithUpperBounds")
-            assertThat(withUpperBounds.kmType.typeArguments).hasSize(2)
-            assertThat(withUpperBounds.kmType.typeArguments[0].extendsBound?.isNullable()).isFalse()
-            assertThat(withUpperBounds.kmType.typeArguments[1].extendsBound?.isNullable()).isTrue()
+            assertThat(withUpperBounds.type.typeArguments).hasSize(2)
+            assertThat(withUpperBounds.type.typeArguments[0].upperBounds).hasSize(1)
+            assertThat(withUpperBounds.type.typeArguments[0].upperBounds!![0].isNullable())
+                .isFalse()
+            assertThat(withUpperBounds.type.typeArguments[1].upperBounds).hasSize(1)
+            assertThat(withUpperBounds.type.typeArguments[1].upperBounds!![0].isNullable())
+                .isTrue()
 
             val (_, withSuperType) = getMetadataElement(invocation, "WithSuperType")
             assertThat(withSuperType.superType?.typeArguments?.get(0)?.isNullable()).isFalse()
@@ -817,7 +821,8 @@
 
     private fun getMetadataElement(processingEnv: ProcessingEnvironment, qName: String) =
         processingEnv.elementUtils.getTypeElement(qName).let {
-            it to KotlinMetadataElement.createFor(it)!!
+            it to KmClassContainer.createFor(
+                JavacProcessingEnv(processingEnv, XProcessingEnvConfig.DEFAULT), it)!!
         }
 
     companion object {
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspJvmDescriptorUtilsTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspJvmDescriptorUtilsTest.kt
new file mode 100644
index 0000000..f8e5914
--- /dev/null
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspJvmDescriptorUtilsTest.kt
@@ -0,0 +1,509 @@
+/*
+ * 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.room.compiler.processing.ksp
+
+import androidx.room.compiler.processing.XElement
+import androidx.room.compiler.processing.isConstructor
+import androidx.room.compiler.processing.isField
+import androidx.room.compiler.processing.isMethod
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.runProcessorTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class KspJvmDescriptorUtilsTest {
+    private val describeAnnotation =
+        Source.java(
+            "androidx.room.test.Describe",
+            """
+            package androidx.room.test;
+
+            import java.lang.annotation.ElementType;
+            import java.lang.annotation.Target;
+
+            @Target({ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
+            public @interface Describe { }
+            """)
+
+    @Test
+    fun descriptor_method_simple() {
+        fun checkSources(vararg sources: Source) {
+            runTest(sources = sources) { invocation ->
+                assertThat(invocation.annotatedElements().map(this::descriptor))
+                    .containsExactly("emptyMethod()V")
+            }
+        }
+        checkSources(
+            Source.java(
+                "androidx.room.test.DummyClass",
+                """
+                package androidx.room.test;
+                public class DummyClass {
+                    @Describe public void emptyMethod() {}
+                }
+                """)
+        )
+        checkSources(
+            Source.kotlin(
+                "androidx.room.test.DummyClass.kt",
+                """
+                package androidx.room.test
+                class DummyClass {
+                    @Describe fun emptyMethod() {}
+                }
+                """)
+        )
+    }
+
+    @Test
+    fun descriptor_field() {
+        fun checkSources(vararg sources: Source) {
+            runTest(sources = sources) { invocation ->
+                assertThat(invocation.annotatedElements().map(this::descriptor)).containsExactly(
+                    "field1:I",
+                    "field2:Ljava/lang/String;",
+                    "field3:Ljava/lang/Object;",
+                    "field4:Ljava/util/List;"
+                )
+            }
+        }
+        checkSources(
+            Source.java(
+                "androidx.room.test.DummyClass",
+                """
+                package androidx.room.test;
+
+                import java.util.List;
+
+                class DummyClass<T> {
+                    @Describe int field1;
+                    @Describe String field2;
+                    @Describe T field3;
+                    @Describe List<String> field4;
+                }
+                """)
+        )
+        checkSources(
+            Source.kotlin(
+                "androidx.room.test.DummyClass.kt",
+                """
+                package androidx.room.test
+                class DummyClass<T> {
+                    @Describe val field1: Int = TODO()
+                    @Describe val field2: String = TODO()
+                    @Describe val field3: T = TODO()
+                    @Describe val field4: List<String> = TODO()
+                }
+                """)
+        )
+    }
+
+    @Test
+    fun descriptor_method_erasured() {
+        fun checkSources(vararg sources: Source) {
+            runTest(sources = sources) { invocation ->
+                assertThat(invocation.annotatedElements().map(this::descriptor)).containsAtLeast(
+                    "method1(Landroidx/room/test/Foo;)V",
+                    "method2()Landroidx/room/test/Foo;",
+                    "method3()Ljava/util/List;",
+                    "method4()Ljava/util/Map;",
+                    "method5()Ljava/util/ArrayList;",
+                    "method6(Ljava/lang/Object;)Landroidx/room/test/Foo;",
+                    "method7(Ljava/lang/Object;)Ljava/lang/Object;",
+                    "method8(Ljava/lang/Object;)Ljava/lang/String;",
+                    "method9(Landroidx/room/test/Foo;)Landroidx/room/test/Foo;",
+                    "method10()Ljava/util/Collection;",
+                    "method11()Landroidx/room/test/Foo;",
+                )
+            }
+        }
+        checkSources(
+            Source.java(
+                "androidx.room.test.DummyClass",
+                """
+                package androidx.room.test;
+
+                import java.util.ArrayList;
+                import java.util.Collection;
+                import java.util.List;
+                import java.util.Map;
+
+                class DummyClass<T extends Foo> {
+                    @Describe void method1(T param) { }
+                    @Describe T method2() { return null; }
+                    @Describe List<? extends String> method3() { return null; }
+                    @Describe Map<T, String> method4() { return null; }
+                    @Describe ArrayList<Map<T, String>> method5() { return null; }
+                    @Describe <I, O extends T> O method6(I param) { return null; }
+                    @Describe static <I, O extends I> O method7(I param) { return null; }
+                    @Describe static <I, O extends String> O method8(I param) { return null; }
+                    @Describe
+                    static <I extends Foo, O extends I> O method9(I param) { return null; }
+                    @Describe static <P extends Collection & Foo> P method10() { return null; }
+                    @Describe static <P extends Foo & Collection<?>> P method11() { return null; }
+                }
+                interface Foo {}
+                """)
+        )
+        checkSources(
+            Source.kotlin(
+                "androidx.room.test.DummyClass.kt",
+                """
+                package androidx.room.test
+                class DummyClass<T: Foo> {
+                    @Describe fun method1(param: T) {}
+                    @Describe fun method2(): T = TODO()
+                    @Describe fun method3(): List<out String> = TODO()
+                    @Describe fun method4(): Map<T, String> = TODO()
+                    @Describe fun method5(): ArrayList<Map<T, String>> = TODO()
+                    @Describe fun <I, O: T> method6(param: I): O = TODO()
+                    companion object {
+                        @Describe fun <I, O : I> method7(param: I): O  = TODO()
+                        @Describe fun <I, O: String> method8(param: I): O = TODO()
+                        @Describe fun <I: Foo, O: I> method9(param: I): O  = TODO()
+                        @Describe fun <P> method10(): P where P: Collection<*>, P: Foo = TODO()
+                        @Describe fun <P> method11(): P where P: Foo, P: Collection<*> = TODO()
+                    }
+                }
+                interface Foo
+                """)
+        )
+    }
+
+    @Test
+    fun descriptor_class_erasured() {
+        fun checkSources(vararg sources: Source) {
+            runTest(sources = sources) { invocation ->
+                assertThat(invocation.annotatedElements().map(this::descriptor)).containsExactly(
+                    "method1(Ljava/lang/Object;)Ljava/lang/Object;",
+                    "method2(Ljava/lang/Object;)Ljava/lang/String;",
+                    "method3(Ljava/lang/Object;)Ljava/lang/String;",
+                    "method4(Ljava/lang/Object;)Ljava/lang/Object;",
+                    "method5(Landroidx/room/test/Outer\$Foo;)Landroidx/room/test/Outer\$Foo;",
+                    "method6(Landroidx/room/test/Outer\$Bar;)Landroidx/room/test/Outer\$Bar;",
+                )
+            }
+        }
+        checkSources(
+            Source.java(
+                "androidx.room.test.Outer",
+                """
+                package androidx.room.test;
+                class Outer {
+                    class MyClass1<I, O extends I> {
+                        @Describe O method1(I input) { return null; }
+                        @Describe static <I, O extends String> O method2(I input) { return null; }
+                    }
+                    class MyClass2<I, O extends String> {
+                        @Describe O method3(I input) { return null; }
+                        @Describe static <I, O extends I> O method4(I input) { return null; }
+                    }
+                    class MyClass3<I extends Foo, O extends I> {
+                        @Describe O method5(I input) { return null; }
+                        @Describe static <I extends Bar, O extends I> O method6(I input) {
+                         return null;
+                        }
+                    }
+                    class Foo {}
+                    class Bar {}
+                }
+                """)
+        )
+        checkSources(
+            Source.kotlin(
+                "androidx.room.test.Outer.kt",
+                """
+                package androidx.room.test
+                class Outer {
+                    class MyClass1<I, O: I> {
+                        @Describe fun method1(input: I): O = TODO()
+                        companion object {
+                            @Describe fun <I, O: String> method2(input: I): O = TODO()
+                        }
+                    }
+                    class MyClass2<I, O: String> {
+                        @Describe fun method3(input: I): O = TODO()
+                        companion object {
+                            @Describe fun <I, O: I> method4(input: I): O = TODO()
+                        }
+                    }
+                    class MyClass3<I: Foo, O: I> {
+                        @Describe fun method5(input: I): O = TODO()
+                        companion object {
+                            @Describe fun <I: Bar, O: I> method6(input: I): O = TODO()
+                        }
+                    }
+                    class Foo
+                    class Bar
+                }
+                """)
+        )
+    }
+
+    @Test
+    fun descriptor_method_primitiveParams() {
+        fun checkSources(vararg sources: Source) {
+            runTest(sources = sources) { invocation ->
+                assertThat(invocation.annotatedElements().map(this::descriptor)).containsExactly(
+                    "method1(ZI)V",
+                    "method2(C)B",
+                    "method3(DF)V",
+                    "method4(JS)V"
+                )
+            }
+        }
+        checkSources(
+            Source.java(
+                "androidx.room.test.DummyClass",
+                """
+                package androidx.room.test;
+                class DummyClass {
+                    @Describe void method1(boolean yesOrNo, int number) { }
+                    @Describe byte method2(char letter) { return 0; }
+                    @Describe void method3(double realNumber1, float realNumber2) { }
+                    @Describe void method4(long bigNumber, short littlerNumber) { }
+                }
+                """)
+        )
+        checkSources(
+            Source.kotlin(
+                "androidx.room.test.DummyClass.kt",
+                """
+                package androidx.room.test
+                class DummyClass {
+                    @Describe fun method1(yesOrNo: Boolean, number: Int) {}
+                    @Describe fun method2(letter: Char): Byte = TODO()
+                    @Describe fun method3(realNumber1: Double, realNumber2: Float) {}
+                    @Describe fun method4(bigNumber: Long, littlerNumber: Short) {}
+                }
+                """)
+        )
+    }
+
+    @Test
+    fun descriptor_method_classParam_javaTypes() {
+        fun checkSources(vararg sources: Source) {
+            runTest(sources = sources) { invocation ->
+                assertThat(invocation.annotatedElements().map(this::descriptor)).containsExactly(
+                    "method1(Ljava/lang/Object;)V",
+                    "method2()Ljava/lang/Object;",
+                    "method3(Ljava/util/ArrayList;)Ljava/util/List;",
+                    "method4()Ljava/util/Map;"
+                )
+            }
+        }
+        checkSources(
+            Source.java(
+                "androidx.room.test.DummyClass",
+                """
+                package androidx.room.test;
+
+                import java.util.ArrayList;
+                import java.util.List;
+                import java.util.Map;
+
+                class DummyClass {
+                    @Describe void method1(Object something) { }
+                    @Describe Object method2() { return null; }
+                    @Describe List<String> method3(ArrayList<Integer> list) { return null; }
+                    @Describe Map<String, Object> method4() { return null; }
+                }
+                """)
+        )
+        checkSources(
+            Source.kotlin(
+                "androidx.room.test.DummyClass.kt",
+                """
+                package androidx.room.test;
+                class DummyClass {
+                    @Describe fun method1(something: Object) {}
+                    @Describe fun method2(): Object = TODO()
+                    @Describe fun method3(list: ArrayList<Integer>): List<String> = TODO()
+                    @Describe fun method4(): Map<String, Object> = TODO()
+                }
+                """)
+        )
+    }
+
+    @Test
+    fun descriptor_method_classParam_testClass() {
+        fun checkSources(vararg sources: Source) {
+            runTest(sources = sources) { invocation ->
+                assertThat(invocation.annotatedElements().map(this::descriptor)).containsExactly(
+                    "method1(Landroidx/room/test/DataClass;)V",
+                    "method2()Landroidx/room/test/DataClass;"
+                )
+            }
+        }
+        checkSources(
+            Source.java(
+                "androidx.room.test.DataClass",
+                """
+                package androidx.room.test;
+                class DataClass {}
+                """),
+            Source.java(
+                "androidx.room.test.DummyClass",
+                """
+                package androidx.room.test;
+                class DummyClass {
+                    @Describe void method1(DataClass data) { }
+                    @Describe DataClass method2() { return null; }
+                }
+                """),
+        )
+        checkSources(
+            Source.kotlin(
+                "androidx.room.test.DummyClass.kt",
+                """
+                package androidx.room.test;
+                class DummyClass {
+                    @Describe fun method1(data: DataClass) {}
+                    @Describe fun method2(): DataClass = TODO()
+                }
+                class DataClass
+                """),
+        )
+    }
+
+    @Test
+    fun descriptor_method_classParam_innerTestClass() {
+        fun checkSources(vararg sources: Source) {
+            runTest(sources = sources) { invocation ->
+                assertThat(invocation.annotatedElements().map(this::descriptor)).containsExactly(
+                    "method1(Landroidx/room/test/DataClass\$MemberInnerData;)V",
+                    "method2(Landroidx/room/test/DataClass\$StaticInnerData;)V",
+                    "method3(Landroidx/room/test/DataClass\$EnumData;)V",
+                    "method4()Landroidx/room/test/DataClass\$StaticInnerData;"
+                )
+            }
+        }
+        checkSources(
+            Source.java(
+                "androidx.room.test.DataClass",
+                """
+                package androidx.room.test;
+                class DataClass {
+                    class MemberInnerData { }
+                    static class StaticInnerData { }
+                    enum EnumData { VALUE1, VALUE2 }
+                }
+                """
+            ),
+            Source.java(
+                "androidx.room.test.DummyClass",
+                """
+                package androidx.room.test;
+                class DummyClass {
+                    @Describe void method1(DataClass.MemberInnerData data) { }
+                    @Describe void method2(DataClass.StaticInnerData data) { }
+                    @Describe void method3(DataClass.EnumData enumData) { }
+                    @Describe DataClass.StaticInnerData method4() { return null; }
+                }
+                """),
+        )
+        checkSources(
+            Source.kotlin(
+                "androidx.room.test.DummyClass.kt",
+                """
+                package androidx.room.test
+                class DummyClass {
+                    @Describe fun method1(data: DataClass.MemberInnerData) {}
+                    @Describe fun method2(data: DataClass.StaticInnerData) {}
+                    @Describe fun method3(enumData: DataClass.EnumData) {}
+                    @Describe fun method4(): DataClass.StaticInnerData = TODO()
+                }
+                class DataClass {
+                    inner class MemberInnerData
+                    class StaticInnerData
+                    enum class EnumData { VALUE1, VALUE2 }
+                }
+                """),
+        )
+    }
+
+    @Test
+    fun descriptor_method_arrayParams() {
+        fun checkSources(vararg sources: Source) {
+            runTest(sources = sources) { invocation ->
+                assertThat(invocation.annotatedElements().map(this::descriptor)).containsExactly(
+                    "method1([Landroidx/room/test/DataClass;)V",
+                    "method2()[Landroidx/room/test/DataClass;",
+                    "method3([I)V",
+                    "method4([I)V"
+                )
+            }
+        }
+        checkSources(
+            Source.java(
+                "androidx.room.test.DataClass",
+                """
+                package androidx.room.test;
+                class DataClass {}
+                """),
+            Source.java(
+                "androidx.room.test.DummyClass",
+                """
+                package androidx.room.test;
+                class DummyClass {
+                    @Describe void method1(DataClass[] data) { }
+                    @Describe DataClass[] method2() { return null; }
+                    @Describe void method3(int[] array) { }
+                    @Describe void method4(int... array) { }
+                }
+                """),
+        )
+        checkSources(
+            Source.kotlin(
+                "androidx.room.test.DummyClass.kt",
+                """
+                package androidx.room.test;
+                class DummyClass {
+                    @Describe fun method1(data: Array<DataClass>) {}
+                    @Describe fun method2(): Array<DataClass> = TODO()
+                    @Describe fun method3(array: IntArray) {}
+                    @Describe fun method4(vararg array: Int) {}
+                }
+                class DataClass
+                """),
+        )
+    }
+
+    private fun runTest(vararg sources: Source, handler: (XTestInvocation) -> Unit) {
+        runProcessorTest(
+            sources = sources.toList() + describeAnnotation,
+            handler = handler
+        )
+    }
+
+    private fun XTestInvocation.annotatedElements(): Set<XElement> {
+        return roundEnv.getElementsAnnotatedWith("androidx.room.test.Describe")
+    }
+
+    private fun descriptor(element: XElement): String {
+        return when {
+            element.isField() -> element.jvmDescriptor()
+            element.isMethod() -> element.jvmDescriptor()
+            element.isConstructor() -> element.jvmDescriptor()
+            else -> error("Unsupported element to describe.")
+        }
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeTest.kt
index ef0ec14..35cf851 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeTest.kt
@@ -19,19 +19,23 @@
 import androidx.room.compiler.codegen.XClassName
 import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.codegen.asClassName
+import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XNullability.NONNULL
 import androidx.room.compiler.processing.XNullability.NULLABLE
 import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XTypeElement
 import androidx.room.compiler.processing.isByte
 import androidx.room.compiler.processing.isInt
 import androidx.room.compiler.processing.isLong
 import androidx.room.compiler.processing.isVoid
 import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.getDeclaredField
 import androidx.room.compiler.processing.util.getField
 import androidx.room.compiler.processing.util.getMethodByJvmName
 import androidx.room.compiler.processing.util.runKspTest
 import androidx.room.compiler.processing.util.runProcessorTest
 import com.google.common.truth.Truth.assertThat
+import com.squareup.kotlinpoet.TypeVariableName
 import com.squareup.kotlinpoet.UNIT
 import com.squareup.kotlinpoet.javapoet.JClassName
 import com.squareup.kotlinpoet.javapoet.JTypeName
@@ -477,50 +481,97 @@
 
     @Suppress("MapGetWithNotNullAssertionOperator")
     @Test
-    fun extendsBounds() {
-        val src = Source.kotlin(
-            "foo.kt",
-            """
-            open class Foo
-            class Bar<T : Foo> {
-            }
-            class Bar_NullableFoo<T : Foo?>
-            """.trimIndent()
-        )
+    fun upperBounds() {
         runProcessorTest(
-            listOf(src)
+            listOf(
+                Source.kotlin(
+                    "Foo.kt",
+                    """
+                    open class Foo
+                    class Bar<T : Foo>
+                    class Bar_NullableFoo<T : Foo?>
+                    """.trimIndent()
+                )
+            )
         ) { invocation ->
-            val classNames = listOf("Bar", "Bar_NullableFoo")
-            val typeArgs = classNames.associateWith { className ->
-                invocation.processingEnv
-                    .requireType(className)
-                    .typeArguments
-                    .single()
+            fun checkTypeElement(
+                typeElement: XTypeElement,
+                typeArgumentJClassName: JTypeVariableName,
+                typeArgumentKClassName: TypeVariableName,
+                nullability: XNullability,
+            ) {
+                val typeParameter = typeElement.typeParameters.single()
+                assertThat(typeParameter.typeVariableName).isEqualTo(typeArgumentJClassName)
+
+                val typeArgument = typeElement.type.typeArguments.single()
+                assertThat(typeArgument.asTypeName().java).isEqualTo(typeArgumentJClassName)
+                if (invocation.isKsp) {
+                    assertThat(typeArgument.asTypeName().kotlin).isEqualTo(typeArgumentKClassName)
+                }
+                assertThat(typeArgument.nullability).isEqualTo(nullability)
             }
-            assertThat(typeArgs["Bar"]!!.asTypeName().java)
-                .isEqualTo(
-                    JTypeVariableName.get("T", JClassName.get("", "Foo"))
+            checkTypeElement(
+                typeElement = invocation.processingEnv.requireTypeElement("Bar"),
+                typeArgumentJClassName = JTypeVariableName.get("T", JClassName.get("", "Foo")),
+                typeArgumentKClassName = KTypeVariableName("T", KClassName("", "Foo")),
+                nullability = NONNULL
+            )
+            checkTypeElement(
+                typeElement = invocation.processingEnv.requireTypeElement("Bar_NullableFoo"),
+                typeArgumentJClassName = JTypeVariableName.get("T", JClassName.get("", "Foo")),
+                typeArgumentKClassName = KTypeVariableName(
+                    "T", KClassName("", "Foo").copy(nullable = true)
+                ),
+                nullability = NULLABLE
+            )
+        }
+    }
+
+    @Suppress("MapGetWithNotNullAssertionOperator")
+    @Test
+    fun extendsBounds() {
+        runProcessorTest(
+            listOf(
+                Source.kotlin(
+                    "Foo.kt",
+                    """
+                    class Foo {
+                        val barFoo: Bar<out Foo> = TODO()
+                        val barNullableFoo: Bar<out Foo?> = TODO()
+                    }
+                    class Bar<T>
+                    """.trimIndent()
                 )
-            if (invocation.isKsp) {
-                assertThat(typeArgs["Bar"]!!.asTypeName().kotlin)
-                    .isEqualTo(
-                        KTypeVariableName("T", KClassName("", "Foo"))
-                    )
+            )
+        ) { invocation ->
+            fun checkType(
+                type: XType,
+                typeArgumentJClassName: JWildcardTypeName,
+                typeArgumentKClassName: KWildcardTypeName,
+                nullability: XNullability,
+            ) {
+                val typeArgument = type.typeArguments.single()
+                assertThat(typeArgument.asTypeName().java).isEqualTo(typeArgumentJClassName)
+                if (invocation.isKsp) {
+                    assertThat(typeArgument.asTypeName().kotlin).isEqualTo(typeArgumentKClassName)
+                }
+                assertThat(typeArgument.nullability).isEqualTo(nullability)
             }
-            assertThat(typeArgs["Bar"]!!.nullability).isEqualTo(NONNULL)
-            assertThat(typeArgs["Bar_NullableFoo"]!!.asTypeName().java)
-                .isEqualTo(
-                    JTypeVariableName.get("T", JClassName.get("", "Foo"))
-                )
-            if (invocation.isKsp) {
-                assertThat(typeArgs["Bar_NullableFoo"]!!.asTypeName().kotlin)
-                    .isEqualTo(
-                        KTypeVariableName(
-                            "T", KClassName("", "Foo").copy(nullable = true)
-                        )
-                    )
-            }
-            assertThat(typeArgs["Bar_NullableFoo"]!!.nullability).isEqualTo(NULLABLE)
+            val foo = invocation.processingEnv.requireTypeElement("Foo")
+            checkType(
+                type = foo.getDeclaredField("barFoo").type,
+                typeArgumentJClassName = JWildcardTypeName.subtypeOf(JClassName.get("", "Foo")),
+                typeArgumentKClassName = KWildcardTypeName.producerOf(KClassName("", "Foo")),
+                nullability = NONNULL
+            )
+            checkType(
+                type = foo.getDeclaredField("barNullableFoo").type,
+                typeArgumentJClassName = JWildcardTypeName.subtypeOf(JClassName.get("", "Foo")),
+                typeArgumentKClassName = KWildcardTypeName.producerOf(
+                    KClassName("", "Foo").copy(nullable = true)
+                ),
+                nullability = NULLABLE
+            )
         }
     }
 
diff --git a/room/room-compiler/build.gradle b/room/room-compiler/build.gradle
index e0d4d13..08515b3 100644
--- a/room/room-compiler/build.gradle
+++ b/room/room-compiler/build.gradle
@@ -101,7 +101,6 @@
     implementation(libs.kspApi)
     shadowed(libs.antlr4)
     implementation(libs.sqliteJdbc)
-    implementation(libs.kotlinMetadataJvm)
     implementation(libs.apacheCommonsCodec)
     implementation(libs.intellijAnnotations)
     testImplementation(libs.truth)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/package_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/package_ext.kt
index 446eb00..515bda9 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/package_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/package_ext.kt
@@ -26,7 +26,7 @@
  *
  * Example of a typical config:
  * ```
- * # Room dejetifier packages for JavaPoet class names.
+ * # Room dejetifier packages for XPoet class names.
  * androidx.sqlite = android.arch.persistence
  * androidx.room = android.arch.persistence.room
  * androidx.paging = android.arch.paging
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
index 0ff01df..8895edf 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
@@ -28,21 +28,8 @@
 import androidx.room.compiler.codegen.XTypeSpec
 import androidx.room.compiler.codegen.asClassName
 import androidx.room.compiler.codegen.asMutableClassName
-import com.squareup.javapoet.ArrayTypeName
-import com.squareup.javapoet.ClassName
+import com.squareup.kotlinpoet.javapoet.JTypeName
 import java.util.concurrent.Callable
-import kotlin.reflect.KClass
-
-val L = "\$L"
-val T = "\$T"
-val N = "\$N"
-val S = "\$S"
-val W = "\$W"
-
-val KClass<*>.typeName: ClassName
-    get() = ClassName.get(this.java)
-val KClass<*>.arrayTypeName: ArrayTypeName
-    get() = ArrayTypeName.of(typeName)
 
 object SupportDbTypeNames {
     val DB = XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteDatabase")
@@ -89,10 +76,8 @@
 }
 
 object PagingTypeNames {
-    val DATA_SOURCE: ClassName =
-        ClassName.get(PAGING_PACKAGE, "DataSource")
-    val POSITIONAL_DATA_SOURCE: ClassName =
-        ClassName.get(PAGING_PACKAGE, "PositionalDataSource")
+    val DATA_SOURCE = XClassName.get(PAGING_PACKAGE, "DataSource")
+    val POSITIONAL_DATA_SOURCE = XClassName.get(PAGING_PACKAGE, "PositionalDataSource")
     val DATA_SOURCE_FACTORY = XClassName.get(PAGING_PACKAGE, "DataSource", "Factory")
     val PAGING_SOURCE = XClassName.get(PAGING_PACKAGE, "PagingSource")
     val LISTENABLE_FUTURE_PAGING_SOURCE =
@@ -332,7 +317,7 @@
             callBody()
         }.apply(
             javaMethodBuilder = {
-                addException(Exception::class.typeName)
+                addException(JTypeName.get(Exception::class.java))
             },
             kotlinFunctionBuilder = { }
         ).build()
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/preconditions/Checks.kt b/room/room-compiler/src/main/kotlin/androidx/room/preconditions/Checks.kt
index f91c0b4..b57d3fd 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/preconditions/Checks.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/preconditions/Checks.kt
@@ -18,6 +18,7 @@
 
 import androidx.room.compiler.processing.XElement
 import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.isTypeVariable
 import androidx.room.log.RLog
 import kotlin.contracts.contract
 import kotlin.reflect.KClass
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..b077b98 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
@@ -206,6 +206,9 @@
         "private, final, or abstract. It can be abstract only if the method is also" +
         " annotated with @Query."
 
+    fun nullableParamInShortcutMethod(param: String) = "Methods annotated with [@Insert, " +
+        "@Upsert, @Update, @Delete] shouldn't declare nullable parameters ($param)."
+
     fun transactionMethodAsync(returnTypeName: String) = "Method annotated with @Transaction must" +
         " not return deferred/async return type $returnTypeName. Since transactions are" +
         " thread confined and Room cannot guarantee that all queries in the method" +
@@ -1130,4 +1133,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/processor/ShortcutParameterProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutParameterProcessor.kt
index d706e03..442c03e 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutParameterProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutParameterProcessor.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.processor
 
+import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XVariableElement
 import androidx.room.compiler.processing.isArray
@@ -32,10 +33,20 @@
     val context = baseContext.fork(element)
     fun process(): ShortcutQueryParameter {
         val asMember = element.asMemberOf(containing)
+        if (isParamNullable(asMember)) {
+            context.logger.e(
+                element = element,
+                msg = ProcessorErrors.nullableParamInShortcutMethod(
+                    asMember.asTypeName().toString(context.codeLanguage)
+                )
+            )
+        }
+
         val name = element.name
         context.checker.check(
-            !name.startsWith("_"), element,
-            ProcessorErrors.QUERY_PARAMETERS_CANNOT_START_WITH_UNDERSCORE
+            !name.startsWith("_"),
+            element = element,
+            errorMsg = ProcessorErrors.QUERY_PARAMETERS_CANNOT_START_WITH_UNDERSCORE
         )
 
         val (pojoType, isMultiple) = extractPojoType(asMember)
@@ -48,6 +59,13 @@
         )
     }
 
+    private fun isParamNullable(paramType: XType): Boolean {
+        if (paramType.nullability == XNullability.NULLABLE) return true
+        if (paramType.isArray() && paramType.componentType.nullability == XNullability.NULLABLE)
+            return true
+        return paramType.typeArguments.any { it.nullability == XNullability.NULLABLE }
+    }
+
     @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
     private fun extractPojoType(typeMirror: XType): Pair<XType?, Boolean> {
 
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/TransactionMethodProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/TransactionMethodProcessor.kt
index 24add23..71f283c 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/TransactionMethodProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/TransactionMethodProcessor.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.processor
 
+import androidx.room.compiler.codegen.CodeLanguage
 import androidx.room.compiler.processing.XMethodElement
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeElement
@@ -68,10 +69,22 @@
                 TransactionMethod.CallType.CONCRETE
         }
 
+        val isVarArgs = executableElement.isVarArgs()
+        val parameters = delegate.extractParams()
+        val processedParamNames = parameters.mapIndexed { index, param ->
+            // Apply spread operator when delegating to a vararg parameter in Kotlin.
+            if (context.codeLanguage == CodeLanguage.KOTLIN &&
+                isVarArgs && index == parameters.size - 1) {
+                "*${param.name}"
+            } else {
+                param.name
+            }
+        }
+
         return TransactionMethod(
             element = executableElement,
             returnType = returnType,
-            parameterNames = delegate.extractParams().map { it.name },
+            parameterNames = processedParamNames,
             callType = callType,
             methodBinder = delegate.findTransactionMethodBinder(callType)
         )
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt
index 9d622a4..a4e230b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt
@@ -17,9 +17,7 @@
 package androidx.room.solver
 
 import androidx.annotation.VisibleForTesting
-import androidx.room.compiler.codegen.JCodeBlockBuilder
 import androidx.room.compiler.codegen.XCodeBlock
-import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.apply
 import androidx.room.writer.TypeWriter
 
 /**
@@ -44,17 +42,6 @@
             "$prefix${if (index == 0) "" else "_$index"}"
     }
 
-    // TODO(b/248383583): Remove once XPoet is more widely adopted.
-    // @Deprecated("Use property, will be removed once more writers are migrated to XPoet")
-    fun builder(): JCodeBlockBuilder {
-        lateinit var leakJavaBuilder: JCodeBlockBuilder
-        builder.apply(
-            javaCodeBuilder = { leakJavaBuilder = this },
-            kotlinCodeBuilder = { error("KotlinPoet code builder should have not been invoked!") }
-        )
-        return leakJavaBuilder
-    }
-
     fun getTmpVar(): String {
         return getTmpVar(TMP_VAR_DEFAULT_PREFIX)
     }
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..8d6dbe0 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,9 @@
 package androidx.room.solver
 
 import androidx.annotation.VisibleForTesting
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XTypeName
+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,15 +118,14 @@
 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
 import com.google.common.collect.ImmutableMap
 import com.google.common.collect.ImmutableMultimap
 import com.google.common.collect.ImmutableSetMultimap
-import com.squareup.javapoet.TypeName
 
-@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
 /**
  * Holds all type adapters and can create on demand composite type adapters to convert a type into a
  * database column.
@@ -199,7 +201,7 @@
         }
     }
 
-    val queryResultBinderProviders: List<QueryResultBinderProvider> =
+    private val queryResultBinderProviders: List<QueryResultBinderProvider> =
         mutableListOf<QueryResultBinderProvider>().apply {
             add(CursorQueryResultBinderProvider(context))
             add(LiveDataQueryResultBinderProvider(context))
@@ -216,28 +218,28 @@
             add(InstantQueryResultBinderProvider(context))
         }
 
-    val preparedQueryResultBinderProviders: List<PreparedQueryResultBinderProvider> =
+    private val preparedQueryResultBinderProviders: List<PreparedQueryResultBinderProvider> =
         mutableListOf<PreparedQueryResultBinderProvider>().apply {
             addAll(RxPreparedQueryResultBinderProvider.getAll(context))
             add(GuavaListenableFuturePreparedQueryResultBinderProvider(context))
             add(InstantPreparedQueryResultBinderProvider(context))
         }
 
-    val insertBinderProviders: List<InsertOrUpsertMethodBinderProvider> =
+    private val insertBinderProviders: List<InsertOrUpsertMethodBinderProvider> =
         mutableListOf<InsertOrUpsertMethodBinderProvider>().apply {
             addAll(RxCallableInsertMethodBinderProvider.getAll(context))
             add(GuavaListenableFutureInsertMethodBinderProvider(context))
             add(InstantInsertMethodBinderProvider(context))
         }
 
-    val deleteOrUpdateBinderProvider: List<DeleteOrUpdateMethodBinderProvider> =
+    private val deleteOrUpdateBinderProvider: List<DeleteOrUpdateMethodBinderProvider> =
         mutableListOf<DeleteOrUpdateMethodBinderProvider>().apply {
             addAll(RxCallableDeleteOrUpdateMethodBinderProvider.getAll(context))
             add(GuavaListenableFutureDeleteOrUpdateMethodBinderProvider(context))
             add(InstantDeleteOrUpdateMethodBinderProvider(context))
         }
 
-    val upsertBinderProviders: List<InsertOrUpsertMethodBinderProvider> =
+    private val upsertBinderProviders: List<InsertOrUpsertMethodBinderProvider> =
         mutableListOf<InsertOrUpsertMethodBinderProvider>().apply {
             addAll(RxCallableUpsertMethodBinderProvider.getAll(context))
             add(GuavaListenableFutureUpsertMethodBinderProvider(context))
@@ -480,6 +482,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 +490,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 +502,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 +514,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 +523,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 +534,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 +559,7 @@
         ) {
             val keyTypeArg = typeMirror.typeArguments[0].extendsBoundOrSelf()
             val valueTypeArg = typeMirror.typeArguments[1].extendsBoundOrSelf()
+            checkTypeNullability(typeMirror, keyTypeArg)
 
             if (valueTypeArg.typeElement == null) {
                 context.logger.e(
@@ -611,12 +623,13 @@
             }
             val keyTypeArg = when (mapType) {
                 MultimapQueryResultAdapter.MapType.LONG_SPARSE ->
-                    context.processingEnv.requireType(TypeName.LONG)
+                    context.processingEnv.requireType(XTypeName.PRIMITIVE_LONG)
                 MultimapQueryResultAdapter.MapType.INT_SPARSE ->
-                    context.processingEnv.requireType(TypeName.INT)
+                    context.processingEnv.requireType(XTypeName.PRIMITIVE_INT)
                 else ->
                     typeMirror.typeArguments[0].extendsBoundOrSelf()
             }
+            checkTypeNullability(typeMirror, keyTypeArg)
 
             val mapValueTypeArg = if (mapType.isSparseArray()) {
                 typeMirror.typeArguments[0].extendsBoundOrSelf()
@@ -723,6 +736,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 +924,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/solver/binderprovider/DataSourceQueryResultBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt
index a3c2db3f..25688ea 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt
@@ -30,11 +30,15 @@
 
 class DataSourceQueryResultBinderProvider(val context: Context) : QueryResultBinderProvider {
     private val dataSourceType: XRawType? by lazy {
-        context.processingEnv.findType(PagingTypeNames.DATA_SOURCE)?.rawType
+        context.processingEnv.findType(
+            PagingTypeNames.DATA_SOURCE.canonicalName
+        )?.rawType
     }
 
     private val positionalDataSourceType: XRawType? by lazy {
-        context.processingEnv.findType(PagingTypeNames.POSITIONAL_DATA_SOURCE)?.rawType
+        context.processingEnv.findType(
+            PagingTypeNames.POSITIONAL_DATA_SOURCE.canonicalName
+        )?.rawType
     }
 
     override fun provide(
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/main/kotlin/androidx/room/writer/TypeWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/TypeWriter.kt
index 7249aa7..2e46c6e 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/TypeWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/TypeWriter.kt
@@ -26,7 +26,6 @@
 import androidx.room.compiler.codegen.XTypeSpec.Builder.Companion.apply
 import androidx.room.compiler.processing.XProcessingEnv
 import androidx.room.compiler.processing.writeTo
-import androidx.room.ext.S
 import androidx.room.solver.CodeGenScope
 import com.squareup.kotlinpoet.javapoet.JAnnotationSpec
 import com.squareup.kotlinpoet.javapoet.JClassName
@@ -82,7 +81,7 @@
             javaTypeBuilder = {
                 addAnnotation(
                     com.squareup.javapoet.AnnotationSpec.builder(SuppressWarnings::class.java)
-                        .addMember("value", "{$S, $S}", "unchecked", "deprecation")
+                        .addMember("value", "{\$S, \$S}", "unchecked", "deprecation")
                         .build()
                 )
             },
@@ -107,7 +106,7 @@
                 javaTypeBuilder = {
                     addAnnotation(
                         JAnnotationSpec.builder(JClassName.bestGuess(annotationName))
-                            .addMember("value", "$S", memberValue)
+                            .addMember("value", "\$S", memberValue)
                             .build()
                     )
                 },
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/CustomConverterProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/CustomConverterProcessorTest.kt
index aa449e9..4b9437c 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/CustomConverterProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/CustomConverterProcessorTest.kt
@@ -17,11 +17,22 @@
 package androidx.room.processor
 
 import androidx.room.TypeConverter
-import androidx.room.compiler.codegen.toJavaPoet
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.VisibilityModifier
+import androidx.room.compiler.codegen.XAnnotationSpec
+import androidx.room.compiler.codegen.XClassName
+import androidx.room.compiler.codegen.XFunSpec
+import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.addStatement
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.XTypeSpec
+import androidx.room.compiler.codegen.XTypeSpec.Builder.Companion.apply
+import androidx.room.compiler.codegen.asClassName
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.XTestInvocation
 import androidx.room.compiler.processing.util.runProcessorTest
-import androidx.room.ext.typeName
+import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.CommonTypeNames.MUTABLE_LIST
+import androidx.room.ext.CommonTypeNames.STRING
 import androidx.room.processor.ProcessorErrors.TYPE_CONVERTER_EMPTY_CLASS
 import androidx.room.processor.ProcessorErrors.TYPE_CONVERTER_MISSING_NOARG_CONSTRUCTOR
 import androidx.room.processor.ProcessorErrors.TYPE_CONVERTER_MUST_BE_PUBLIC
@@ -29,26 +40,18 @@
 import androidx.room.testing.context
 import androidx.room.vo.CustomTypeConverter
 import com.google.common.truth.Truth.assertThat
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
 import com.squareup.javapoet.TypeVariableName
 import java.util.Date
-import javax.lang.model.element.Modifier
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
 class CustomConverterProcessorTest {
+
     companion object {
-        val CONVERTER = ClassName.get("foo.bar", "MyConverter")!!
-        val CONVERTER_QNAME = CONVERTER.packageName() + "." + CONVERTER.simpleName()
+        val CONVERTER = XClassName.get("foo.bar", "MyConverter")
+        val CONVERTER_NAME = CONVERTER.canonicalName
         val CONTAINER = Source.java(
             "foo.bar.Container",
             """
@@ -62,53 +65,76 @@
 
     @Test
     fun validCase() {
-        singleClass(createConverter(TypeName.SHORT.box(), TypeName.CHAR.box())) { converter, _ ->
-            assertThat(converter?.fromTypeName?.toJavaPoet(), `is`(TypeName.SHORT.box()))
-            assertThat(converter?.toTypeName?.toJavaPoet(), `is`(TypeName.CHAR.box()))
+        singleClass(
+            createConverter(
+                XTypeName.BOXED_SHORT.copy(nullable = true),
+                XTypeName.BOXED_CHAR.copy(nullable = true)
+            )
+        ) {
+                converter, _ ->
+            assertThat(converter?.fromTypeName).isEqualTo(
+                XTypeName.BOXED_SHORT.copy(nullable = true)
+            )
+            assertThat(converter?.toTypeName).isEqualTo(XTypeName.BOXED_CHAR.copy(nullable = true))
         }
     }
 
     @Test
     fun primitiveFrom() {
-        singleClass(createConverter(TypeName.SHORT, TypeName.CHAR.box())) { converter, _ ->
-            assertThat(converter?.fromTypeName?.toJavaPoet(), `is`(TypeName.SHORT))
-            assertThat(converter?.toTypeName?.toJavaPoet(), `is`(TypeName.CHAR.box()))
+        singleClass(
+            createConverter(
+                XTypeName.PRIMITIVE_SHORT,
+                XTypeName.BOXED_CHAR.copy(nullable = true)
+            )
+        ) {
+                converter, _ ->
+            assertThat(converter?.fromTypeName).isEqualTo(XTypeName.PRIMITIVE_SHORT)
+            assertThat(converter?.toTypeName).isEqualTo(XTypeName.BOXED_CHAR.copy(nullable = true))
         }
     }
 
     @Test
     fun primitiveTo() {
-        singleClass(createConverter(TypeName.INT.box(), TypeName.DOUBLE)) { converter, _ ->
-            assertThat(converter?.fromTypeName?.toJavaPoet(), `is`(TypeName.INT.box()))
-            assertThat(converter?.toTypeName?.toJavaPoet(), `is`(TypeName.DOUBLE))
+        singleClass(
+            createConverter(
+                XTypeName.BOXED_INT.copy(nullable = true),
+                XTypeName.PRIMITIVE_DOUBLE)
+        ) {
+                converter, _ ->
+            assertThat(converter?.fromTypeName).isEqualTo(XTypeName.BOXED_INT.copy(nullable = true))
+            assertThat(converter?.toTypeName).isEqualTo(XTypeName.PRIMITIVE_DOUBLE)
         }
     }
 
     @Test
     fun primitiveBoth() {
-        singleClass(createConverter(TypeName.INT, TypeName.DOUBLE)) { converter, _ ->
-            assertThat(converter?.fromTypeName?.toJavaPoet(), `is`(TypeName.INT))
-            assertThat(converter?.toTypeName?.toJavaPoet(), `is`(TypeName.DOUBLE))
+        singleClass(createConverter(XTypeName.PRIMITIVE_INT, XTypeName.PRIMITIVE_DOUBLE)) {
+                converter, _ ->
+            assertThat(converter?.fromTypeName).isEqualTo(XTypeName.PRIMITIVE_INT)
+            assertThat(converter?.toTypeName).isEqualTo(XTypeName.PRIMITIVE_DOUBLE)
         }
     }
 
     @Test
     fun nonNullButNotBoxed() {
-        val string = String::class.typeName
-        val date = Date::class.typeName
-        singleClass(createConverter(string, date)) { converter, _ ->
-            assertThat(converter?.fromTypeName?.toJavaPoet(), `is`(string as TypeName))
-            assertThat(converter?.toTypeName?.toJavaPoet(), `is`(date as TypeName))
+        val date = Date::class.asClassName()
+        singleClass(createConverter(STRING, date)) { converter, _ ->
+            assertThat(converter?.fromTypeName).isEqualTo(STRING)
+            assertThat(converter?.toTypeName).isEqualTo(date)
         }
     }
 
     @Test
     fun parametrizedTypeUnbound() {
         val typeVarT = TypeVariableName.get("T")
-        val list = ParameterizedTypeName.get(List::class.typeName, typeVarT)
+        val list = CommonTypeNames.MUTABLE_LIST.parametrizedBy(XClassName.get("", "T"))
         val typeVarK = TypeVariableName.get("K")
-        val map = ParameterizedTypeName.get(Map::class.typeName, typeVarK, typeVarT)
-        singleClass(createConverter(list, map, listOf(typeVarK, typeVarT))) { _, invocation ->
+        val map = CommonTypeNames.MUTABLE_MAP.parametrizedBy(
+            XClassName.get("", "K"),
+            XClassName.get("", "T")
+        )
+        singleClass(createConverter(list, map, listOf(typeVarK, typeVarT))) {
+                _, invocation ->
             invocation.assertCompilationResult {
                 hasErrorContaining(TYPE_CONVERTER_UNBOUND_GENERIC)
             }
@@ -117,13 +143,12 @@
 
     @Test
     fun parametrizedTypeSpecific() {
-        val string = String::class.typeName
-        val date = Date::class.typeName
-        val list = ParameterizedTypeName.get(List::class.typeName, string)
-        val map = ParameterizedTypeName.get(Map::class.typeName, string, date)
+        val date = Date::class.asClassName()
+        val list = CommonTypeNames.MUTABLE_LIST.parametrizedBy(STRING)
+        val map = CommonTypeNames.MUTABLE_MAP.parametrizedBy(STRING, date)
         singleClass(createConverter(list, map)) { converter, _ ->
-            assertThat(converter?.fromTypeName?.toJavaPoet(), `is`(list as TypeName))
-            assertThat(converter?.toTypeName?.toJavaPoet(), `is`(map as TypeName))
+            assertThat(converter?.fromTypeName).isEqualTo(list)
+            assertThat(converter?.toTypeName).isEqualTo(map)
         }
     }
 
@@ -131,10 +156,10 @@
     fun testNoConverters() {
         singleClass(
             Source.java(
-                CONVERTER_QNAME,
+                CONVERTER_NAME,
                 """
-                package ${CONVERTER.packageName()};
-                public class ${CONVERTER.simpleName()} {
+                package ${CONVERTER.packageName};
+                public class ${CONVERTER.simpleNames.first()} {
                 }
                 """
             )
@@ -149,13 +174,13 @@
     fun checkNoArgConstructor() {
         singleClass(
             Source.java(
-                CONVERTER_QNAME,
+                CONVERTER_NAME,
                 """
-                package ${CONVERTER.packageName()};
+                package ${CONVERTER.packageName};
                 import androidx.room.TypeConverter;
 
-                public class ${CONVERTER.simpleName()} {
-                    public ${CONVERTER.simpleName()}(int x) {}
+                public class ${CONVERTER.simpleNames.first()} {
+                    public ${CONVERTER.simpleNames.first()}(int x) {}
                     @TypeConverter
                     public int x(short y) {return 0;}
                 }
@@ -172,22 +197,22 @@
     fun checkNoArgConstructor_withStatic() {
         singleClass(
             Source.java(
-                CONVERTER_QNAME,
+                CONVERTER_NAME,
                 """
-                package ${CONVERTER.packageName()};
+                package ${CONVERTER.packageName};
                 import androidx.room.TypeConverter;
 
-                public class ${CONVERTER.simpleName()} {
-                    public ${CONVERTER.simpleName()}(int x) {}
+                public class ${CONVERTER.simpleNames.first()} {
+                    public ${CONVERTER.simpleNames.first()}(int x) {}
                     @TypeConverter
                     public static int x(short y) {return 0;}
                 }
                 """
             )
         ) { converter, _ ->
-            assertThat(converter?.fromTypeName?.toJavaPoet(), `is`(TypeName.SHORT))
-            assertThat(converter?.toTypeName?.toJavaPoet(), `is`(TypeName.INT))
-            assertThat(converter?.isStatic, `is`(true))
+            assertThat(converter?.fromTypeName).isEqualTo(XTypeName.PRIMITIVE_SHORT)
+            assertThat(converter?.toTypeName).isEqualTo(XTypeName.PRIMITIVE_INT)
+            assertThat(converter?.isStatic).isTrue()
         }
     }
 
@@ -195,21 +220,21 @@
     fun checkPublic() {
         singleClass(
             Source.java(
-                CONVERTER_QNAME,
+                CONVERTER_NAME,
                 """
-                package ${CONVERTER.packageName()};
+                package ${CONVERTER.packageName};
                 import androidx.room.TypeConverter;
 
-                public class ${CONVERTER.simpleName()} {
+                public class ${CONVERTER.simpleNames.first()} {
                     @TypeConverter static int x(short y) {return 0;}
                     @TypeConverter private static int y(boolean y) {return 0;}
                 }
                 """
             )
         ) { converter, invocation ->
-            assertThat(converter?.fromTypeName?.toJavaPoet(), `is`(TypeName.SHORT))
-            assertThat(converter?.toTypeName?.toJavaPoet(), `is`(TypeName.INT))
-            assertThat(converter?.isStatic, `is`(true))
+            assertThat(converter?.fromTypeName).isEqualTo(XTypeName.PRIMITIVE_SHORT)
+            assertThat(converter?.toTypeName).isEqualTo(XTypeName.PRIMITIVE_INT)
+            assertThat(converter?.isStatic).isTrue()
             invocation.assertCompilationResult {
                 hasErrorContaining(TYPE_CONVERTER_MUST_BE_PUBLIC)
                 hasErrorCount(2)
@@ -217,51 +242,47 @@
         }
     }
 
-    @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
     @Test
     fun parametrizedTypeBoundViaParent() {
         val typeVarT = TypeVariableName.get("T")
-        val list = ParameterizedTypeName.get(List::class.typeName, typeVarT)
+        val list = CommonTypeNames.MUTABLE_LIST.parametrizedBy(XClassName.get("", "T"))
         val typeVarK = TypeVariableName.get("K")
-        val map = ParameterizedTypeName.get(Map::class.typeName, typeVarK, typeVarT)
-
-        val baseConverter = createConverter(list, map, listOf(typeVarT, typeVarK))
-        val extendingQName = "foo.bar.Extending"
+        val map = CommonTypeNames.MUTABLE_MAP.parametrizedBy(
+            XClassName.get("", "K"),
+            XClassName.get("", "T")
+        )
+        val baseConverter = createConverter(
+            list,
+            map,
+            typeVariables = listOf(typeVarT, typeVarK)
+        )
+        val extendingClassName = XClassName.get("foo.bar", "Extending")
         val extendingClass = Source.java(
-            extendingQName,
+            extendingClassName.canonicalName,
             "package foo.bar;\n" +
-                TypeSpec.classBuilder(ClassName.bestGuess(extendingQName)).apply {
+                XTypeSpec.classBuilder(
+                    CodeLanguage.JAVA,
+                    extendingClassName
+                ).apply {
                     superclass(
-                        ParameterizedTypeName.get(
-                            CONVERTER, String::class.typeName,
-                            Integer::class.typeName
+                        CONVERTER.parametrizedBy(
+                            STRING,
+                            XTypeName.BOXED_INT
                         )
                     )
                 }.build().toString()
         )
-
         runProcessorTest(
             sources = listOf(baseConverter, extendingClass)
         ) { invocation ->
-            val element = invocation.processingEnv.requireTypeElement(extendingQName)
+            val element = invocation.processingEnv.requireTypeElement(
+                extendingClassName.canonicalName
+            )
             val converter = CustomConverterProcessor(invocation.context, element)
                 .process().firstOrNull()
-            assertThat(
-                converter?.fromTypeName?.toJavaPoet(),
-                `is`(
-                    ParameterizedTypeName.get(
-                        List::class.typeName, String::class.typeName
-                    ) as TypeName
-                )
-            )
-            assertThat(
-                converter?.toTypeName?.toJavaPoet(),
-                `is`(
-                    ParameterizedTypeName.get(
-                        Map::class.typeName,
-                        Integer::class.typeName, String::class.typeName
-                    ) as TypeName
-                )
+            assertThat(converter?.fromTypeName).isEqualTo(MUTABLE_LIST.parametrizedBy(STRING))
+            assertThat(converter?.toTypeName).isEqualTo(
+                CommonTypeNames.MUTABLE_MAP.parametrizedBy(XTypeName.BOXED_INT, STRING)
             )
         }
     }
@@ -269,10 +290,16 @@
     @Test
     fun checkDuplicates() {
         singleClass(
-            createConverter(TypeName.SHORT.box(), TypeName.CHAR.box(), duplicate = true)
+            createConverter(
+                XTypeName.BOXED_SHORT.copy(nullable = true),
+                XTypeName.BOXED_CHAR.copy(nullable = true),
+                duplicate = true
+            )
         ) { converter, invocation ->
-            assertThat(converter?.fromTypeName?.toJavaPoet(), `is`(TypeName.SHORT.box()))
-            assertThat(converter?.toTypeName?.toJavaPoet(), `is`(TypeName.CHAR.box()))
+            assertThat(converter?.fromTypeName).isEqualTo(
+                XTypeName.BOXED_SHORT.copy(nullable = true)
+            )
+            assertThat(converter?.toTypeName).isEqualTo(XTypeName.BOXED_CHAR.copy(nullable = true))
             invocation.assertCompilationResult {
                 hasErrorContaining("Multiple methods define the same conversion")
             }
@@ -284,9 +311,9 @@
         val source = Source.kotlin(
             "MyConverter.kt",
             """
-        package ${CONVERTER.packageName()}
+        package ${CONVERTER.packageName}
         import androidx.room.*
-        class ${CONVERTER.simpleName()} {
+        class ${CONVERTER.simpleNames.first()} {
             @TypeConverter
             fun nonNulls(input: Int): String {
                 TODO()
@@ -345,33 +372,45 @@
     }
 
     private fun createConverter(
-        from: TypeName,
-        to: TypeName,
+        from: XTypeName,
+        to: XTypeName,
         typeVariables: List<TypeVariableName> = emptyList(),
         duplicate: Boolean = false
     ): Source {
-        val code = TypeSpec.classBuilder(CONVERTER).apply {
-            addTypeVariables(typeVariables)
-            addModifiers(Modifier.PUBLIC)
-            fun buildMethod(name: String) = MethodSpec.methodBuilder(name).apply {
-                addAnnotation(TypeConverter::class.java)
-                addModifiers(Modifier.PUBLIC)
+        val code = XTypeSpec.classBuilder(CodeLanguage.JAVA, CONVERTER, isOpen = true).apply {
+            setVisibility(VisibilityModifier.PUBLIC)
+            fun buildMethod(name: String) = XFunSpec.builder(
+                CodeLanguage.JAVA,
+                name,
+                VisibilityModifier.PUBLIC
+            ).apply {
+                addAnnotation(
+                    XAnnotationSpec.builder(
+                        CodeLanguage.JAVA,
+                        TypeConverter::class.asClassName()
+                    ).build()
+                )
                 returns(to)
-                addParameter(ParameterSpec.builder(from, "input").build())
+                addParameter(from, "input")
                 if (to.isPrimitive) {
                     addStatement("return 0")
                 } else {
                     addStatement("return null")
                 }
             }.build()
-            addMethod(buildMethod("convertF"))
+            addFunction(buildMethod("convertF"))
             if (duplicate) {
-                addMethod(buildMethod("convertF2"))
+                addFunction(buildMethod("convertF2"))
             }
-        }.build().toString()
+        }.apply(
+            javaTypeBuilder = {
+                addTypeVariables(typeVariables)
+            },
+            kotlinTypeBuilder = { error("Test converter shouldn't be generated in Kotlin") }
+        ).build().toString()
         return Source.java(
-            CONVERTER.toString(),
-            "package ${CONVERTER.packageName()};\n$code"
+            CONVERTER.canonicalName,
+        "package ${CONVERTER.packageName};\n$code"
         )
     }
 
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 e9d5134..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
@@ -17,7 +17,6 @@
 package androidx.room.processor
 
 import COMMON
-import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.isTypeElement
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.XTestInvocation
@@ -25,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
@@ -225,7 +226,7 @@
             """
         ) { dao, invocation ->
             val dbType = invocation.context.processingEnv
-                .requireType(ROOM_DB.toJavaPoet())
+                .requireType(ROOM_DB)
             val daoProcessor =
                 DaoProcessor(invocation.context, dao.element, dbType, null)
 
@@ -273,7 +274,7 @@
             if (!dao.isTypeElement()) {
                 error("Expected DAO to be a type")
             }
-            val dbType = invocation.context.processingEnv.requireType(ROOM_DB.toJavaPoet())
+            val dbType = invocation.context.processingEnv.requireType(ROOM_DB)
             val daoProcessor =
                 DaoProcessor(invocation.context, dao, dbType, null)
             Truth.assertThat(daoProcessor.context.logger.suppressedWarnings)
@@ -294,7 +295,7 @@
             """
         ) { dao, invocation ->
             val dbType = invocation.context.processingEnv
-                .requireType(ROOM_DB.toJavaPoet())
+                .requireType(ROOM_DB)
             val daoProcessor =
                 DaoProcessor(invocation.context, dao.element, dbType, null)
             assertThat(
@@ -458,7 +459,7 @@
         """.trimIndent())
         runProcessorTest(sources = listOf(source)) { invocation ->
             val dao = invocation.processingEnv.requireTypeElement("MyDao")
-            val dbType = invocation.context.processingEnv.requireType(ROOM_DB.toJavaPoet())
+            val dbType = invocation.context.processingEnv.requireType(ROOM_DB)
             DaoProcessor(
                 baseContext = invocation.context,
                 element = dao,
@@ -511,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(),
@@ -538,7 +778,7 @@
                 null
             }
             val dbType = invocation.context.processingEnv
-                .requireType(ROOM_DB.toJavaPoet())
+                .requireType(ROOM_DB)
             val parser = DaoProcessor(
                 invocation.context,
                 dao, dbType, dbVerifier
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/DeleteOrUpdateShortcutMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/DeleteOrUpdateShortcutMethodProcessorTest.kt
index 4bb5f54..32f453f 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/DeleteOrUpdateShortcutMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/DeleteOrUpdateShortcutMethodProcessorTest.kt
@@ -118,6 +118,20 @@
     }
 
     @Test
+    fun singleNullableParamError() {
+        singleShortcutMethodKotlin(
+            """
+                @${annotation.java.canonicalName}
+                abstract fun foo(user: User?)
+                """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(ProcessorErrors.nullableParamInShortcutMethod("foo.bar.User"))
+            }
+        }
+    }
+
+    @Test
     fun notAnEntity() {
         singleShortcutMethod(
             """
@@ -168,6 +182,21 @@
     }
 
     @Test
+    fun twoNullableParamError() {
+        singleShortcutMethodKotlin(
+            """
+                @${annotation.java.canonicalName}
+                abstract fun foo(user1: User?, user2: User?)
+                """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(ProcessorErrors.nullableParamInShortcutMethod("foo.bar.User"))
+                hasErrorCount(2)
+            }
+        }
+    }
+
+    @Test
     fun list() {
         listOf(
             "int",
@@ -206,6 +235,24 @@
     }
 
     @Test
+    fun nullableListParamError() {
+        singleShortcutMethodKotlin(
+            """
+                @${annotation.java.canonicalName}
+                abstract fun foo(users: List<User?>)
+                """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.nullableParamInShortcutMethod(
+                        "java.util.List<? extends foo.bar.User>"
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
     fun array() {
         singleShortcutMethod(
             """
@@ -231,6 +278,22 @@
     }
 
     @Test
+    fun nullableArrayParamError() {
+        singleShortcutMethodKotlin(
+            """
+                @${annotation.java.canonicalName}
+                abstract fun foo(users: Array<User?>)
+                """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.nullableParamInShortcutMethod("foo.bar.User[]")
+                )
+            }
+        }
+    }
+
+    @Test
     fun set() {
         singleShortcutMethod(
             """
@@ -256,6 +319,24 @@
     }
 
     @Test
+    fun nullableSetParamError() {
+        singleShortcutMethodKotlin(
+            """
+                @${annotation.java.canonicalName}
+                abstract fun foo(users: Set<User?>)
+                """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.nullableParamInShortcutMethod(
+                        "java.util.Set<? extends foo.bar.User>"
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
     fun iterable() {
         singleShortcutMethod(
             """
@@ -309,6 +390,25 @@
     }
 
     @Test
+    fun nullableCustomCollectionParamError() {
+        singleShortcutMethodKotlin(
+            """
+                class MyList<Irrelevant, Item> : ArrayList<Item> {}
+                @${annotation.java.canonicalName}
+                abstract fun foo(users: MyList<String?, User?>)
+                """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.nullableParamInShortcutMethod(
+                        "foo.bar.MyClass.MyList<java.lang.String, foo.bar.User>"
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
     fun differentTypes() {
         listOf(
             "void",
@@ -352,6 +452,22 @@
     }
 
     @Test
+    fun twoNullableDifferentParamError() {
+        singleShortcutMethodKotlin(
+            """
+                @${annotation.java.canonicalName}
+                abstract fun foo(user1: User?, book1: Book?)
+                """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(ProcessorErrors.nullableParamInShortcutMethod("foo.bar.User"))
+                hasErrorContaining(ProcessorErrors.nullableParamInShortcutMethod("foo.bar.Book"))
+                hasErrorCount(2)
+            }
+        }
+    }
+
+    @Test
     fun invalidReturnType() {
         listOf(
             "long",
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt
index 093dcc2..170837e 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt
@@ -17,13 +17,13 @@
 package androidx.room.processor
 
 import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XTypeName
 import androidx.room.parser.SQLTypeAffinity
 import androidx.room.vo.CallType
 import androidx.room.vo.Field
 import androidx.room.vo.FieldGetter
 import androidx.room.vo.FieldSetter
 import com.google.common.truth.Truth.assertThat
-import com.squareup.javapoet.TypeName
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -65,7 +65,7 @@
                 .isEqualTo("foo.bar.MyEntity")
             assertThat(entity.fields.size).isEqualTo(1)
             val field = entity.fields.first()
-            val intType = invocation.processingEnv.requireType(TypeName.INT)
+            val intType = invocation.processingEnv.requireType(XTypeName.PRIMITIVE_INT)
             assertThat(field).isEqualTo(
                     Field(
                         element = field.element,
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts3TableEntityProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts3TableEntityProcessorTest.kt
index 5fe3711..ec64f94 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts3TableEntityProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts3TableEntityProcessorTest.kt
@@ -34,6 +34,7 @@
 
 import androidx.room.FtsOptions
 import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XTypeName
 import androidx.room.parser.FtsVersion
 import androidx.room.parser.SQLTypeAffinity
 import androidx.room.vo.CallType
@@ -41,7 +42,6 @@
 import androidx.room.vo.FieldGetter
 import androidx.room.vo.FieldSetter
 import androidx.room.vo.Fields
-import com.squareup.javapoet.TypeName
 import org.hamcrest.CoreMatchers.`is`
 import org.hamcrest.MatcherAssert.assertThat
 import org.junit.Test
@@ -68,7 +68,7 @@
                 `is`("foo.bar.MyEntity"))
             assertThat(entity.fields.size, `is`(1))
             val field = entity.fields.first()
-            val intType = invocation.processingEnv.requireType(TypeName.INT)
+            val intType = invocation.processingEnv.requireType(XTypeName.PRIMITIVE_INT)
             assertThat(
                 field,
                 `is`(
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt
index 9b1f407..fef806c 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.room.FtsOptions
 import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.runProcessorTest
 import androidx.room.parser.FtsVersion
@@ -28,7 +29,6 @@
 import androidx.room.vo.FieldGetter
 import androidx.room.vo.FieldSetter
 import androidx.room.vo.Fields
-import com.squareup.javapoet.TypeName
 import org.hamcrest.CoreMatchers.`is`
 import org.hamcrest.MatcherAssert.assertThat
 import org.junit.Test
@@ -55,7 +55,7 @@
                 `is`("foo.bar.MyEntity"))
             assertThat(entity.fields.size, `is`(1))
             val field = entity.fields.first()
-            val intType = invocation.processingEnv.requireType(TypeName.INT)
+            val intType = invocation.processingEnv.requireType(XTypeName.PRIMITIVE_INT)
             assertThat(
                 field,
                 `is`(
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/InsertOrUpsertShortcutMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/InsertOrUpsertShortcutMethodProcessorTest.kt
index 9906e42..bce40fb 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/InsertOrUpsertShortcutMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/InsertOrUpsertShortcutMethodProcessorTest.kt
@@ -23,7 +23,6 @@
 import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.codegen.asClassName
 import androidx.room.compiler.codegen.asMutableClassName
-import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XMethodElement
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeElement
@@ -146,6 +145,21 @@
                 .isEqualTo(XTypeName.PRIMITIVE_LONG)
         }
     }
+
+    @Test
+    fun singleNullableParamError() {
+        singleInsertUpsertShortcutMethodKotlin(
+            """
+                @${annotation.java.canonicalName}
+                abstract fun foo(user: User?)
+                """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(ProcessorErrors.nullableParamInShortcutMethod("foo.bar.User"))
+            }
+        }
+    }
+
     @Test
     fun two() {
         singleInsertUpsertShortcutMethod(
@@ -179,6 +193,21 @@
     }
 
     @Test
+    fun twoNullableParamError() {
+        singleInsertUpsertShortcutMethodKotlin(
+            """
+                @${annotation.java.canonicalName}
+                abstract fun foo(user1: User?, user2: User?)
+                """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(ProcessorErrors.nullableParamInShortcutMethod("foo.bar.User"))
+                hasErrorCount(2)
+            }
+        }
+    }
+
+    @Test
     fun list() {
         singleInsertUpsertShortcutMethod(
             """
@@ -210,6 +239,24 @@
     }
 
     @Test
+    fun nullableListParamError() {
+        singleInsertUpsertShortcutMethodKotlin(
+            """
+                @${annotation.java.canonicalName}
+                abstract fun foo(users: List<User?>)
+                """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.nullableParamInShortcutMethod(
+                        "java.util.List<? extends foo.bar.User>"
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
     fun array() {
         singleInsertUpsertShortcutMethod(
             """
@@ -234,6 +281,20 @@
     }
 
     @Test
+    fun nullableArrayParamError() {
+        singleInsertUpsertShortcutMethodKotlin(
+            """
+                @${annotation.java.canonicalName}
+                abstract fun foo(users: Array<User?>)
+                """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(ProcessorErrors.nullableParamInShortcutMethod("foo.bar.User[]"))
+            }
+        }
+    }
+
+    @Test
     fun set() {
         singleInsertUpsertShortcutMethod(
             """
@@ -259,6 +320,24 @@
     }
 
     @Test
+    fun nullableSetParamError() {
+        singleInsertUpsertShortcutMethodKotlin(
+            """
+                @${annotation.java.canonicalName}
+                abstract fun foo(users: Set<User?>)
+                """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.nullableParamInShortcutMethod(
+                        "java.util.Set<? extends foo.bar.User>"
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
     fun queue() {
         singleInsertUpsertShortcutMethod(
             """
@@ -337,6 +416,25 @@
     }
 
     @Test
+    fun nullableCustomCollectionParamError() {
+        singleInsertUpsertShortcutMethodKotlin(
+            """
+                class MyList<Irrelevant, Item> : ArrayList<Item> {}
+                @${annotation.java.canonicalName}
+                abstract fun foo(users: MyList<String?, User?>)
+                """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.nullableParamInShortcutMethod(
+                        "foo.bar.MyClass.MyList<java.lang.String, foo.bar.User>"
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
     fun differentTypes() {
         singleInsertUpsertShortcutMethod(
             """
@@ -368,6 +466,22 @@
     }
 
     @Test
+    fun twoNullableDifferentParamError() {
+        singleInsertUpsertShortcutMethodKotlin(
+            """
+                @${annotation.java.canonicalName}
+                abstract fun foo(user1: User?, book1: Book?)
+                """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(ProcessorErrors.nullableParamInShortcutMethod("foo.bar.User"))
+                hasErrorContaining(ProcessorErrors.nullableParamInShortcutMethod("foo.bar.Book"))
+                hasErrorCount(2)
+            }
+        }
+    }
+
+    @Test
     fun invalidReturnType() {
         listOf(
             "int",
@@ -647,7 +761,7 @@
             invocation.assertCompilationResult {
                 hasErrorContaining(
                     ProcessorErrors.missingRequiredColumnsInPartialEntity(
-                        partialEntityName = USERNAME_TYPE_NAME.toJavaPoet().toString(),
+                        partialEntityName = USERNAME_TYPE_NAME.toString(CodeLanguage.JAVA),
                         missingColumnNames = listOf("ageColumn")
                     )
                 )
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTargetMethodTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTargetMethodTest.kt
index d867db1..0a35d47 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTargetMethodTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTargetMethodTest.kt
@@ -16,11 +16,11 @@
 
 package androidx.room.processor
 
+import androidx.room.compiler.codegen.XClassName
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.XTestInvocation
 import androidx.room.compiler.processing.util.runProcessorTest
 import androidx.room.testing.context
-import com.squareup.javapoet.ClassName
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -29,8 +29,8 @@
 class PojoProcessorTargetMethodTest {
 
     companion object {
-        val MY_POJO: ClassName = ClassName.get("foo.bar", "MyPojo")
-        val AUTOVALUE_MY_POJO: ClassName = ClassName.get("foo.bar", "AutoValue_MyPojo")
+        val MY_POJO = XClassName.get("foo.bar", "MyPojo")
+        val AUTOVALUE_MY_POJO = XClassName.get("foo.bar", "AutoValue_MyPojo")
         const val HEADER = """
             package foo.bar;
 
@@ -56,7 +56,7 @@
     @Test
     fun invalidAnnotationInMethod() {
         val source = Source.java(
-            MY_POJO.toString(),
+            MY_POJO.canonicalName,
             """
             package foo.bar;
 
@@ -83,7 +83,7 @@
     @Test
     fun invalidAnnotationInStaticMethod() {
         val source = Source.java(
-            MY_POJO.toString(),
+            MY_POJO.canonicalName,
             """
             package foo.bar;
 
@@ -110,7 +110,7 @@
     @Test
     fun invalidAnnotationInAbstractMethod() {
         val source = Source.java(
-            MY_POJO.toString(),
+            MY_POJO.canonicalName,
             """
             package foo.bar;
 
@@ -222,7 +222,7 @@
     @Test
     fun validAnnotationInField() {
         val source = Source.java(
-            MY_POJO.toString(),
+            MY_POJO.canonicalName,
             """
             package foo.bar;
 
@@ -240,7 +240,7 @@
     @Test
     fun validAnnotationInStaticField() {
         val source = Source.java(
-            MY_POJO.toString(),
+            MY_POJO.canonicalName,
             """
             package foo.bar;
 
@@ -493,8 +493,8 @@
         vararg sources: Source,
         handler: ((XTestInvocation) -> Unit)? = null
     ) {
-        val pojoSource = Source.java(MY_POJO.toString(), pojoCode)
-        val autoValuePojoSource = Source.java(AUTOVALUE_MY_POJO.toString(), autoValuePojoCode)
+        val pojoSource = Source.java(MY_POJO.canonicalName, pojoCode)
+        val autoValuePojoSource = Source.java(AUTOVALUE_MY_POJO.canonicalName, autoValuePojoCode)
         val all = sources.toList() + pojoSource + autoValuePojoSource
         return runProcessorTest(
             sources = all
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
index 3dae9d4..7623237 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
@@ -18,7 +18,7 @@
 
 import COMMON
 import androidx.room.Embedded
-import androidx.room.compiler.codegen.toJavaPoet
+import androidx.room.compiler.codegen.XClassName
 import androidx.room.compiler.processing.XFieldElement
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.XTestInvocation
@@ -42,8 +42,6 @@
 import androidx.room.vo.Pojo
 import androidx.room.vo.RelationCollector
 import com.google.common.truth.Truth
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.TypeName
 import java.io.File
 import org.hamcrest.CoreMatchers.instanceOf
 import org.hamcrest.CoreMatchers.`is`
@@ -65,7 +63,7 @@
 class PojoProcessorTest {
 
     companion object {
-        val MY_POJO: ClassName = ClassName.get("foo.bar", "MyPojo")
+        val MY_POJO = XClassName.get("foo.bar", "MyPojo")
         val HEADER = """
             package foo.bar;
             import androidx.room.*;
@@ -89,11 +87,11 @@
         runProcessorTest(
             sources = listOf(
                 Source.java(
-                    MY_POJO.toString(),
+                    MY_POJO.canonicalName,
                     """
                     package foo.bar;
                     import androidx.room.*;
-                    public class ${MY_POJO.simpleName()} extends foo.bar.x.BaseClass {
+                    public class ${MY_POJO.simpleNames.single()} extends foo.bar.x.BaseClass {
                         public String myField;
                     }
                     """
@@ -213,8 +211,8 @@
             assertThat(parent.prefix, `is`(""))
             assertThat(parent.field.name, `is`("myPoint"))
             assertThat(
-                parent.pojo.typeName.toJavaPoet(),
-                `is`(ClassName.get("foo.bar.MyPojo", "Point") as TypeName)
+                parent.pojo.typeName,
+                `is`(XClassName.get("foo.bar.MyPojo", "Point"))
             )
         }
     }
@@ -316,8 +314,8 @@
             )
             val pointField = pojo.embeddedFields.first { it.field.name == "genericField" }
             assertThat(
-                pointField.pojo.typeName.toJavaPoet(),
-                `is`(ClassName.get("foo.bar", "Point") as TypeName)
+                pointField.pojo.typeName,
+                `is`(XClassName.get("foo.bar", "Point"))
             )
         }
     }
@@ -1061,7 +1059,7 @@
     @Test
     fun cache() {
         val pojo = Source.java(
-            MY_POJO.toString(),
+            MY_POJO.canonicalName,
             """
             $HEADER
             int id;
@@ -1194,7 +1192,7 @@
             invocation.assertCompilationResult {
                 hasErrorContaining(
                     ProcessorErrors.ambiguousConstructor(
-                        MY_POJO.toString(),
+                        MY_POJO.canonicalName,
                         "name", listOf("mName", "_name")
                     )
                 )
@@ -1722,12 +1720,12 @@
     @Test
     fun ignoredColumns() {
         val source = Source.java(
-            MY_POJO.toString(),
+            MY_POJO.canonicalName,
             """
             package foo.bar;
             import androidx.room.*;
             @Entity(ignoredColumns = {"bar"})
-            public class ${MY_POJO.simpleName()} {
+            public class ${MY_POJO.simpleNames.single()} {
                 public String foo;
                 public String bar;
             }
@@ -1752,15 +1750,15 @@
         runProcessorTest(
             listOf(
                 Source.java(
-                    MY_POJO.toString(),
+                    MY_POJO.canonicalName,
                     """
                     package foo.bar;
                     import androidx.room.*;
                     @Entity(ignoredColumns = {"bar"})
-                    public class ${MY_POJO.simpleName()} {
+                    public class ${MY_POJO.simpleNames.single()} {
                         private final String foo;
                         private final String bar;
-                        public ${MY_POJO.simpleName()}(String foo) {
+                        public ${MY_POJO.simpleNames.single()}(String foo) {
                           this.foo = foo;
                           this.bar = null;
                         }
@@ -1789,12 +1787,12 @@
         runProcessorTest(
             listOf(
                 Source.java(
-                    MY_POJO.toString(),
+                    MY_POJO.canonicalName,
                     """
                     package foo.bar;
                     import androidx.room.*;
                     @Entity(ignoredColumns = {"bar"})
-                    public class ${MY_POJO.simpleName()} {
+                    public class ${MY_POJO.simpleNames.single()} {
                         private String foo;
                         private String bar;
                         public String getFoo() {
@@ -1824,12 +1822,12 @@
         runProcessorTest(
             listOf(
                 Source.java(
-                    MY_POJO.toString(),
+                    MY_POJO.canonicalName,
                     """
                     package foo.bar;
                     import androidx.room.*;
                     @Entity(ignoredColumns = {"my_bar"})
-                    public class ${MY_POJO.simpleName()} {
+                    public class ${MY_POJO.simpleNames.single()} {
                         public String foo;
                         @ColumnInfo(name = "my_bar")
                         public String bar;
@@ -1854,12 +1852,12 @@
         runProcessorTest(
             listOf(
                 Source.java(
-                    MY_POJO.toString(),
+                    MY_POJO.canonicalName,
                     """
                     package foo.bar;
                     import androidx.room.*;
                     @Entity(ignoredColumns = {"no_such_column"})
-                    public class ${MY_POJO.simpleName()} {
+                    public class ${MY_POJO.simpleNames.single()} {
                         public String foo;
                         public String bar;
                     }
@@ -1888,11 +1886,11 @@
         runProcessorTest(
             listOf(
                 Source.java(
-                    MY_POJO.toString(),
+                    MY_POJO.canonicalName,
                     """
                     package foo.bar;
                     import androidx.room.*;
-                    public class ${MY_POJO.simpleName()} {
+                    public class ${MY_POJO.simpleNames.single()} {
                         private String foo;
                         private String bar;
                         public String getFoo() { return foo; }
@@ -1916,11 +1914,11 @@
         runProcessorTest(
             listOf(
                 Source.java(
-                    MY_POJO.toString(),
+                    MY_POJO.canonicalName,
                     """
                     package foo.bar;
                     import androidx.room.*;
-                    public class ${MY_POJO.simpleName()} {
+                    public class ${MY_POJO.simpleNames.single()} {
                         private String foo;
                         private String bar;
                         public String getFoo() { return foo; }
@@ -1949,11 +1947,11 @@
         runProcessorTest(
             listOf(
                 Source.java(
-                    MY_POJO.toString(),
+                    MY_POJO.canonicalName,
                     """
                     package foo.bar;
                     import androidx.room.*;
-                    public class ${MY_POJO.simpleName()} {
+                    public class ${MY_POJO.simpleNames.single()} {
                         private String foo;
                         private String bar;
                         public String getFoo() { return foo; }
@@ -1982,11 +1980,11 @@
         runProcessorTest(
             listOf(
                 Source.java(
-                    MY_POJO.toString(),
+                    MY_POJO.canonicalName,
                     """
                     package foo.bar;
                     import androidx.room.*;
-                    public class ${MY_POJO.simpleName()} {
+                    public class ${MY_POJO.simpleNames.single()} {
                         private String foo;
                         private String bar;
                         public void setFoo(String foo) { this.foo = foo; }
@@ -2013,11 +2011,11 @@
         runProcessorTest(
             listOf(
                 Source.java(
-                    MY_POJO.toString(),
+                    MY_POJO.canonicalName,
                     """
                     package foo.bar;
                     import androidx.room.*;
-                    public class ${MY_POJO.simpleName()} {
+                    public class ${MY_POJO.simpleNames.single()} {
                         private String foo;
                         private String bar;
                         public void setFoo(String foo) { this.foo = foo; }
@@ -2044,11 +2042,11 @@
         runProcessorTest(
             listOf(
                 Source.java(
-                    MY_POJO.toString(),
+                    MY_POJO.canonicalName,
                     """
                     package foo.bar;
                     import androidx.room.*;
-                    public class ${MY_POJO.simpleName()} {
+                    public class ${MY_POJO.simpleNames.single()} {
                         private String foo;
                         private String bar;
                         public void setFoo(String foo) { this.foo = foo; }
@@ -2188,7 +2186,7 @@
         classpath: List<File> = emptyList(),
         handler: (Pojo, XTestInvocation) -> Unit
     ) {
-        val pojoSource = Source.java(MY_POJO.toString(), code)
+        val pojoSource = Source.java(MY_POJO.canonicalName, code)
         val all = sources.toList() + pojoSource
         runProcessorTest(
             sources = all,
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
index 9a18e76..768c1a1 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
@@ -23,7 +23,6 @@
 import androidx.room.compiler.codegen.XClassName
 import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.codegen.asClassName
-import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeElement
 import androidx.room.compiler.processing.util.Source
@@ -31,6 +30,7 @@
 import androidx.room.compiler.processing.util.runProcessorTest
 import androidx.room.ext.CommonTypeNames
 import androidx.room.ext.CommonTypeNames.LIST
+import androidx.room.ext.CommonTypeNames.MUTABLE_LIST
 import androidx.room.ext.CommonTypeNames.STRING
 import androidx.room.ext.GuavaUtilConcurrentTypeNames
 import androidx.room.ext.KotlinTypeNames
@@ -59,10 +59,6 @@
 import androidx.room.vo.Warning
 import androidx.room.vo.WriteQueryMethod
 import com.google.common.truth.Truth.assertThat
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeVariableName
 import createVerifierFromEntitiesAndViews
 import mockElementAndType
 import org.hamcrest.CoreMatchers.hasItem
@@ -78,7 +74,6 @@
 import org.junit.runners.Parameterized
 import org.mockito.Mockito
 
-@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
 @RunWith(Parameterized::class)
 class QueryMethodProcessorTest(private val enableVerification: Boolean) {
     companion object {
@@ -156,7 +151,7 @@
             assertThat(param.sqlName, `is`("x"))
             assertThat(
                 param.type,
-                `is`(invocation.processingEnv.requireType(TypeName.INT))
+                `is`(invocation.processingEnv.requireType(XTypeName.PRIMITIVE_INT))
             )
         }
     }
@@ -340,11 +335,10 @@
                 abstract public <T> ${LIST.canonicalName}<T> foo(int x);
                 """
         ) { parsedQuery, invocation ->
-            val expected: TypeName = ParameterizedTypeName.get(
-                ClassName.get(List::class.java),
-                TypeVariableName.get("T")
+            val expected = MUTABLE_LIST.parametrizedBy(
+                XClassName.get("", "T")
             )
-            assertThat(parsedQuery.returnType.typeName, `is`(expected))
+            assertThat(parsedQuery.returnType.asTypeName(), `is`(expected))
             invocation.assertCompilationResult {
                 hasErrorContaining(
                     ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_QUERY_METHODS
@@ -374,7 +368,7 @@
             """
                 @Query("WITH RECURSIVE tempTable(n, fact) AS (SELECT 0, 1 UNION ALL SELECT n+1,"
                 + " (n+1)*fact FROM tempTable WHERE n < 9) SELECT fact FROM tempTable, User")
-                abstract public ${LifecyclesTypeNames.LIVE_DATA.canonicalName}<${LIST.toJavaPoet()}<Integer>>
+                abstract public ${LifecyclesTypeNames.LIVE_DATA.canonicalName}<${LIST.canonicalName}<Integer>>
                 getFactorialLiveData();
                 """
         ) { parsedQuery, _ ->
@@ -409,7 +403,7 @@
             """
                 @Query("WITH RECURSIVE tempTable(n, fact) AS (SELECT 0, 1 UNION ALL SELECT n+1,"
                 + " (n+1)*fact FROM tempTable WHERE n < 9) SELECT fact FROM tempTable")
-                abstract public ${LifecyclesTypeNames.LIVE_DATA.canonicalName}<${LIST.toJavaPoet()}<Integer>>
+                abstract public ${LifecyclesTypeNames.LIVE_DATA.canonicalName}<${LIST.canonicalName}<Integer>>
                 getFactorialLiveData();
                 """
         ) { _, invocation ->
@@ -521,7 +515,7 @@
             assertThat(parsedQuery.returnType.asTypeName(), `is`(XTypeName.UNIT_VOID))
             assertThat(
                 parsedQuery.parameters.first().type.asTypeName(),
-                `is`(CommonTypeNames.STRING)
+                `is`(STRING)
             )
         }
     }
@@ -537,10 +531,7 @@
             assertThat(parsedQuery.element.jvmName, `is`("insertUsername"))
             assertThat(parsedQuery.parameters.size, `is`(1))
             assertThat(parsedQuery.returnType.asTypeName(), `is`(XTypeName.UNIT_VOID))
-            assertThat(
-                parsedQuery.parameters.first().type.asTypeName(),
-                `is`(CommonTypeNames.STRING)
-            )
+            assertThat(parsedQuery.parameters.first().type.asTypeName(), `is`(STRING))
         }
     }
 
@@ -555,10 +546,7 @@
             assertThat(parsedQuery.element.jvmName, `is`("insertUsername"))
             assertThat(parsedQuery.parameters.size, `is`(1))
             assertThat(parsedQuery.returnType.asTypeName(), `is`(XTypeName.PRIMITIVE_LONG))
-            assertThat(
-                parsedQuery.parameters.first().type.asTypeName(),
-                `is`(CommonTypeNames.STRING)
-            )
+            assertThat(parsedQuery.parameters.first().type.asTypeName(), `is`(STRING))
         }
     }
 
@@ -813,10 +801,9 @@
                 `is`(COMMON.NOT_AN_ENTITY_TYPE_NAME)
             )
             val adapter = parsedQuery.queryResultBinder.adapter
-            assertThat(checkNotNull(adapter))
+            checkNotNull(adapter)
             assertThat(adapter::class, `is`(SingleItemQueryResultAdapter::class))
             val rowAdapter = adapter.rowAdapters.single()
-            assertThat(checkNotNull(rowAdapter))
             assertThat(rowAdapter::class, `is`(PojoRowAdapter::class))
         }
     }
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
index 9e64549..d6d58c9 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
@@ -153,7 +153,7 @@
         singleQueryMethod(
             """
                 @RawQuery
-                abstract public ${PagingTypeNames.POSITIONAL_DATA_SOURCE}<User> getOne();
+                abstract public ${PagingTypeNames.POSITIONAL_DATA_SOURCE.canonicalName}<User> getOne();
                 """
         ) { _, invocation ->
             invocation.assertCompilationResult {
@@ -169,7 +169,7 @@
         singleQueryMethod(
             """
                 @RawQuery(observedEntities = {User.class})
-                abstract public ${PagingTypeNames.POSITIONAL_DATA_SOURCE}<User> getOne(
+                abstract public ${PagingTypeNames.POSITIONAL_DATA_SOURCE.canonicalName}<User> getOne(
                         SupportSQLiteQuery query);
                 """
         ) { _, _ ->
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt
index 37534d1..a7291ea 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt
@@ -19,7 +19,7 @@
 import COMMON
 import androidx.room.compiler.codegen.CodeLanguage
 import androidx.room.compiler.codegen.XTypeName
-import androidx.room.compiler.codegen.toJavaPoet
+import androidx.room.compiler.codegen.XTypeName.Companion.PRIMITIVE_LONG
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.compileFiles
 import androidx.room.compiler.processing.util.runProcessorTest
@@ -35,7 +35,6 @@
 import androidx.room.vo.Pojo
 import androidx.room.vo.columnNames
 import com.google.common.truth.Truth.assertThat
-import com.squareup.javapoet.TypeName
 import org.hamcrest.CoreMatchers.hasItems
 import org.hamcrest.CoreMatchers.`is`
 import org.hamcrest.MatcherAssert.assertThat
@@ -63,7 +62,7 @@
             )
             assertThat(entity.fields.size, `is`(1))
             val field = entity.fields.first()
-            val intType = invocation.processingEnv.requireType(TypeName.INT)
+            val intType = invocation.processingEnv.requireType(XTypeName.PRIMITIVE_INT)
             assertThat(
                 field,
                 `is`(
@@ -123,9 +122,8 @@
             classpathFiles = libraryClasspath
         ) { _, invocation ->
             invocation.assertCompilationResult {
-                hasError(
-                    ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD +
-                        " - id in test.library.MissingGetterEntity"
+                hasErrorContaining(
+                    ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD
                 )
             }
         }
@@ -2606,7 +2604,7 @@
             )
             val parsed = parser.process()
             val field = parsed.primaryKey.fields.first()
-            assertThat(field.typeName.toJavaPoet()).isEqualTo(TypeName.LONG)
+            assertThat(field.typeName).isEqualTo(PRIMITIVE_LONG)
         }
     }
 }
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/BasicColumnTypeAdaptersTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/BasicColumnTypeAdaptersTest.kt
index 7ef769c..e582e54 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/BasicColumnTypeAdaptersTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/BasicColumnTypeAdaptersTest.kt
@@ -16,18 +16,21 @@
 
 package androidx.room.solver
 
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.VisibilityModifier
+import androidx.room.compiler.codegen.XClassName
+import androidx.room.compiler.codegen.XFunSpec
+import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.XTypeSpec
+import androidx.room.compiler.codegen.asClassName
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.util.XTestInvocation
 import androidx.room.compiler.processing.util.runProcessorTest
 import androidx.room.compiler.processing.writeTo
+import androidx.room.ext.AndroidTypeNames
 import androidx.room.processor.Context
 import androidx.room.vo.BuiltInConverterFlags
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.JavaFile
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
 import org.hamcrest.CoreMatchers.`is`
 import org.hamcrest.MatcherAssert.assertThat
 import org.junit.Test
@@ -37,60 +40,58 @@
 
 @RunWith(Parameterized::class)
 class BasicColumnTypeAdaptersTest(
-    val input: TypeName,
+    val input: XTypeName,
     val bindCode: String,
     val cursorCode: String
 ) {
     companion object {
-        val SQLITE_STMT: TypeName = ClassName.get("android.database.sqlite", "SQLiteStatement")
-        val CURSOR: TypeName = ClassName.get("android.database", "Cursor")
 
         @Parameterized.Parameters(name = "kind:{0},bind:_{1},cursor:_{2}")
         @JvmStatic
         fun params(): List<Array<Any>> {
             return listOf(
                 arrayOf(
-                    TypeName.INT,
+                    XTypeName.PRIMITIVE_INT,
                     "st.bindLong(6, inp);",
                     "out = crs.getInt(9);"
                 ),
                 arrayOf(
-                    TypeName.BYTE,
+                    XTypeName.PRIMITIVE_BYTE,
                     "st.bindLong(6, inp);",
                     "out = (byte) (crs.getShort(9));"
                 ),
                 arrayOf(
-                    TypeName.SHORT,
+                    XTypeName.PRIMITIVE_SHORT,
                     "st.bindLong(6, inp);",
                     "out = crs.getShort(9);"
                 ),
                 arrayOf(
-                    TypeName.LONG,
+                    XTypeName.PRIMITIVE_LONG,
                     "st.bindLong(6, inp);",
                     "out = crs.getLong(9);"
                 ),
                 arrayOf(
-                    TypeName.CHAR,
+                    XTypeName.PRIMITIVE_CHAR,
                     "st.bindLong(6, inp);",
                     "out = (char) (crs.getInt(9));"
                 ),
                 arrayOf(
-                    TypeName.FLOAT,
+                    XTypeName.PRIMITIVE_FLOAT,
                     "st.bindDouble(6, inp);",
                     "out = crs.getFloat(9);"
                 ),
                 arrayOf(
-                    TypeName.DOUBLE,
+                    XTypeName.PRIMITIVE_DOUBLE,
                     "st.bindDouble(6, inp);",
                     "out = crs.getDouble(9);"
                 ),
                 arrayOf(
-                    TypeName.get(String::class.java),
+                    String::class.asClassName(),
                     "st.bindString(6, inp);",
                     "out = crs.getString(9);"
                 ),
                 arrayOf(
-                    TypeName.get(ByteArray::class.java),
+                    XTypeName.getArrayName(XTypeName.PRIMITIVE_BYTE),
                     "st.bindBlob(6, inp);",
                     "out = crs.getBlob(9);"
                 )
@@ -124,7 +125,7 @@
                 """.trimIndent()
             }
             adapter.bindToStmt("st", "6", "inp", scope)
-            assertThat(scope.builder().build().toString().trim(), `is`(expected))
+            assertThat(scope.generate().toString().trim(), `is`(expected))
             generateCode(invocation, scope, type)
         }
     }
@@ -155,7 +156,7 @@
                 """.trimIndent()
             }
             assertThat(
-                scope.builder().build().toString().trim(),
+                scope.generate().toString().trim(),
                 `is`(
                     expected
                 )
@@ -180,7 +181,7 @@
                 )!!
             adapter.bindToStmt("st", "6", "inp", scope)
             assertThat(
-                scope.builder().build().toString().trim(),
+                scope.generate().toString().trim(),
                 `is`(
                     """
                     if (inp == null) {
@@ -200,20 +201,52 @@
             // guard against multi round
             return
         }
-        val spec = TypeSpec.classBuilder("OutClass")
-            .addField(FieldSpec.builder(SQLITE_STMT, "st").build())
-            .addField(FieldSpec.builder(CURSOR, "crs").build())
-            .addField(FieldSpec.builder(type.typeName, "out").build())
-            .addField(FieldSpec.builder(type.typeName, "inp").build())
-            .addMethod(
-                MethodSpec.methodBuilder("foo")
-                    .addCode(scope.builder().build())
+        XTypeSpec.classBuilder(
+            language = CodeLanguage.JAVA,
+            className = XClassName.get("foo.bar", "OuterClass")
+        ).apply {
+            addProperty(
+                XPropertySpec.builder(
+                    language = CodeLanguage.JAVA,
+                    name = "st",
+                    typeName = XClassName.get("android.database.sqlite", "SQLiteStatement"),
+                    visibility = VisibilityModifier.PUBLIC,
+                    isMutable = true
+                ).build()
+            )
+            addProperty(
+                XPropertySpec.builder(
+                    language = CodeLanguage.JAVA,
+                    name = "crs",
+                    typeName = AndroidTypeNames.CURSOR,
+                    visibility = VisibilityModifier.PUBLIC,
+                    isMutable = true
+                ).build()
+            )
+            addProperty(
+                XPropertySpec.builder(
+                    language = CodeLanguage.JAVA,
+                    name = "out",
+                    typeName = type.asTypeName(),
+                    visibility = VisibilityModifier.PUBLIC,
+                    isMutable = true
+                ).build()
+            )
+            addProperty(
+                XPropertySpec.builder(
+                    language = CodeLanguage.JAVA,
+                    name = "inp",
+                    typeName = type.asTypeName(),
+                    visibility = VisibilityModifier.PUBLIC,
+                    isMutable = true
+                ).build()
+            )
+            addFunction(
+                XFunSpec.builder(CodeLanguage.JAVA, "foo", VisibilityModifier.PUBLIC)
+                    .addCode(scope.generate())
                     .build()
             )
-            .build()
-        JavaFile.builder("foo.bar", spec).build().writeTo(
-            invocation.processingEnv.filer
-        )
+        }.build().writeTo(invocation.processingEnv.filer)
     }
 
     @Test
@@ -241,7 +274,7 @@
                 """.trimIndent()
             }
             adapter.readFromCursor("out", "crs", "9", scope)
-            assertThat(scope.builder().build().toString().trim(), `is`(expected))
+            assertThat(scope.generate().toString().trim(), `is`(expected))
             generateCode(invocation, scope, type)
         }
     }
@@ -272,7 +305,7 @@
                 """.trimIndent()
             }
             assertThat(
-                scope.builder().build().toString().trim(),
+                scope.generate().toString().trim(),
                 `is`(
                     expected
                 )
@@ -292,7 +325,7 @@
             ).findColumnTypeAdapter(nullableType, null, false)!!
             adapter.readFromCursor("out", "crs", "9", scope)
             assertThat(
-                scope.builder().build().toString().trim(),
+                scope.generate().toString().trim(),
                 `is`(
                     """
                     if (crs.isNull(9)) {
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/CustomTypeConverterResolutionTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/CustomTypeConverterResolutionTest.kt
index 712dd2b..5033e65 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/CustomTypeConverterResolutionTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/CustomTypeConverterResolutionTest.kt
@@ -26,85 +26,82 @@
 import androidx.room.Query
 import androidx.room.TypeConverter
 import androidx.room.TypeConverters
-import androidx.room.compiler.codegen.toJavaPoet
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.VisibilityModifier
+import androidx.room.compiler.codegen.XAnnotationSpec
+import androidx.room.compiler.codegen.XClassName
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XFunSpec
+import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.XTypeSpec
+import androidx.room.compiler.codegen.asClassName
 import androidx.room.compiler.processing.util.CompilationResultSubject
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.runProcessorTest
+import androidx.room.ext.CommonTypeNames
 import androidx.room.ext.RoomTypeNames.ROOM_DB
-import androidx.room.ext.S
-import androidx.room.ext.T
 import androidx.room.processor.ProcessorErrors.CANNOT_BIND_QUERY_PARAMETER_INTO_STMT
-import com.squareup.javapoet.AnnotationSpec
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import javax.lang.model.element.Modifier
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
 class CustomTypeConverterResolutionTest {
-    fun TypeSpec.toSource(): Source {
+    fun XTypeSpec.toSource(): Source {
         return Source.java(
-            "foo.bar.${this.name}",
+            this.className.canonicalName,
             "package foo.bar;\n" + toString()
         )
     }
 
     companion object {
-        val ENTITY = ClassName.get("foo.bar", "MyEntity")
-        val DB = ClassName.get("foo.bar", "MyDb")
-        val DAO = ClassName.get("foo.bar", "MyDao")
+        val ENTITY = XClassName.get("foo.bar", "MyEntity")
+        val DB = XClassName.get("foo.bar", "MyDb")
+        val DAO = XClassName.get("foo.bar", "MyDao")
 
-        val CUSTOM_TYPE = ClassName.get("foo.bar", "CustomType")
+        val CUSTOM_TYPE = XClassName.get("foo.bar", "CustomType")
         val CUSTOM_TYPE_JFO = Source.java(
-            CUSTOM_TYPE.toString(),
+            CUSTOM_TYPE.canonicalName,
             """
-                package ${CUSTOM_TYPE.packageName()};
-                public class ${CUSTOM_TYPE.simpleName()} {
+                package ${CUSTOM_TYPE.packageName};
+                public class ${CUSTOM_TYPE.simpleNames.first()} {
                     public int value;
                 }
                 """
         )
-        val CUSTOM_TYPE_CONVERTER = ClassName.get("foo.bar", "MyConverter")
+        val CUSTOM_TYPE_CONVERTER = XClassName.get("foo.bar", "MyConverter")
         val CUSTOM_TYPE_CONVERTER_JFO = Source.java(
-            CUSTOM_TYPE_CONVERTER.toString(),
+            CUSTOM_TYPE_CONVERTER.canonicalName,
             """
-                package ${CUSTOM_TYPE_CONVERTER.packageName()};
-                public class ${CUSTOM_TYPE_CONVERTER.simpleName()} {
+                package ${CUSTOM_TYPE_CONVERTER.packageName};
+                public class ${CUSTOM_TYPE_CONVERTER.simpleNames.first()} {
                     @${TypeConverter::class.java.canonicalName}
-                    public static $CUSTOM_TYPE toCustom(int value) {
+                    public static ${CUSTOM_TYPE.canonicalName} toCustom(int value) {
                         return null;
                     }
                     @${TypeConverter::class.java.canonicalName}
-                    public static int fromCustom($CUSTOM_TYPE input) {
+                    public static int fromCustom(${CUSTOM_TYPE.canonicalName} input) {
                         return 0;
                     }
                 }
                 """
         )
-        val CUSTOM_TYPE_SET = ParameterizedTypeName.get(
-            ClassName.get(Set::class.java), CUSTOM_TYPE
-        )
-        val CUSTOM_TYPE_SET_CONVERTER = ClassName.get("foo.bar", "MySetConverter")
+        val CUSTOM_TYPE_SET = CommonTypeNames.SET.parametrizedBy(CUSTOM_TYPE)
+        val CUSTOM_TYPE_SET_CONVERTER = XClassName.get("foo.bar", "MySetConverter")
         val CUSTOM_TYPE_SET_CONVERTER_JFO = Source.java(
-            CUSTOM_TYPE_SET_CONVERTER.toString(),
+            CUSTOM_TYPE_SET_CONVERTER.canonicalName,
             """
-                package ${CUSTOM_TYPE_SET_CONVERTER.packageName()};
+                package ${CUSTOM_TYPE_SET_CONVERTER.packageName};
                 import java.util.HashSet;
                 import java.util.Set;
-                public class ${CUSTOM_TYPE_SET_CONVERTER.simpleName()} {
+                public class ${CUSTOM_TYPE_SET_CONVERTER.simpleNames.first()} {
                     @${TypeConverter::class.java.canonicalName}
-                    public static $CUSTOM_TYPE_SET toCustom(int value) {
+                    public static ${CUSTOM_TYPE_SET.toString(CodeLanguage.JAVA)} toCustom(int value) {
                         return null;
                     }
                     @${TypeConverter::class.java.canonicalName}
-                    public static int fromCustom($CUSTOM_TYPE_SET input) {
+                    public static int fromCustom(${CUSTOM_TYPE_SET.toString(CodeLanguage.JAVA)} input) {
                         return 0;
                     }
                 }
@@ -280,7 +277,7 @@
         hasConverters: Boolean = false,
         hasConverterOnField: Boolean = false,
         useCollection: Boolean = false
-    ): TypeSpec {
+    ): XTypeSpec {
         if (hasConverterOnField && hasConverters) {
             throw IllegalArgumentException("cannot have both converters")
         }
@@ -289,12 +286,20 @@
         } else {
             CUSTOM_TYPE
         }
-        return TypeSpec.classBuilder(ENTITY).apply {
-            addAnnotation(Entity::class.java)
-            addModifiers(Modifier.PUBLIC)
+        return XTypeSpec.classBuilder(CodeLanguage.JAVA, ENTITY).apply {
+            addAnnotation(
+                XAnnotationSpec.builder(CodeLanguage.JAVA, Entity::class.asClassName()).build()
+            )
+            setVisibility(VisibilityModifier.PUBLIC)
             if (hasCustomField) {
-                addField(
-                    FieldSpec.builder(type, "myCustomField", Modifier.PUBLIC).apply {
+                addProperty(
+                    XPropertySpec.builder(
+                        CodeLanguage.JAVA,
+                        "myCustomField",
+                        type,
+                        VisibilityModifier.PUBLIC,
+                        isMutable = true
+                    ).apply {
                         if (hasConverterOnField) {
                             addAnnotation(createConvertersAnnotation())
                         }
@@ -304,10 +309,19 @@
             if (hasConverters) {
                 addAnnotation(createConvertersAnnotation())
             }
-            addField(
-                FieldSpec.builder(TypeName.INT, "id", Modifier.PUBLIC).apply {
-                    addAnnotation(PrimaryKey::class.java)
-                }.build()
+            addProperty(
+                XPropertySpec.builder(
+                    language = CodeLanguage.JAVA,
+                    name = "id",
+                    typeName = XTypeName.PRIMITIVE_INT,
+                    visibility = VisibilityModifier.PUBLIC,
+                    isMutable = true
+                ).addAnnotation(
+                    XAnnotationSpec.builder(
+                        CodeLanguage.JAVA,
+                        PrimaryKey::class.asClassName()
+                    ).build()
+                ).build()
             )
         }.build()
     }
@@ -316,30 +330,67 @@
         hasConverters: Boolean = false,
         hasDao: Boolean = false,
         useCollection: Boolean = false
-    ): TypeSpec {
-        return TypeSpec.classBuilder(DB).apply {
-            addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
-            superclass(ROOM_DB.toJavaPoet())
+    ): XTypeSpec {
+        return XTypeSpec.classBuilder(CodeLanguage.JAVA, DB, isOpen = true).apply {
+            addAbstractModifier()
+            setVisibility(VisibilityModifier.PUBLIC)
+            superclass(ROOM_DB)
             if (hasConverters) {
                 addAnnotation(createConvertersAnnotation(useCollection = useCollection))
             }
-            addField(
-                FieldSpec.builder(TypeName.INT, "id", Modifier.PUBLIC).apply {
-                    addAnnotation(PrimaryKey::class.java)
-                }.build()
+            addProperty(
+                XPropertySpec.builder(
+                    language = CodeLanguage.JAVA,
+                    name = "id",
+                    typeName = XTypeName.PRIMITIVE_INT,
+                    visibility = VisibilityModifier.PUBLIC,
+                    isMutable = true
+                ).addAnnotation(
+                    XAnnotationSpec.builder(
+                        CodeLanguage.JAVA,
+                        PrimaryKey::class.asClassName()
+                    ).build()
+                ).build()
             )
             if (hasDao) {
-                addMethod(
-                    MethodSpec.methodBuilder("getDao").apply {
-                        addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+                addFunction(
+                    XFunSpec.builder(
+                        language = CodeLanguage.JAVA,
+                        "getDao",
+                        VisibilityModifier.PUBLIC
+                    ).apply {
+                        addAbstractModifier()
                         returns(DAO)
                     }.build()
                 )
             }
             addAnnotation(
-                AnnotationSpec.builder(Database::class.java).apply {
-                    addMember("entities", "{$T.class}", ENTITY)
-                    addMember("version", "42")
+                XAnnotationSpec.builder(
+                    CodeLanguage.JAVA,
+                    Database::class.asClassName()
+                ).apply {
+                    addMember(
+                        "entities",
+                        XCodeBlock.of(
+                            language,
+                            "{%T.class}",
+                            ENTITY
+                        )
+                    )
+                    addMember(
+                        "version",
+                        XCodeBlock.of(
+                            language,
+                            "42"
+                        )
+                    )
+                    addMember(
+                        "exportSchema",
+                        XCodeBlock.of(
+                            language,
+                            "false"
+                        )
+                    )
                 }.build()
             )
         }.build()
@@ -352,7 +403,7 @@
         hasMethodConverters: Boolean = false,
         hasParameterConverters: Boolean = false,
         useCollection: Boolean = false
-    ): TypeSpec {
+    ): XTypeSpec {
         val annotationCount = listOf(hasMethodConverters, hasConverters, hasParameterConverters)
             .map { if (it) 1 else 0 }.sum()
         if (annotationCount > 1) {
@@ -361,25 +412,39 @@
         if (hasParameterConverters && !hasQueryWithCustomParam) {
             throw IllegalArgumentException("inconsistent")
         }
-        return TypeSpec.classBuilder(DAO).apply {
-            addAnnotation(Dao::class.java)
-            addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
+        return XTypeSpec.classBuilder(
+            CodeLanguage.JAVA,
+            DAO,
+            isOpen = true
+        ).apply {
+            addAbstractModifier()
+            addAnnotation(XAnnotationSpec.builder(
+                CodeLanguage.JAVA,
+                Dao::class.asClassName()
+            ).build())
+            setVisibility(VisibilityModifier.PUBLIC)
             if (hasConverters) {
                 addAnnotation(createConvertersAnnotation(useCollection = useCollection))
             }
             if (hasQueryReturningEntity) {
-                addMethod(
-                    MethodSpec.methodBuilder("loadAll").apply {
-                        addAnnotation(
-                            AnnotationSpec.builder(Query::class.java).apply {
-                                addMember(
-                                    "value",
-                                    S,
-                                    "SELECT * FROM ${ENTITY.simpleName()} LIMIT 1"
-                                )
-                            }.build()
-                        )
-                        addModifiers(Modifier.ABSTRACT)
+                addFunction(
+                    XFunSpec.builder(
+                        CodeLanguage.JAVA,
+                        "loadAll",
+                        VisibilityModifier.PUBLIC
+                    ).apply {
+                        addAbstractModifier()
+                        addAnnotation(XAnnotationSpec.builder(
+                            CodeLanguage.JAVA,
+                            Query::class.asClassName()
+                        ).addMember(
+                            "value",
+                            XCodeBlock.of(
+                                CodeLanguage.JAVA,
+                                "%S",
+                                "SELECT * FROM ${ENTITY.simpleNames.first()} LIMIT 1"
+                            )
+                        ).build())
                         returns(ENTITY)
                     }.build()
                 )
@@ -390,44 +455,52 @@
                 CUSTOM_TYPE
             }
             if (hasQueryWithCustomParam) {
-                addMethod(
-                    MethodSpec.methodBuilder("queryWithCustom").apply {
-                        addAnnotation(
-                            AnnotationSpec.builder(Query::class.java).apply {
-                                addMember(
-                                    "value", S,
-                                    "SELECT COUNT(*) FROM ${ENTITY.simpleName()} where" +
-                                        " id = :custom"
-                                )
-                            }.build()
-                        )
+                addFunction(
+                    XFunSpec.builder(
+                        CodeLanguage.JAVA,
+                        "queryWithCustom",
+                        VisibilityModifier.PUBLIC
+                    ).apply {
+                        addAbstractModifier()
+                        addAnnotation(XAnnotationSpec.builder(
+                            CodeLanguage.JAVA,
+                            Query::class.asClassName()
+                        ).addMember(
+                            "value",
+                            XCodeBlock.of(
+                                CodeLanguage.JAVA,
+                                "%S",
+                                "SELECT COUNT(*) FROM ${ENTITY.simpleNames.first()} where" +
+                                    " id = :custom"
+                            )
+                        ).build())
                         if (hasMethodConverters) {
                             addAnnotation(createConvertersAnnotation(useCollection = useCollection))
                         }
                         addParameter(
-                            ParameterSpec.builder(customType, "custom").apply {
-                                if (hasParameterConverters) {
-                                    addAnnotation(
-                                        createConvertersAnnotation(useCollection = useCollection)
-                                    )
-                                }
-                            }.build()
-                        )
-                        addModifiers(Modifier.ABSTRACT)
-                        returns(TypeName.INT)
+                            customType,
+                            "custom"
+                        ).apply {
+                            if (hasParameterConverters) {
+                                addAnnotation(
+                                    createConvertersAnnotation(useCollection = useCollection)
+                                )
+                            }
+                        }.build()
+                        returns(XTypeName.PRIMITIVE_INT)
                     }.build()
                 )
             }
         }.build()
     }
 
-    private fun createConvertersAnnotation(useCollection: Boolean = false): AnnotationSpec {
+    private fun createConvertersAnnotation(useCollection: Boolean = false): XAnnotationSpec {
         val converter = if (useCollection) {
             CUSTOM_TYPE_SET_CONVERTER
         } else {
             CUSTOM_TYPE_CONVERTER
         }
-        return AnnotationSpec.builder(TypeConverters::class.java)
-            .addMember("value", "$T.class", converter).build()
+        return XAnnotationSpec.builder(CodeLanguage.JAVA, TypeConverters::class.asClassName())
+            .addMember("value", XCodeBlock.of(CodeLanguage.JAVA, "%T.class", converter)).build()
     }
 }
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
index 52fae72..84d9666 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
@@ -22,7 +22,7 @@
 import androidx.room.Dao
 import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.codegen.XTypeName
-import androidx.room.compiler.codegen.toJavaPoet
+import androidx.room.compiler.codegen.asClassName
 import androidx.room.compiler.processing.XProcessingEnv
 import androidx.room.compiler.processing.XRawType
 import androidx.room.compiler.processing.isTypeElement
@@ -39,7 +39,6 @@
 import androidx.room.ext.RxJava2TypeNames
 import androidx.room.ext.RxJava3TypeNames
 import androidx.room.ext.implementsEqualsAndHashcode
-import androidx.room.ext.typeName
 import androidx.room.parser.SQLTypeAffinity
 import androidx.room.processor.Context
 import androidx.room.processor.CustomConverterProcessor
@@ -75,7 +74,6 @@
 import androidx.room.vo.BuiltInConverterFlags
 import androidx.room.vo.ReadQueryMethod
 import com.google.common.truth.Truth.assertThat
-import com.squareup.javapoet.TypeName
 import java.util.UUID
 import org.hamcrest.CoreMatchers.instanceOf
 import org.hamcrest.CoreMatchers.`is`
@@ -87,7 +85,6 @@
 import org.junit.runners.JUnit4
 import testCodeGenScope
 
-@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
 @RunWith(JUnit4::class)
 class TypeAdapterStoreTest {
     companion object {
@@ -156,7 +153,7 @@
                 Context(invocation.processingEnv),
                 BuiltInConverterFlags.DEFAULT
             )
-            val primitiveType = invocation.processingEnv.requireType(TypeName.INT)
+            val primitiveType = invocation.processingEnv.requireType(XTypeName.PRIMITIVE_INT)
             val adapter = store.findColumnTypeAdapter(
                 primitiveType,
                 null,
@@ -251,7 +248,7 @@
             )
             val uuid = invocation
                 .processingEnv
-                .requireType(UUID::class.typeName)
+                .requireType(UUID::class.asClassName())
             val adapter = store.findColumnTypeAdapter(
                 out = uuid,
                 affinity = null,
@@ -270,7 +267,7 @@
                 Context(invocation.processingEnv),
                 BuiltInConverterFlags.DEFAULT
             )
-            val booleanType = invocation.processingEnv.requireType(TypeName.BOOLEAN)
+            val booleanType = invocation.processingEnv.requireType(XTypeName.PRIMITIVE_BOOLEAN)
             val adapter = store.findColumnTypeAdapter(
                 booleanType,
                 null,
@@ -281,7 +278,7 @@
             val bindScope = testCodeGenScope()
             adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope)
             assertThat(
-                bindScope.builder().build().toString().trim(),
+                bindScope.generate().toString().trim(),
                 `is`(
                     """
                     final int ${tmp(0)} = fooVar ? 1 : 0;
@@ -293,7 +290,7 @@
             val cursorScope = testCodeGenScope()
             adapter.readFromCursor("res", "curs", "7", cursorScope)
             assertThat(
-                cursorScope.builder().build().toString().trim(),
+                cursorScope.generate().toString().trim(),
                 `is`(
                     """
                     final int ${tmp(0)};
@@ -356,7 +353,7 @@
             val bindScope = testCodeGenScope()
             adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope)
             assertThat(
-                bindScope.builder().build().toString().trim(),
+                bindScope.generate().toString().trim(),
                 `is`(
                     """
                     final boolean ${tmp(0)} = foo.bar.Point.toBoolean(fooVar);
@@ -369,7 +366,7 @@
             val cursorScope = testCodeGenScope()
             adapter.readFromCursor("res", "curs", "11", cursorScope).toString()
             assertThat(
-                cursorScope.builder().build().toString().trim(),
+                cursorScope.generate().toString().trim(),
                 `is`(
                     """
                     final int ${tmp(0)};
@@ -397,7 +394,7 @@
             val bindScope = testCodeGenScope()
             adapter!!.readFromCursor("outDate", "curs", "0", bindScope)
             assertThat(
-                bindScope.builder().build().toString().trim(),
+                bindScope.generate().toString().trim(),
                 `is`(
                     """
                 final java.lang.Long _tmp;
@@ -446,7 +443,7 @@
                 }
                 """.trimIndent()
             }
-            assertThat(bindScope.builder().build().toString().trim()).isEqualTo(
+            assertThat(bindScope.generate().toString().trim()).isEqualTo(
                 """
                 |final java.lang.String ${tmp(0)} = androidx.room.util.StringUtil.joinIntoString(fooVar);
                 |$expectedAdapterCode
@@ -492,7 +489,6 @@
 
     @Test
     fun testMissingRx2Room() {
-        @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
         runProcessorTest(
             sources = listOf(COMMON.PUBLISHER, COMMON.RX2_FLOWABLE)
         ) { invocation ->
@@ -513,7 +509,6 @@
 
     @Test
     fun testMissingRx3Room() {
-        @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
         runProcessorTest(
             sources = listOf(COMMON.PUBLISHER, COMMON.RX3_FLOWABLE)
         ) { invocation ->
@@ -628,7 +623,6 @@
             COMMON.RX2_FLOWABLE to COMMON.RX2_ROOM,
             COMMON.RX3_FLOWABLE to COMMON.RX3_ROOM
         ).forEach { (rxTypeSrc, rxRoomSrc) ->
-            @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
             runProcessorTest(
                 sources = listOf(
                     COMMON.RX2_SINGLE,
@@ -659,7 +653,6 @@
             Triple(COMMON.RX2_FLOWABLE, COMMON.RX2_ROOM, RxJava2TypeNames.FLOWABLE),
             Triple(COMMON.RX3_FLOWABLE, COMMON.RX3_ROOM, RxJava3TypeNames.FLOWABLE)
         ).forEach { (rxTypeSrc, rxRoomSrc, rxTypeClassName) ->
-            @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
             runProcessorTest(
                 sources = listOf(
                     COMMON.RX2_SINGLE,
@@ -688,7 +681,6 @@
             Triple(COMMON.RX2_OBSERVABLE, COMMON.RX2_ROOM, RxJava2TypeNames.OBSERVABLE),
             Triple(COMMON.RX3_OBSERVABLE, COMMON.RX3_ROOM, RxJava3TypeNames.OBSERVABLE)
         ).forEach { (rxTypeSrc, rxRoomSrc, rxTypeClassName) ->
-            @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
             runProcessorTest(
                 sources = listOf(
                     COMMON.RX2_SINGLE,
@@ -718,7 +710,6 @@
             Triple(COMMON.RX2_SINGLE, COMMON.RX2_ROOM, RxJava2TypeNames.SINGLE),
             Triple(COMMON.RX3_SINGLE, COMMON.RX3_ROOM, RxJava3TypeNames.SINGLE)
         ).forEach { (rxTypeSrc, _, rxTypeClassName) ->
-            @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
             runProcessorTest(sources = listOf(rxTypeSrc)) { invocation ->
                 val single = invocation.processingEnv.requireTypeElement(rxTypeClassName)
                 assertThat(single, notNullValue())
@@ -850,7 +841,6 @@
             Triple(COMMON.RX2_SINGLE, COMMON.RX2_ROOM, RxJava2TypeNames.SINGLE),
             Triple(COMMON.RX3_SINGLE, COMMON.RX3_ROOM, RxJava3TypeNames.SINGLE)
         ).forEach { (rxTypeSrc, _, rxTypeClassName) ->
-            @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
             runProcessorTest(sources = listOf(rxTypeSrc)) { invocation ->
                 val single = invocation.processingEnv.requireTypeElement(rxTypeClassName)
                 assertThat(single).isNotNull()
@@ -1216,7 +1206,7 @@
                 ).first()
             check(dao.isTypeElement())
             val dbType = invocation.context.processingEnv
-                .requireType(ROOM_DB.toJavaPoet())
+                .requireType(ROOM_DB)
             val parser = DaoProcessor(
                 invocation.context,
                 dao, dbType, null,
@@ -1269,7 +1259,7 @@
                 ).first()
             check(dao.isTypeElement())
             val dbType = invocation.context.processingEnv
-                .requireType(ROOM_DB.toJavaPoet())
+                .requireType(ROOM_DB)
             val parser = DaoProcessor(
                 invocation.context,
                 dao, dbType, null,
@@ -1321,7 +1311,7 @@
                 ).first()
             check(dao.isTypeElement())
             val dbType = invocation.context.processingEnv
-                .requireType(ROOM_DB.toJavaPoet())
+                .requireType(ROOM_DB)
             val parser = DaoProcessor(
                 invocation.context,
                 dao, dbType, null,
@@ -1369,7 +1359,7 @@
                 ).first()
             check(dao.isTypeElement())
             val dbType = invocation.context.processingEnv
-                .requireType(ROOM_DB.toJavaPoet())
+                .requireType(ROOM_DB)
             val parser = DaoProcessor(
                 invocation.context,
                 dao, dbType, null,
@@ -1463,7 +1453,7 @@
             collectionTypes.map { collectionType ->
                 invocation.processingEnv.getDeclaredType(
                     invocation.processingEnv.requireTypeElement(collectionType),
-                    invocation.processingEnv.requireType(TypeName.INT).boxed()
+                    invocation.processingEnv.requireType(XTypeName.PRIMITIVE_INT).boxed()
                 )
             }.forEach { type ->
                 val adapter = store.findQueryParameterAdapter(
@@ -1722,7 +1712,7 @@
                 return XCodeBlock.of(
                     scope.language,
                     "%T.joinIntoString(%L)",
-                    STRING_UTIL.toJavaPoet(),
+                    STRING_UTIL,
                     inputVarName
                 )
             }
@@ -1735,7 +1725,7 @@
                 return XCodeBlock.of(
                     scope.language,
                     "%T.splitToIntList(%L)",
-                    STRING_UTIL.toJavaPoet(),
+                    STRING_UTIL,
                     inputVarName
                 )
             }
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt
index aa5618a..e334883 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt
@@ -18,7 +18,7 @@
 
 import androidx.room.Dao
 import androidx.room.Query
-import androidx.room.compiler.codegen.toJavaPoet
+import androidx.room.compiler.codegen.CodeLanguage
 import androidx.room.compiler.processing.XTypeElement
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.runProcessorTest
@@ -45,7 +45,7 @@
                 abstract class MyClass {
                 """
         const val DAO_SUFFIX = "}"
-        val QUERY = ROOM_SQL_QUERY.toJavaPoet().toString()
+        val QUERY = ROOM_SQL_QUERY.toString(CodeLanguage.JAVA)
     }
 
     @Test
@@ -58,7 +58,7 @@
         ) { _, writer ->
             val scope = testCodeGenScope()
             writer.prepareReadAndBind("_sql", "_stmt", scope)
-            assertThat(scope.builder().build().toString().trim()).isEqualTo(
+            assertThat(scope.generate().toString().trim()).isEqualTo(
                 """
                 final java.lang.String _sql = "SELECT id FROM users";
                 final $QUERY _stmt = $QUERY.acquire(_sql, 0);
@@ -90,7 +90,7 @@
                 }
                 """.trimIndent()
             }
-            assertThat(scope.builder().build().toString().trim()).isEqualTo(
+            assertThat(scope.generate().toString().trim()).isEqualTo(
                 """
                 |final java.lang.String _sql = "SELECT id FROM users WHERE name LIKE ?";
                 |final $QUERY _stmt = $QUERY.acquire(_sql, 1);
@@ -111,7 +111,7 @@
         ) { _, writer ->
             val scope = testCodeGenScope()
             writer.prepareReadAndBind("_sql", "_stmt", scope)
-            assertThat(scope.builder().build().toString().trim()).isEqualTo(
+            assertThat(scope.generate().toString().trim()).isEqualTo(
                 """
                 final java.lang.String _sql = "SELECT id FROM users WHERE id IN(?,?)";
                 final $QUERY _stmt = $QUERY.acquire(_sql, 2);
@@ -134,12 +134,12 @@
         ) { _, writer ->
             val scope = testCodeGenScope()
             writer.prepareReadAndBind("_sql", "_stmt", scope)
-            assertThat(scope.builder().build().toString().trim()).isEqualTo(
+            assertThat(scope.generate().toString().trim()).isEqualTo(
                 """
-                final java.lang.StringBuilder _stringBuilder = ${STRING_UTIL.toJavaPoet()}.newStringBuilder();
+                final java.lang.StringBuilder _stringBuilder = ${STRING_UTIL.canonicalName}.newStringBuilder();
                 _stringBuilder.append("SELECT id FROM users WHERE id IN(");
                 final int _inputSize = ids == null ? 1 : ids.length;
-                ${STRING_UTIL.toJavaPoet()}.appendPlaceholders(_stringBuilder, _inputSize);
+                ${STRING_UTIL.canonicalName}.appendPlaceholders(_stringBuilder, _inputSize);
                 _stringBuilder.append(") AND age > ");
                 _stringBuilder.append("?");
                 final java.lang.String _sql = _stringBuilder.toString();
@@ -162,10 +162,10 @@
     }
 
     val collectionOut = """
-        final java.lang.StringBuilder _stringBuilder = ${STRING_UTIL.toJavaPoet()}.newStringBuilder();
+        final java.lang.StringBuilder _stringBuilder = ${STRING_UTIL.canonicalName}.newStringBuilder();
         _stringBuilder.append("SELECT id FROM users WHERE id IN(");
         final int _inputSize = ids == null ? 1 : ids.size();
-        ${STRING_UTIL.toJavaPoet()}.appendPlaceholders(_stringBuilder, _inputSize);
+        ${STRING_UTIL.canonicalName}.appendPlaceholders(_stringBuilder, _inputSize);
         _stringBuilder.append(") AND age > ");
         _stringBuilder.append("?");
         final java.lang.String _sql = _stringBuilder.toString();
@@ -198,7 +198,7 @@
         ) { _, writer ->
             val scope = testCodeGenScope()
             writer.prepareReadAndBind("_sql", "_stmt", scope)
-            assertThat(scope.builder().build().toString().trim()).isEqualTo(collectionOut)
+            assertThat(scope.generate().toString().trim()).isEqualTo(collectionOut)
         }
     }
 
@@ -212,7 +212,7 @@
         ) { _, writer ->
             val scope = testCodeGenScope()
             writer.prepareReadAndBind("_sql", "_stmt", scope)
-            assertThat(scope.builder().build().toString().trim()).isEqualTo(collectionOut)
+            assertThat(scope.generate().toString().trim()).isEqualTo(collectionOut)
         }
     }
 
@@ -226,7 +226,7 @@
         ) { _, writer ->
             val scope = testCodeGenScope()
             writer.prepareReadAndBind("_sql", "_stmt", scope)
-            assertThat(scope.builder().build().toString().trim()).isEqualTo(collectionOut)
+            assertThat(scope.generate().toString().trim()).isEqualTo(collectionOut)
         }
     }
 
@@ -240,7 +240,7 @@
         ) { _, writer ->
             val scope = testCodeGenScope()
             writer.prepareReadAndBind("_sql", "_stmt", scope)
-            assertThat(scope.builder().build().toString().trim()).isEqualTo(
+            assertThat(scope.generate().toString().trim()).isEqualTo(
                 """
                 final java.lang.String _sql = "SELECT id FROM users WHERE age > ? OR bage > ?";
                 final $QUERY _stmt = $QUERY.acquire(_sql, 2);
@@ -263,16 +263,16 @@
         ) { _, writer ->
             val scope = testCodeGenScope()
             writer.prepareReadAndBind("_sql", "_stmt", scope)
-            assertThat(scope.builder().build().toString().trim()).isEqualTo(
+            assertThat(scope.generate().toString().trim()).isEqualTo(
                 """
-                final java.lang.StringBuilder _stringBuilder = ${STRING_UTIL.toJavaPoet()}.newStringBuilder();
+                final java.lang.StringBuilder _stringBuilder = ${STRING_UTIL.canonicalName}.newStringBuilder();
                 _stringBuilder.append("SELECT id FROM users WHERE age > ");
                 _stringBuilder.append("?");
                 _stringBuilder.append(" OR bage > ");
                 _stringBuilder.append("?");
                 _stringBuilder.append(" OR fage IN(");
                 final int _inputSize = ages == null ? 1 : ages.length;
-                ${STRING_UTIL.toJavaPoet()}.appendPlaceholders(_stringBuilder, _inputSize);
+                ${STRING_UTIL.canonicalName}.appendPlaceholders(_stringBuilder, _inputSize);
                 _stringBuilder.append(")");
                 final java.lang.String _sql = _stringBuilder.toString();
                 final int _argCount = 2 + _inputSize;
@@ -305,17 +305,17 @@
         ) { _, writer ->
             val scope = testCodeGenScope()
             writer.prepareReadAndBind("_sql", "_stmt", scope)
-            assertThat(scope.builder().build().toString().trim()).isEqualTo(
+            assertThat(scope.generate().toString().trim()).isEqualTo(
                 """
-                final java.lang.StringBuilder _stringBuilder = ${STRING_UTIL.toJavaPoet()}.newStringBuilder();
+                final java.lang.StringBuilder _stringBuilder = ${STRING_UTIL.canonicalName}.newStringBuilder();
                 _stringBuilder.append("SELECT id FROM users WHERE age IN (");
                 final int _inputSize = ages == null ? 1 : ages.length;
-                ${STRING_UTIL.toJavaPoet()}.appendPlaceholders(_stringBuilder, _inputSize);
+                ${STRING_UTIL.canonicalName}.appendPlaceholders(_stringBuilder, _inputSize);
                 _stringBuilder.append(") OR bage > ");
                 _stringBuilder.append("?");
                 _stringBuilder.append(" OR fage IN(");
                 final int _inputSize_1 = ages == null ? 1 : ages.length;
-                ${STRING_UTIL.toJavaPoet()}.appendPlaceholders(_stringBuilder, _inputSize_1);
+                ${STRING_UTIL.canonicalName}.appendPlaceholders(_stringBuilder, _inputSize_1);
                 _stringBuilder.append(")");
                 final java.lang.String _sql = _stringBuilder.toString();
                 final int _argCount = 1 + _inputSize + _inputSize_1;
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
index 73af03b..dd79611f 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.verifier
 
+import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.processing.XConstructorElement
 import androidx.room.compiler.processing.XElement
 import androidx.room.compiler.processing.XFieldElement
@@ -38,7 +39,6 @@
 import androidx.room.vo.FieldSetter
 import androidx.room.vo.Fields
 import androidx.room.vo.PrimaryKey
-import com.squareup.javapoet.TypeName
 import java.sql.Connection
 import org.hamcrest.CoreMatchers.containsString
 import org.hamcrest.CoreMatchers.hasItem
@@ -312,7 +312,7 @@
                         "User",
                         field(
                             "id",
-                            primitive(invocation.context, TypeName.INT),
+                            primitive(invocation.context, XTypeName.PRIMITIVE_INT),
                             SQLTypeAffinity.INTEGER
                         ),
                         field(
@@ -348,16 +348,19 @@
                 entity(
                     invocation,
                     "User",
-                    field("id", primitive(context, TypeName.INT), SQLTypeAffinity.INTEGER),
+                    field("id",
+                        primitive(context, XTypeName.PRIMITIVE_INT), SQLTypeAffinity.INTEGER),
                     field("name", context.COMMON_TYPES.STRING, SQLTypeAffinity.TEXT),
                     field("lastName", context.COMMON_TYPES.STRING, SQLTypeAffinity.TEXT),
-                    field("ratio", primitive(context, TypeName.FLOAT), SQLTypeAffinity.REAL)
+                    field("ratio",
+                        primitive(context, XTypeName.PRIMITIVE_FLOAT), SQLTypeAffinity.REAL)
                 )
             ),
             listOf(
                 view(
                     "UserSummary", "SELECT id, name FROM User",
-                    field("id", primitive(context, TypeName.INT), SQLTypeAffinity.INTEGER),
+                    field("id",
+                        primitive(context, XTypeName.PRIMITIVE_INT), SQLTypeAffinity.INTEGER),
                     field("name", context.COMMON_TYPES.STRING, SQLTypeAffinity.TEXT)
                 )
             )
@@ -439,7 +442,7 @@
         f.setter = FieldSetter(f.name, name, type, CallType.FIELD)
     }
 
-    private fun primitive(context: Context, typeName: TypeName): XType {
+    private fun primitive(context: Context, typeName: XTypeName): XType {
         return context.processingEnv.requireType(typeName)
     }
 
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..e39639d 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
@@ -1074,12 +1074,19 @@
               }
 
               @Transaction
-              open fun concreteInternal() {
+              internal open fun concreteInternal() {
               }
 
               @Transaction
               open suspend fun suspendConcrete() {
+              }
 
+              @Transaction
+              open fun concreteWithVararg(vararg arr: Long) {
+              }
+
+              @Transaction
+              open suspend fun suspendConcreteWithVararg(vararg arr: Long) {
               }
             }
 
@@ -1200,15 +1207,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 +1253,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 +1373,6 @@
             interface MyDao {
               @Query("SELECT * FROM MyEntity")
               fun queryOfList(): ImmutableList<MyEntity>
-
-              @Query("SELECT * FROM MyEntity")
-              fun queryOfNullableEntityList(): ImmutableList<MyEntity?>
             }
 
             @Entity
@@ -1894,9 +1883,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/kotlin/androidx/room/writer/DaoWriterTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt
index 45e244d..de81792 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt
@@ -18,7 +18,6 @@
 
 import COMMON
 import androidx.room.compiler.codegen.CodeLanguage
-import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XTypeElement
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.XTestInvocation
@@ -176,7 +175,7 @@
                         androidx.room.Database::class.qualifiedName!!
                     ).filterIsInstance<XTypeElement>().firstOrNull()
                     ?: invocation.context.processingEnv
-                        .requireTypeElement(ROOM_DB.toJavaPoet())
+                        .requireTypeElement(ROOM_DB)
                 val dbType = db.type
                 val dbVerifier = createVerifierFromEntitiesAndViews(invocation)
                 invocation.context.attachDatabaseVerifier(dbVerifier)
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DefaultsInDaoTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DefaultsInDaoTest.kt
index 4a7cc1d..7d67afd 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DefaultsInDaoTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DefaultsInDaoTest.kt
@@ -18,7 +18,6 @@
 
 import COMMON
 import androidx.room.compiler.codegen.CodeLanguage
-import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XTypeElement
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.runKaptTest
@@ -157,7 +156,7 @@
                 ).filterIsInstance<XTypeElement>()
                 .forEach { dao ->
                     val db = invocation.context.processingEnv
-                        .requireTypeElement(ROOM_DB.toJavaPoet())
+                        .requireTypeElement(ROOM_DB)
                     val dbType = db.type
                     val parser = DaoProcessor(
                         baseContext = invocation.context,
@@ -170,7 +169,7 @@
                         .write(invocation.processingEnv)
                     invocation.assertCompilationResult {
                         val relativePath =
-                            parsedDao.implTypeName.toJavaPoet().simpleName() + ".java"
+                            parsedDao.implTypeName.canonicalName + ".java"
                         handler(generatedSourceFileWithPath(relativePath))
                     }
                 }
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..f775cf2 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutines.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutines.kt
@@ -15,7 +15,6 @@
 import java.util.ArrayList
 import java.util.concurrent.Callable
 import javax.`annotation`.processing.Generated
-import kotlin.Array
 import kotlin.Int
 import kotlin.String
 import kotlin.Suppress
@@ -128,7 +127,7 @@
         })
     }
 
-    public override suspend fun getSuspendList(arg: Array<out String?>): List<MyEntity> {
+    public override suspend fun getSuspendList(vararg arg: String?): List<MyEntity> {
         val _stringBuilder: StringBuilder = newStringBuilder()
         _stringBuilder.append("SELECT * FROM MyEntity WHERE pk IN (")
         val _inputSize: Int = arg.size
@@ -172,50 +171,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/room/room-compiler/src/test/test-data/kotlinCodeGen/shortcutMethods_suspend.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/shortcutMethods_suspend.kt
index 7362f6a..de09864 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/shortcutMethods_suspend.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/shortcutMethods_suspend.kt
@@ -7,7 +7,6 @@
 import java.lang.Class
 import java.util.concurrent.Callable
 import javax.`annotation`.processing.Generated
-import kotlin.Array
 import kotlin.Int
 import kotlin.Long
 import kotlin.String
@@ -79,7 +78,7 @@
         })
     }
 
-    public override suspend fun insert(entities: Array<out MyEntity>): List<Long> =
+    public override suspend fun insert(vararg entities: MyEntity): List<Long> =
         CoroutinesRoom.execute(__db, true, object : Callable<List<Long>> {
             public override fun call(): List<Long> {
                 __db.beginTransaction()
@@ -123,7 +122,7 @@
             }
         })
 
-    public override suspend fun upsert(entities: Array<out MyEntity>): List<Long> =
+    public override suspend fun upsert(vararg entities: MyEntity): List<Long> =
         CoroutinesRoom.execute(__db, true, object : Callable<List<Long>> {
             public override fun call(): List<Long> {
                 __db.beginTransaction()
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt
index 3db73a3..d59e245 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt
@@ -2,6 +2,7 @@
 import androidx.room.withTransaction
 import java.lang.Class
 import javax.`annotation`.processing.Generated
+import kotlin.Long
 import kotlin.Suppress
 import kotlin.Unit
 import kotlin.collections.List
@@ -43,7 +44,7 @@
         }
     }
 
-    public override fun concreteInternal(): Unit {
+    internal override fun concreteInternal(): Unit {
         __db.beginTransaction()
         try {
             super@MyDao_Impl.concreteInternal()
@@ -59,6 +60,22 @@
         }
     }
 
+    public override fun concreteWithVararg(vararg arr: Long): Unit {
+        __db.beginTransaction()
+        try {
+            super@MyDao_Impl.concreteWithVararg(*arr)
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override suspend fun suspendConcreteWithVararg(vararg arr: Long): Unit {
+        __db.withTransaction {
+            super@MyDao_Impl.suspendConcreteWithVararg(*arr)
+        }
+    }
+
     public companion object {
         @JvmStatic
         public fun getRequiredConverters(): List<Class<*>> = emptyList()
diff --git a/room/room-runtime/src/main/java/androidx/room/EntityUpsertionAdapter.kt b/room/room-runtime/src/main/java/androidx/room/EntityUpsertionAdapter.kt
index aaefe4c..89fb70a 100644
--- a/room/room-runtime/src/main/java/androidx/room/EntityUpsertionAdapter.kt
+++ b/room/room-runtime/src/main/java/androidx/room/EntityUpsertionAdapter.kt
@@ -208,7 +208,11 @@
     private fun checkUniquenessException(ex: SQLiteConstraintException) {
         val message = ex.message ?: throw ex
         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
-            if (!message.contains(ErrorMsg, ignoreCase = true)) {
+            if (!message.contains(ErrorMsg, ignoreCase = true) && !message.contains(
+                    ErrorCode,
+                    ignoreCase = true
+                )
+            ) {
                 throw ex
             }
         } else {
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 25932b8..7e69d77 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
@@ -16,8 +16,20 @@
 
 package com.example.androidx.mediarouting;
 
+import static com.example.androidx.mediarouting.data.RouteItem.ControlFilter.BASIC;
+import static com.example.androidx.mediarouting.data.RouteItem.DeviceType.SPEAKER;
+import static com.example.androidx.mediarouting.data.RouteItem.DeviceType.TV;
+import static com.example.androidx.mediarouting.data.RouteItem.PlaybackStream.MUSIC;
+import static com.example.androidx.mediarouting.data.RouteItem.PlaybackType.REMOTE;
+import static com.example.androidx.mediarouting.data.RouteItem.VolumeHandling.VARIABLE;
+
+import android.content.Context;
+import android.content.res.Resources;
+
 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;
 
@@ -29,21 +41,31 @@
 /** Holds the data needed to control the provider for the routes dynamically. */
 public final class RoutesManager {
 
-    private boolean mDynamicRoutingEnabled;
-    private final Map<String, RouteItem> mRouteItems;
+    private static final String VARIABLE_VOLUME_BASIC_ROUTE_ID = "variable_basic";
+    private static final int VOLUME_MAX = 25;
+    private static final int VOLUME_DEFAULT = 5;
+
     private static RoutesManager sInstance;
 
-    private RoutesManager() {
-        this.mDynamicRoutingEnabled = true;
-        this.mRouteItems = new HashMap<>();
+    private final Context mContext;
+    private final Map<String, RouteItem> mRouteItems;
+    private boolean mDynamicRoutingEnabled;
+    private DialogType mDialogType;
+
+    private RoutesManager(Context context) {
+        mContext = context;
+        mDynamicRoutingEnabled = true;
+        mDialogType = DialogType.OUTPUT_SWITCHER;
+        mRouteItems = new HashMap<>();
+        initTestRoutes();
     }
 
     /** Singleton method. */
     @NonNull
-    public static RoutesManager getInstance() {
+    public static RoutesManager getInstance(@NonNull Context context) {
         synchronized (RoutesManager.class) {
             if (sInstance == null) {
-                sInstance = new RoutesManager();
+                sInstance = new RoutesManager(context);
             }
         }
         return sInstance;
@@ -62,6 +84,10 @@
         this.mDynamicRoutingEnabled = dynamicRoutingEnabled;
     }
 
+    public void setDialogType(@NonNull DialogType dialogType) {
+        this.mDialogType = dialogType;
+    }
+
     /**
      * Deletes the route with the passed id.
      *
@@ -83,7 +109,97 @@
     }
 
     /** 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();
+
+        RouteItem r1 = new RouteItem();
+        r1.setId(VARIABLE_VOLUME_BASIC_ROUTE_ID + "1");
+        r1.setName(r.getString(R.string.dg_tv_route_name1));
+        r1.setDescription(r.getString(R.string.sample_route_description));
+        r1.setControlFilter(BASIC);
+        r1.setDeviceType(TV);
+        r1.setPlaybackStream(MUSIC);
+        r1.setPlaybackType(REMOTE);
+        r1.setVolumeHandling(VARIABLE);
+        r1.setVolumeMax(VOLUME_MAX);
+        r1.setVolume(VOLUME_DEFAULT);
+        r1.setCanDisconnect(true);
+
+        RouteItem r2 = new RouteItem();
+        r2.setId(VARIABLE_VOLUME_BASIC_ROUTE_ID + "2");
+        r2.setName(r.getString(R.string.dg_tv_route_name2));
+        r2.setDescription(r.getString(R.string.sample_route_description));
+        r2.setControlFilter(BASIC);
+        r2.setDeviceType(TV);
+        r2.setPlaybackStream(MUSIC);
+        r2.setPlaybackType(REMOTE);
+        r2.setVolumeHandling(VARIABLE);
+        r2.setVolumeMax(VOLUME_MAX);
+        r2.setVolume(VOLUME_DEFAULT);
+        r2.setCanDisconnect(true);
+
+        RouteItem r3 = new RouteItem();
+        r3.setId(VARIABLE_VOLUME_BASIC_ROUTE_ID + "3");
+        r3.setName(r.getString(R.string.dg_speaker_route_name3));
+        r3.setDescription(r.getString(R.string.sample_route_description));
+        r3.setControlFilter(BASIC);
+        r3.setDeviceType(SPEAKER);
+        r3.setPlaybackStream(MUSIC);
+        r3.setPlaybackType(REMOTE);
+        r3.setVolumeHandling(VARIABLE);
+        r3.setVolumeMax(VOLUME_MAX);
+        r3.setVolume(VOLUME_DEFAULT);
+        r3.setCanDisconnect(true);
+
+        RouteItem r4 = new RouteItem();
+        r4.setId(VARIABLE_VOLUME_BASIC_ROUTE_ID + "4");
+        r4.setName(r.getString(R.string.dg_speaker_route_name4));
+        r4.setDescription(r.getString(R.string.sample_route_description));
+        r4.setControlFilter(BASIC);
+        r4.setDeviceType(SPEAKER);
+        r4.setPlaybackStream(MUSIC);
+        r4.setPlaybackType(REMOTE);
+        r4.setVolumeHandling(VARIABLE);
+        r4.setVolumeMax(VOLUME_MAX);
+        r4.setVolume(VOLUME_DEFAULT);
+        r4.setCanDisconnect(true);
+
+        mRouteItems.put(r1.getId(), r1);
+        mRouteItems.put(r2.getId(), r2);
+        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..6b2b256
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/AddEditRouteActivity.java
@@ -0,0 +1,241 @@
+/*
+ * 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,
+                findViewById(R.id.control_filters_spinner),
+                mRouteItem.getControlFilter(),
+                newControlFilter ->
+                        mRouteItem.setControlFilter((RouteItem.ControlFilter) newControlFilter));
+
+        setUpEnumBasedSpinner(
+                /* context= */ this,
+                findViewById(R.id.playback_stream_spinner),
+                mRouteItem.getPlaybackStream(),
+                newPlaybackStream ->
+                        mRouteItem.setPlaybackStream((RouteItem.PlaybackStream) newPlaybackStream));
+
+        setUpEnumBasedSpinner(
+                /* context= */ this,
+                findViewById(R.id.playback_type_spinner),
+                mRouteItem.getPlaybackType(),
+                newPlaybackType ->
+                        mRouteItem.setPlaybackType((RouteItem.PlaybackType) newPlaybackType));
+
+        setUpEnumBasedSpinner(
+                /* context= */ this,
+                findViewById(R.id.device_type_spinner),
+                mRouteItem.getDeviceType(),
+                newDeviceType -> mRouteItem.setDeviceType((RouteItem.DeviceType) newDeviceType));
+
+        setUpEnumBasedSpinner(
+                /* context= */ this,
+                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,
+            Spinner spinner,
+            Enum<?> anEnum,
+            RoutePropertySetter<Enum<?>> routePropertySetter) {
+        Enum<?>[] enumValues = anEnum.getDeclaringClass().getEnumConstants();
+        ArrayAdapter<Enum<?>> adapter =
+                new ArrayAdapter<>(context, android.R.layout.simple_spinner_item, enumValues);
+        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..6133c5e 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,18 +103,29 @@
 
     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;
 
+    private final MediaRouter.OnPrepareTransferListener mOnPrepareTransferListener =
+            (fromRoute, toRoute) -> {
+                Log.d(TAG, "onPrepareTransfer: from=" + fromRoute.getId()
+                        + ", to=" + toRoute.getId());
+                return CallbackToFutureAdapter.getFuture(completer -> {
+                    mHandler.postDelayed(() -> completer.set(null), 3000);
+                    return "onPrepareTransfer";
+                });
+            };
+
     private final MediaRouter.Callback mMediaRouterCB = new MediaRouter.Callback() {
         // Return a custom callback that will simply log all of the route events
         // for demonstration purposes.
@@ -204,21 +222,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)
@@ -226,14 +239,7 @@
                 .addControlCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE)
                 .build();
 
-        mMediaRouter.setOnPrepareTransferListener((fromRoute, toRoute) -> {
-            Log.d(TAG, "onPrepareTransfer: from=" + fromRoute.getId()
-                    + ", to=" + toRoute.getId());
-            return CallbackToFutureAdapter.getFuture(completer -> {
-                mHandler.postDelayed(() -> completer.set(null), 3000);
-                return "onPrepareTransfer";
-            });
-        });
+        mMediaRouter.setOnPrepareTransferListener(mOnPrepareTransferListener);
 
         // Add a fragment to take care of media route discovery.
         // This fragment automatically adds or removes a callback whenever the activity
@@ -363,8 +369,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 +399,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 +752,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 9c40caa..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
@@ -62,19 +63,18 @@
 
         mMediaRouter = MediaRouter.getInstance(this);
 
-        mRoutesManager = RoutesManager.getInstance();
+        mRoutesManager = RoutesManager.getInstance(getApplicationContext());
 
         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..eb034ed 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
@@ -16,6 +16,9 @@
 
 package com.example.androidx.mediarouting.data;
 
+import android.media.AudioManager;
+import android.media.MediaRouter;
+
 import androidx.annotation.NonNull;
 
 import java.util.ArrayList;
@@ -53,38 +56,107 @@
         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,
-        SESSION
+        SESSION;
     }
 
     public enum PlaybackStream {
-        ACCESSIBILITY,
-        ALARM,
-        DTMF,
-        MUSIC,
-        NOTIFICATION,
-        RING,
-        SYSTEM,
-        VOICE_CALL
+        ACCESSIBILITY(AudioManager.STREAM_ACCESSIBILITY),
+        ALARM(AudioManager.STREAM_ALARM),
+        DTMF(AudioManager.STREAM_DTMF),
+        MUSIC(AudioManager.STREAM_MUSIC),
+        NOTIFICATION(AudioManager.STREAM_NOTIFICATION),
+        RING(AudioManager.STREAM_RING),
+        SYSTEM(AudioManager.STREAM_SYSTEM),
+        VOICE_CALL(AudioManager.STREAM_VOICE_CALL);
+
+        public final int mIntConstant;
+
+        PlaybackStream(int intConstant) {
+            mIntConstant = intConstant;
+        }
     }
 
     public enum PlaybackType {
-        LOCAL,
-        REMOTE
+        LOCAL(MediaRouter.RouteInfo.PLAYBACK_TYPE_LOCAL),
+        REMOTE(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE);
+
+        public final int mIntConstant;
+
+        PlaybackType(int intConstant) {
+            mIntConstant = intConstant;
+        }
     }
 
     public enum VolumeHandling {
-        FIXED,
-        VARIABLE
+        FIXED(MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED),
+        VARIABLE(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE);
+
+        public final int mIntConstant;
+
+        VolumeHandling(int intConstant) {
+            mIntConstant = intConstant;
+        }
     }
 
     public enum DeviceType {
-        TV,
-        SPEAKER,
-        BLUETOOTH,
-        UNKNOWN
+        TV(MediaRouter.RouteInfo.DEVICE_TYPE_TV),
+        SPEAKER(MediaRouter.RouteInfo.DEVICE_TYPE_SPEAKER),
+        BLUETOOTH(MediaRouter.RouteInfo.DEVICE_TYPE_BLUETOOTH),
+        UNKNOWN(MediaRouter.RouteInfo.DEVICE_TYPE_UNKNOWN);
+
+        public final int mIntConstant;
+
+        DeviceType(int intConstant) {
+            mIntConstant = intConstant;
+        }
     }
 
     @NonNull
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/player/LocalPlayer.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/player/LocalPlayer.java
index 468118b..ff82614 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/player/LocalPlayer.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/player/LocalPlayer.java
@@ -150,19 +150,22 @@
     }
 
     @Override
-    public void getStatus(@NonNull final PlaylistItem item, final boolean update) {
+    public void getStatus(@NonNull final PlaylistItem item, final boolean shouldUpdate) {
         if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
-            // use mSeekToPos if we're currently seeking (mSeekToPos is reset
-            // when seeking is completed)
             item.setDuration(mMediaPlayer.getDuration());
-            item.setPosition(mSeekToPos > 0 ? mSeekToPos : mMediaPlayer.getCurrentPosition());
+            item.setPosition(getCurrentPosition());
             item.setTimestamp(SystemClock.elapsedRealtime());
         }
-        if (update && mCallback != null) {
+        if (shouldUpdate && mCallback != null) {
             mCallback.onPlaylistReady();
         }
     }
 
+    private int getCurrentPosition() {
+        // Use mSeekToPos if we're currently seeking (mSeekToPos is reset when seeking is completed)
+        return mSeekToPos > 0 ? mSeekToPos : mMediaPlayer.getCurrentPosition();
+    }
+
     @Override
     public void pause() {
         if (DEBUG) {
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/player/Player.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/player/Player.java
index cda63c3..3a5d154 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/player/Player.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/player/Player.java
@@ -129,9 +129,9 @@
     /**
      * Get player status of an item.
      * @param item
-     * @param update
+     * @param shouldUpdate
      */
-    public abstract void getStatus(@NonNull PlaylistItem item, boolean update);
+    public abstract void getStatus(@NonNull PlaylistItem item, boolean shouldUpdate);
 
     /**
      * Player pause operation.
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/player/RemotePlayer.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/player/RemotePlayer.java
index 2ffdedd..cc2ded3 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/player/RemotePlayer.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/player/RemotePlayer.java
@@ -161,7 +161,7 @@
     }
 
     @Override
-    public void getStatus(final @NonNull PlaylistItem item, final boolean update) {
+    public void getStatus(final @NonNull PlaylistItem item, final boolean shouldUpdate) {
         if (!mClient.hasSession() || item.getRemoteItemId() == null) {
             // if session is not valid or item id not assigend yet.
             // just return, it's not fatal
@@ -169,8 +169,12 @@
         }
 
         if (DEBUG) {
-            Log.d(TAG, "getStatus: item=" + item + ", update=" + update);
+            Log.d(TAG, "getStatus: item=" + item + ", shouldUpdate=" + shouldUpdate);
         }
+        updateStatus(item, shouldUpdate);
+    }
+
+    private void updateStatus(@NonNull PlaylistItem item, boolean shouldUpdate) {
         mClient.getStatus(item.getRemoteItemId(), null, new ItemActionCallback() {
             @Override
             public void onResult(@NonNull Bundle data, @NonNull String sessionId,
@@ -186,7 +190,7 @@
                     item.setDuration(itemStatus.getContentDuration());
                     item.setTimestamp(itemStatus.getTimestamp());
                 }
-                if (update && mCallback != null) {
+                if (shouldUpdate && mCallback != null) {
                     mCallback.onPlaylistReady();
                 }
             }
@@ -194,7 +198,7 @@
             @Override
             public void onError(String error, int code, Bundle data) {
                 logError("getStatus: failed", error, code);
-                if (update && mCallback != null) {
+                if (shouldUpdate && mCallback != null) {
                     mCallback.onPlaylistReady();
                 }
             }
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java
index baf3cfec..d4b3100 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java
@@ -21,7 +21,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.IntentSender;
-import android.media.AudioManager;
 import android.media.MediaRouter;
 import android.util.Log;
 
@@ -135,7 +134,8 @@
 
     /** Reload the isDynamicRouteEnabled flag. */
     public void reloadDynamicRoutesEnabled() {
-        boolean isDynamicRoutesEnabled = RoutesManager.getInstance().isDynamicRoutingEnabled();
+        boolean isDynamicRoutesEnabled = RoutesManager.getInstance(getContext())
+                .isDynamicRoutingEnabled();
         MediaRouteProviderDescriptor providerDescriptor =
                 new MediaRouteProviderDescriptor.Builder(getDescriptor())
                         .setSupportsDynamicGroupRoute(isDynamicRoutesEnabled)
@@ -154,7 +154,7 @@
         IntentSender is = PendingIntent.getActivity(getContext(), 99, settingsIntent,
                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE).getIntentSender();
 
-        List<RouteItem> routeItems = RoutesManager.getInstance().getRouteItems();
+        List<RouteItem> routeItems = RoutesManager.getInstance(getContext()).getRouteItems();
         for (RouteItem routeItem : routeItems) {
             MediaRouteDescriptor routeDescriptor = buildRouteDescriptor(routeItem, is);
             mVolumes.put(routeItem.getId(), routeItem.getVolume());
@@ -176,10 +176,10 @@
         return new MediaRouteDescriptor.Builder(routeItem.getId(), routeItem.getName())
                 .setDescription(routeItem.getDescription())
                 .addControlFilters(getControlFilters(routeItem.getControlFilter()))
-                .setPlaybackStream(getPlaybackStream(routeItem.getPlaybackStream()))
-                .setPlaybackType(getPlaybackType(routeItem.getPlaybackType()))
-                .setVolumeHandling(getVolumeHandling(routeItem.getVolumeHandling()))
-                .setDeviceType(getDeviceType(routeItem.getDeviceType()))
+                .setPlaybackStream(routeItem.getPlaybackStream().mIntConstant)
+                .setPlaybackType(routeItem.getPlaybackType().mIntConstant)
+                .setVolumeHandling(routeItem.getVolumeHandling().mIntConstant)
+                .setDeviceType(routeItem.getDeviceType().mIntConstant)
                 .setVolumeMax(routeItem.getVolumeMax())
                 .setVolume(routeItem.getVolume())
                 .setCanDisconnect(routeItem.isCanDisconnect())
@@ -187,49 +187,7 @@
                 .build();
     }
 
-    private int getPlaybackStream(RouteItem.PlaybackStream playBackStream) {
-        switch (playBackStream) {
-            case ACCESSIBILITY:
-                return AudioManager.STREAM_ACCESSIBILITY;
-            case ALARM:
-                return AudioManager.STREAM_ALARM;
-            case DTMF:
-                return AudioManager.STREAM_DTMF;
-            case MUSIC:
-                return AudioManager.STREAM_MUSIC;
-            case NOTIFICATION:
-                return AudioManager.STREAM_NOTIFICATION;
-            case RING:
-                return AudioManager.STREAM_RING;
-            case SYSTEM:
-                return AudioManager.STREAM_SYSTEM;
-            case VOICE_CALL:
-                return AudioManager.STREAM_VOICE_CALL;
-        }
-        return AudioManager.STREAM_MUSIC;
-    }
-
-    private int getPlaybackType(RouteItem.PlaybackType playBackType) {
-        switch (playBackType) {
-            case LOCAL:
-                return MediaRouter.RouteInfo.PLAYBACK_TYPE_LOCAL;
-            case REMOTE:
-                return MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE;
-        }
-        return MediaRouter.RouteInfo.PLAYBACK_TYPE_LOCAL;
-    }
-
-    private int getVolumeHandling(RouteItem.VolumeHandling volumeHandling) {
-        switch (volumeHandling) {
-            case FIXED:
-                return MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED;
-            case VARIABLE:
-                return MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE;
-        }
-        return MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED;
-    }
-
-    private List<IntentFilter> getControlFilters(RouteItem.ControlFilter controlFilter) {
+    private static List<IntentFilter> getControlFilters(RouteItem.ControlFilter controlFilter) {
         switch (controlFilter) {
             case BASIC:
                 return CONTROL_FILTERS_BASIC;
@@ -241,20 +199,6 @@
         return new ArrayList<>();
     }
 
-    private int getDeviceType(RouteItem.DeviceType deviceType) {
-        switch (deviceType) {
-            case SPEAKER:
-                return MediaRouter.RouteInfo.DEVICE_TYPE_SPEAKER;
-            case BLUETOOTH:
-                return MediaRouter.RouteInfo.DEVICE_TYPE_BLUETOOTH;
-            case TV:
-                return MediaRouter.RouteInfo.DEVICE_TYPE_TV;
-            case UNKNOWN:
-                return MediaRouter.RouteInfo.DEVICE_TYPE_UNKNOWN;
-        }
-        return MediaRouter.RouteInfo.DEVICE_TYPE_UNKNOWN;
-    }
-
     final class SampleDynamicGroupRouteController
             extends MediaRouteProvider.DynamicGroupRouteController {
         private final String mRouteId;
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..8511ab9
--- /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/savedstate/savedstate/build.gradle b/savedstate/savedstate/build.gradle
index 26d8317..e2be4cf 100644
--- a/savedstate/savedstate/build.gradle
+++ b/savedstate/savedstate/build.gradle
@@ -21,7 +21,7 @@
 
     api("androidx.annotation:annotation:1.1.0")
     implementation("androidx.arch.core:core-common:2.1.0")
-    implementation("androidx.lifecycle:lifecycle-common:2.4.0")
+    implementation(project(":lifecycle:lifecycle-common"))
     api(libs.kotlinStdlib)
 
     androidTestImplementation("androidx.lifecycle:lifecycle-runtime:2.4.0")
diff --git a/savedstate/savedstate/src/androidTest/java/androidx/savedstate/SavedStateRegistryTest.kt b/savedstate/savedstate/src/androidTest/java/androidx/savedstate/SavedStateRegistryTest.kt
index 8389a1c..1d6389e 100644
--- a/savedstate/savedstate/src/androidTest/java/androidx/savedstate/SavedStateRegistryTest.kt
+++ b/savedstate/savedstate/src/androidTest/java/androidx/savedstate/SavedStateRegistryTest.kt
@@ -232,7 +232,8 @@
     val lifecycleRegistry = LifecycleRegistry(this)
     val savedStateRegistryController = SavedStateRegistryController.create(this)
 
-    override fun getLifecycle() = lifecycleRegistry
+    override val lifecycle
+        get() = lifecycleRegistry
     override val savedStateRegistry: SavedStateRegistry
         get() = savedStateRegistryController.savedStateRegistry
 }
diff --git a/savedstate/savedstate/src/androidTest/java/androidx/savedstate/ViewTreeSavedStateRegistryOwnerTest.kt b/savedstate/savedstate/src/androidTest/java/androidx/savedstate/ViewTreeSavedStateRegistryOwnerTest.kt
index d176efc..42cd55e 100644
--- a/savedstate/savedstate/src/androidTest/java/androidx/savedstate/ViewTreeSavedStateRegistryOwnerTest.kt
+++ b/savedstate/savedstate/src/androidTest/java/androidx/savedstate/ViewTreeSavedStateRegistryOwnerTest.kt
@@ -116,9 +116,8 @@
     }
 
     internal class FakeSavedStateRegistryOwner : SavedStateRegistryOwner {
-        override fun getLifecycle(): Lifecycle {
-            throw UnsupportedOperationException("not a real SavedStateRegistryOwner")
-        }
+        override val lifecycle: Lifecycle
+            get() = throw UnsupportedOperationException("not a real SavedStateRegistryOwner")
 
         override val savedStateRegistry: SavedStateRegistry
             get() = throw UnsupportedOperationException("not a real SavedStateRegistryOwner")
diff --git a/settings.gradle b/settings.gradle
index 986447d..cc816be 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")
         }
     }
@@ -360,7 +365,9 @@
 includeProject(":annotation:annotation-experimental-lint")
 includeProject(":annotation:annotation-experimental-lint-integration-tests", "annotation/annotation-experimental-lint/integration-tests")
 includeProject(":annotation:annotation-sampled")
+includeProject(":appactions:interaction:interaction-capabilities-core", [BuildType.MAIN])
 includeProject(":appactions:interaction:interaction-proto", [BuildType.MAIN])
+includeProject(":appactions:interaction:interaction-service", [BuildType.MAIN])
 includeProject(":appcompat:appcompat", [BuildType.MAIN])
 includeProject(":appcompat:appcompat-benchmark", [BuildType.MAIN])
 includeProject(":appcompat:appcompat-lint", [BuildType.MAIN])
@@ -376,8 +383,8 @@
 includeProject(":appsearch:appsearch-local-storage", [BuildType.MAIN])
 includeProject(":appsearch:appsearch-platform-storage", [BuildType.MAIN])
 includeProject(":appsearch:appsearch-test-util", [BuildType.MAIN])
-includeProject(":arch:core:core-common", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
-includeProject(":arch:core:core-runtime", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+includeProject(":arch:core:core-common", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":arch:core:core-runtime", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
 includeProject(":arch:core:core-testing", [BuildType.MAIN])
 includeProject(":asynclayoutinflater:asynclayoutinflater", [BuildType.MAIN])
 includeProject(":asynclayoutinflater:asynclayoutinflater-appcompat", [BuildType.MAIN])
@@ -389,9 +396,17 @@
 includeProject(":benchmark:benchmark-darwin-samples", [BuildType.KMP])
 includeProject(":benchmark:benchmark-darwin-gradle-plugin", [BuildType.KMP])
 includeProject(":benchmark:benchmark-gradle-plugin", "benchmark/gradle-plugin", [BuildType.MAIN])
+includeProject(":benchmark:benchmark-baseline-profiles-gradle-plugin", "benchmark/baseline-profiles-gradle-plugin",[BuildType.MAIN])
 includeProject(":benchmark:benchmark-junit4")
 includeProject(":benchmark:benchmark-macro", [BuildType.MAIN, BuildType.COMPOSE])
 includeProject(":benchmark:benchmark-macro-junit4", [BuildType.MAIN, BuildType.COMPOSE])
+includeProject(":benchmark:integration-tests:baselineprofiles-producer", [BuildType.MAIN])
+includeProject(":benchmark:integration-tests:baselineprofiles-consumer", [BuildType.MAIN])
+includeProject(":benchmark:integration-tests:baselineprofiles-flavors-producer", [BuildType.MAIN])
+includeProject(":benchmark:integration-tests:baselineprofiles-flavors-consumer", [BuildType.MAIN])
+includeProject(":benchmark:integration-tests:baselineprofiles-library-consumer", [BuildType.MAIN])
+includeProject(":benchmark:integration-tests:baselineprofiles-library-producer", [BuildType.MAIN])
+includeProject(":benchmark:integration-tests:baselineprofiles-library-build-provider", [BuildType.MAIN])
 includeProject(":benchmark:integration-tests:dry-run-benchmark", [BuildType.MAIN])
 includeProject(":benchmark:integration-tests:macrobenchmark", [BuildType.MAIN])
 includeProject(":benchmark:integration-tests:macrobenchmark-target", [BuildType.MAIN])
@@ -580,7 +595,7 @@
 includeProject(":concurrent:concurrent-futures-ktx", [BuildType.MAIN, BuildType.CAMERA])
 includeProject(":constraintlayout:constraintlayout-compose", [BuildType.COMPOSE])
 includeProject(":constraintlayout:constraintlayout-compose-lint", [BuildType.COMPOSE])
-includeProject(":constraintlayout:constraintlayout-compose:integration-tests:constraintlayout-compose-demos", [BuildType.COMPOSE])
+includeProject(":constraintlayout:constraintlayout-compose:integration-tests:demos", [BuildType.COMPOSE])
 includeProject(":constraintlayout:constraintlayout-compose:integration-tests:macrobenchmark", [BuildType.COMPOSE])
 includeProject(":constraintlayout:constraintlayout-compose:integration-tests:macrobenchmark-target", [BuildType.COMPOSE])
 includeProject(":constraintlayout:constraintlayout", [BuildType.MAIN])
@@ -708,33 +723,33 @@
 includeProject(":lifecycle:integration-tests:incrementality", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":lifecycle:integration-tests:lifecycle-testapp", "lifecycle/integration-tests/testapp", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":lifecycle:integration-tests:lifecycle-testapp-kotlin", "lifecycle/integration-tests/kotlintestapp", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-common", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-common-java8", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+includeProject(":lifecycle:lifecycle-common", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":lifecycle:lifecycle-common-java8", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
 includeProject(":lifecycle:lifecycle-compiler", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":lifecycle:lifecycle-extensions", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-livedata", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-livedata-core", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-livedata-core-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-livedata-core-ktx-lint", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+includeProject(":lifecycle:lifecycle-livedata", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":lifecycle:lifecycle-livedata-core", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":lifecycle:lifecycle-livedata-core-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":lifecycle:lifecycle-livedata-core-ktx-lint", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
 includeProject(":lifecycle:lifecycle-livedata-core-truth", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-livedata-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+includeProject(":lifecycle:lifecycle-livedata-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
 includeProject(":lifecycle:lifecycle-process", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":lifecycle:lifecycle-reactivestreams", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":lifecycle:lifecycle-reactivestreams-ktx", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-runtime", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+includeProject(":lifecycle:lifecycle-runtime", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
 includeProject(":lifecycle:lifecycle-runtime-compose", [BuildType.COMPOSE])
 includeProject(":lifecycle:lifecycle-runtime-compose:lifecycle-runtime-compose-samples", "lifecycle/lifecycle-runtime-compose/samples", [BuildType.COMPOSE])
 includeProject(":lifecycle:lifecycle-runtime-compose:integration-tests:lifecycle-runtime-compose-demos", [BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-runtime-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-runtime-ktx-lint", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-runtime-testing", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-service", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-viewmodel", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+includeProject(":lifecycle:lifecycle-runtime-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":lifecycle:lifecycle-runtime-ktx-lint", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":lifecycle:lifecycle-runtime-testing", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":lifecycle:lifecycle-service", [BuildType.MAIN, BuildType.FLAN, BuildType.GLANCE])
+includeProject(":lifecycle:lifecycle-viewmodel", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
 includeProject(":lifecycle:lifecycle-viewmodel-compose", [BuildType.COMPOSE])
 includeProject(":lifecycle:lifecycle-viewmodel-compose:lifecycle-viewmodel-compose-samples", "lifecycle/lifecycle-viewmodel-compose/samples", [BuildType.COMPOSE])
 includeProject(":lifecycle:lifecycle-viewmodel-compose:integration-tests:lifecycle-viewmodel-demos", [BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-viewmodel-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-viewmodel-savedstate", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+includeProject(":lifecycle:lifecycle-viewmodel-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":lifecycle:lifecycle-viewmodel-savedstate", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
 includeProject(":lint-checks")
 includeProject(":lint-checks:integration-tests")
 includeProject(":loader:loader", [BuildType.MAIN])
@@ -795,6 +810,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])
@@ -912,6 +929,7 @@
 includeProject(":wear:benchmark:integration-tests:macrobenchmark-target", [BuildType.MAIN, BuildType.COMPOSE])
 includeProject(":wear:benchmark:integration-tests:macrobenchmark", [BuildType.MAIN, BuildType.COMPOSE])
 includeProject(":wear:compose:compose-foundation", [BuildType.COMPOSE])
+includeProject(":wear:compose:compose-foundation-benchmark", "wear/compose/compose-foundation/benchmark", [BuildType.COMPOSE])
 includeProject(":wear:compose:compose-foundation-samples", "wear/compose/compose-foundation/samples", [BuildType.COMPOSE])
 includeProject(":wear:compose:compose-material", [BuildType.COMPOSE])
 includeProject(":wear:compose:compose-material3", [BuildType.COMPOSE])
@@ -964,17 +982,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])
@@ -1022,7 +1040,7 @@
 includeProject(":internal-testutils-appcompat", "testutils/testutils-appcompat", [BuildType.MAIN])
 includeProject(":internal-testutils-espresso", "testutils/testutils-espresso", [BuildType.MAIN, BuildType.COMPOSE])
 includeProject(":internal-testutils-fonts", "testutils/testutils-fonts", [BuildType.MAIN, BuildType.GLANCE, BuildType.MEDIA, BuildType.FLAN, BuildType.COMPOSE, BuildType.WEAR])
-includeProject(":internal-testutils-truth", "testutils/testutils-truth", [BuildType.MAIN, BuildType.GLANCE, BuildType.MEDIA, BuildType.FLAN, BuildType.COMPOSE, BuildType.WEAR, BuildType.KMP, BuildType.CAMERA])
+includeProject(":internal-testutils-truth", "testutils/testutils-truth")
 includeProject(":internal-testutils-ktx", "testutils/testutils-ktx")
 includeProject(":internal-testutils-kmp", "testutils/testutils-kmp", [BuildType.MAIN, BuildType.KMP])
 includeProject(":internal-testutils-macrobenchmark", "testutils/testutils-macrobenchmark", [BuildType.MAIN, BuildType.COMPOSE])
diff --git a/slice/slice-view/src/androidTest/java/androidx/slice/SliceViewManagerTest.java b/slice/slice-view/src/androidTest/java/androidx/slice/SliceViewManagerTest.java
index 1c6b91b..ec1a3eb 100644
--- a/slice/slice-view/src/androidTest/java/androidx/slice/SliceViewManagerTest.java
+++ b/slice/slice-view/src/androidTest/java/androidx/slice/SliceViewManagerTest.java
@@ -36,6 +36,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ProviderInfo;
 import android.net.Uri;
+import android.os.Build;
 
 import androidx.annotation.NonNull;
 import androidx.slice.render.SliceRenderActivity;
@@ -110,8 +111,12 @@
     }
 
     @Test
-    @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
+    @SdkSuppress(minSdkVersion = 19, maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void testPinList() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         Uri uri = new Uri.Builder()
                 .scheme(ContentResolver.SCHEME_CONTENT)
                 .authority(mContext.getPackageName())
diff --git a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/util/ProcessLock.kt b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/util/ProcessLock.kt
index 3b79342..dc07eb9 100644
--- a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/util/ProcessLock.kt
+++ b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/util/ProcessLock.kt
@@ -52,12 +52,12 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 class ProcessLock(
     name: String,
-    lockDir: File,
+    lockDir: File?,
     private val processLock: Boolean
 ) {
-    private val lockFile: File = File(lockDir, "$name.lck")
+    private val lockFile: File? = lockDir?.let { File(it, "$name.lck") }
     @SuppressLint("SyntheticAccessor")
-    private val threadLock: Lock = getThreadLock(lockFile.absolutePath)
+    private val threadLock: Lock = getThreadLock(name)
     private var lockChannel: FileChannel? = null
 
     /**
@@ -69,6 +69,9 @@
         threadLock.lock()
         if (processLock) {
             try {
+                if (lockFile == null) {
+                    throw IOException("No lock directory was provided.")
+                }
                 // Verify parent dir
                 val parentDir = lockFile.parentFile
                 parentDir?.mkdirs()
diff --git a/startup/startup-runtime/src/main/java/androidx/startup/Initializer.java b/startup/startup-runtime/src/main/java/androidx/startup/Initializer.java
index 0641777..01ffacd 100644
--- a/startup/startup-runtime/src/main/java/androidx/startup/Initializer.java
+++ b/startup/startup-runtime/src/main/java/androidx/startup/Initializer.java
@@ -23,15 +23,16 @@
 import java.util.List;
 
 /**
- * {@link Initializer}s can be used to initialize libraries during app startup, without
- * the need to use additional {@link android.content.ContentProvider}s.
+ * Initializes library components during app startup.
  *
- * @param <T> The instance type being initialized
+ * Discovered and initialized by {@link InitializationProvider}.
+ *
+ * @param <T> The type of the component being initialized.
  */
 public interface Initializer<T> {
 
     /**
-     * Initializes and a component given the application {@link Context}
+     * Initializes a library component within the application {@link Context}.
      *
      * @param context The application context.
      */
@@ -39,11 +40,13 @@
     T create(@NonNull Context context);
 
     /**
-     * @return A list of dependencies that this {@link Initializer} depends on. This is
-     * used to determine initialization order of {@link Initializer}s.
-     * <br/>
-     * For e.g. if a {@link Initializer} `B` defines another
-     * {@link Initializer} `A` as its dependency, then `A` gets initialized before `B`.
+     * Gets a list of this initializer's dependencies.
+     *
+     * Dependencies are initialized before the dependent initializer. For
+     * example, if initializer A defines initializer B as a dependency, B is
+     * initialized before A.
+     *
+     * @return A list of initializer dependencies.
      */
     @NonNull
     List<Class<? extends Initializer<?>>> dependencies();
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..3f0f1f5 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,16 @@
 
 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.Build;
 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 +35,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 +88,14 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 31, maxSdkVersion = 33) // b/262909049: Failing on SDK 34
+    @SdkSuppress(minSdkVersion = 32)
     public void testMultiWindow_splitScreen() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         // 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 +104,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..58dab98 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
@@ -24,6 +24,7 @@
 
 import android.app.UiAutomation;
 import android.graphics.Point;
+import android.os.Build;
 import android.os.SystemClock;
 import android.view.KeyEvent;
 import android.widget.TextView;
@@ -37,6 +38,7 @@
 import androidx.test.uiautomator.UiObject2;
 import androidx.test.uiautomator.Until;
 
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
@@ -244,24 +246,15 @@
         assertEquals("keycode Z pressed with meta shift left on", textView.getText());
     }
 
+    @Ignore // b/266617096
     @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
@@ -308,6 +301,7 @@
         assertEquals("I've been clicked!", button.getText());
     }
 
+    @Ignore // b/266617096
     @Test
     public void testSwipe() {
         launchTestActivity(SwipeTestActivity.class);
@@ -337,6 +331,7 @@
         assertTrue(dragDestination.wait(Until.textEquals("drag_received"), TIMEOUT_MS));
     }
 
+    @Ignore // b/266617096
     @Test
     public void testSwipe_withPointArray() {
         launchTestActivity(SwipeTestActivity.class);
@@ -455,6 +450,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void testWaitForWindowUpdate() {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         launchTestActivity(WaitTestActivity.class);
 
         // Returns false when the current window doesn't have the specified package name.
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
index d34b1b3..bdcf9c0 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
@@ -35,6 +35,7 @@
 import androidx.test.uiautomator.UiObject2;
 import androidx.test.uiautomator.Until;
 
+import org.junit.Ignore;
 import org.junit.Test;
 
 import java.util.HashSet;
@@ -155,6 +156,7 @@
                 TIMEOUT_MS));
     }
 
+    @Ignore // b/266617335
     @Test
     @SdkSuppress(minSdkVersion = 24)
     public void testDrag_dest() {
@@ -518,6 +520,7 @@
                 + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch < 1f);
     }
 
+    @Ignore // b/266617335
     @Test
     public void testPinchOpen() {
         launchTestActivity(PinchTestActivity.class);
@@ -544,6 +547,7 @@
                 + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch > 1f);
     }
 
+    @Ignore // b/266617335
     @Test
     public void testSwipe() {
         launchTestActivity(SwipeTestActivity.class);
@@ -624,6 +628,7 @@
         assertTrue(mDevice.hasObject(By.res(TEST_APP, "bottom_text")));
     }
 
+    @Ignore // b/266617335
     @Test
     public void testFling_direction() {
         launchTestActivity(FlingTestActivity.class);
@@ -674,6 +679,7 @@
                 () -> flingRegion.fling(Direction.DOWN, speed));
     }
 
+    @Ignore // b/267208902
     @Test
     public void testSetGestureMargin() {
         launchTestActivity(PinchTestActivity.class);
@@ -704,6 +710,7 @@
                 + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch < 1f);
     }
 
+    @Ignore // b/266617335
     @Test
     public void testSetGestureMargins() {
         launchTestActivity(PinchTestActivity.class);
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
index 175b865..d186654 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
@@ -29,6 +29,7 @@
 import androidx.test.uiautomator.UiObject;
 import androidx.test.uiautomator.UiSelector;
 
+import org.junit.Ignore;
 import org.junit.Test;
 
 public class UiObjectTest extends BaseTest {
@@ -118,6 +119,7 @@
         assertTrue(expectedDragDest.waitForExists(TIMEOUT_MS));
     }
 
+    @Ignore // b/266617747
     @Test
     public void testSwipeUp() throws Exception {
         launchTestActivity(SwipeTestActivity.class);
@@ -139,6 +141,7 @@
         assertTrue(expectedSwipeRegion.waitForExists(TIMEOUT_MS));
     }
 
+    @Ignore // b/266617747
     @Test
     public void testSwipeDown() throws Exception {
         launchTestActivity(SwipeTestActivity.class);
@@ -178,6 +181,7 @@
         assertTrue(expectedSwipeRegion.waitForExists(TIMEOUT_MS));
     }
 
+    @Ignore // b/266617747
     @Test
     public void testSwipeRight() throws Exception {
         launchTestActivity(SwipeTestActivity.class);
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java
index 963f28b..0d7208d 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java
@@ -28,6 +28,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 
 public class UiScrollableTest extends BaseTest {
@@ -52,6 +53,7 @@
                 mDefaultSwipeDeadZonePercentage);
     }
 
+    @Ignore // b/266965027
     @Test
     public void testGetChildByDescription() throws Exception {
         launchTestActivity(VerticalScrollTestActivity.class);
@@ -69,6 +71,7 @@
                         "This is non-existent"));
     }
 
+    @Ignore // b/266965027
     @Test
     public void testGetChildByDescription_withoutScrollSearch() throws Exception {
         launchTestActivity(VerticalScrollTestActivity.class);
@@ -86,6 +89,7 @@
                         false));
     }
 
+    @Ignore // b/266965027
     @Test
     public void testGetChildByInstance() throws Exception {
         launchTestActivity(VerticalScrollTestActivity.class);
@@ -102,6 +106,7 @@
                 1).exists());
     }
 
+    @Ignore // b/266965027
     @Test
     public void testGetChildByText() throws Exception {
         launchTestActivity(VerticalScrollTestActivity.class);
@@ -118,6 +123,7 @@
                         "This is non-existent"));
     }
 
+    @Ignore // b/266965027
     @Test
     public void testGetChildByText_withoutScrollSearch() throws Exception {
         launchTestActivity(VerticalScrollTestActivity.class);
@@ -134,6 +140,7 @@
                         "This is the bottom", false));
     }
 
+    @Ignore // b/266965027
     @Test
     public void testScrollDescriptionIntoView() throws Exception {
         launchTestActivity(VerticalScrollTestActivity.class);
@@ -149,6 +156,7 @@
         assertFalse(relativeLayout.scrollDescriptionIntoView("This is non-existent"));
     }
 
+    @Ignore // b/266965027
     @Test
     public void testScrollIntoView_withUiObject() throws Exception {
         launchTestActivity(VerticalScrollTestActivity.class);
@@ -166,6 +174,7 @@
         assertFalse(relativeLayout.scrollIntoView(nonExistentTarget));
     }
 
+    @Ignore // b/266965027
     @Test
     public void testScrollIntoView_withUiSelector() throws Exception {
         launchTestActivity(VerticalScrollTestActivity.class);
@@ -181,6 +190,7 @@
         assertFalse(relativeLayout.scrollIntoView(nonExistentTarget));
     }
 
+    @Ignore // b/266965027
     @Test
     public void testEnsureFullyVisible() throws Exception {
         launchTestActivity(VerticalScrollTestActivity.class);
@@ -197,6 +207,7 @@
                         mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id/no_node"))));
     }
 
+    @Ignore // b/266965027
     @Test
     public void testScrollTextIntoView() throws Exception {
         launchTestActivity(VerticalScrollTestActivity.class);
@@ -212,12 +223,14 @@
         assertFalse(relativeLayout.scrollTextIntoView("This is non-existent"));
     }
 
+    @Ignore // b/266965027
     @Test
     public void testSetMaxSearchSwipesAndGetMaxSearchSwipes() {
         UiScrollable scrollable = new UiScrollable(new UiSelector()).setMaxSearchSwipes(5);
         assertEquals(5, scrollable.getMaxSearchSwipes());
     }
 
+    @Ignore // b/266965027
     @Test
     public void testFlingForward() throws Exception {
         launchTestActivity(FlingTestActivity.class);
@@ -235,6 +248,7 @@
         assertUiObjectNotFound(noNode::flingForward);
     }
 
+    @Ignore // b/266965027
     @Test
     public void testScrollForward_vertical() throws Exception {
         launchTestActivity(SwipeTestActivity.class);
@@ -246,6 +260,7 @@
         assertEquals("swipe_up", scrollRegion.getText());
     }
 
+    @Ignore // b/266965027
     @Test
     public void testScrollForward_horizontal() throws Exception {
         launchTestActivity(SwipeTestActivity.class);
@@ -257,6 +272,7 @@
         assertEquals("swipe_left", scrollRegion.getText());
     }
 
+    @Ignore // b/266965027
     @Test
     public void testFlingBackward() throws Exception {
         launchTestActivity(FlingTestActivity.class);
@@ -272,6 +288,7 @@
         assertUiObjectNotFound(noNode::flingBackward);
     }
 
+    @Ignore // b/266965027
     @Test
     public void testScrollBackward_vertical() throws Exception {
         launchTestActivity(SwipeTestActivity.class);
@@ -283,6 +300,7 @@
         assertEquals("swipe_down", scrollRegion.getText());
     }
 
+    @Ignore // b/266965027
     @Test
     public void testScrollBackward_horizontal() throws Exception {
         launchTestActivity(SwipeTestActivity.class);
@@ -294,6 +312,7 @@
         assertEquals("swipe_right", scrollRegion.getText());
     }
 
+    @Ignore // b/266965027
     @Test
     public void testScrollToBeginning_withSteps() throws Exception {
         launchTestActivity(VerticalScrollTestActivity.class);
@@ -309,6 +328,7 @@
         assertTrue(topText.exists());
     }
 
+    @Ignore // b/266965027
     @Test
     public void testScrollToBeginning_notEnoughSwipes_failed() throws Exception {
         launchTestActivity(VerticalScrollTestActivity.class);
@@ -324,6 +344,7 @@
         assertFalse(topText.exists());
     }
 
+    @Ignore // b/266965027
     @Test
     public void testScrollToBeginning() throws Exception {
         launchTestActivity(VerticalScrollTestActivity.class);
@@ -339,6 +360,7 @@
         assertTrue(topText.exists());
     }
 
+    @Ignore // b/266965027
     @Test
     public void testFlingToBeginning() throws Exception {
         launchTestActivity(FlingTestActivity.class);
@@ -350,6 +372,7 @@
         assertEquals("fling_up", flingRegion.getText());
     }
 
+    @Ignore // b/266965027
     @Test
     public void testScrollToEnd_withSteps() throws Exception {
         launchTestActivity(VerticalScrollTestActivity.class);
@@ -364,6 +387,7 @@
         assertTrue(bottomText.exists());
     }
 
+    @Ignore // b/266965027
     @Test
     public void testScrollToEnd_notEnoughSwipes_failed() throws Exception {
         launchTestActivity(VerticalScrollTestActivity.class);
@@ -378,6 +402,7 @@
         assertFalse(bottomText.exists());
     }
 
+    @Ignore // b/266965027
     @Test
     public void testScrollToEnd() throws Exception {
         launchTestActivity(VerticalScrollTestActivity.class);
@@ -392,6 +417,7 @@
         assertTrue(bottomText.exists());
     }
 
+    @Ignore // b/266965027
     @Test
     public void testFlingToEnd() throws Exception {
         launchTestActivity(FlingTestActivity.class);
@@ -403,6 +429,7 @@
         assertEquals("fling_down", flingRegion.getText());
     }
 
+    @Ignore // b/266965027
     @Test
     public void testSetSwipeDeadZonePercentageAndGetSwipeDeadZonePercentage() {
         UiScrollable scrollable =
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/test/uiautomator/uiautomator/api/current.txt b/test/uiautomator/uiautomator/api/current.txt
index 6cb3309..152c347 100644
--- a/test/uiautomator/uiautomator/api/current.txt
+++ b/test/uiautomator/uiautomator/api/current.txt
@@ -75,6 +75,10 @@
     method public androidx.test.uiautomator.BySelector textStartsWith(String);
   }
 
+  public interface Condition<T, U> {
+    method public U! apply(T!);
+  }
+
   public final class Configurator {
     method public long getActionAcknowledgmentTimeout();
     method public static androidx.test.uiautomator.Configurator getInstance();
@@ -101,15 +105,16 @@
     enum_constant public static final androidx.test.uiautomator.Direction UP;
   }
 
-  public abstract class EventCondition<U> {
+  public abstract class EventCondition<U> implements android.app.UiAutomation.AccessibilityEventFilter {
     ctor public EventCondition();
+    method public abstract U! getResult();
   }
 
   public interface IAutomationSupport {
     method public void sendStatus(int, android.os.Bundle);
   }
 
-  public abstract class SearchCondition<U> {
+  public abstract class SearchCondition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.Searchable,U> {
     ctor public SearchCondition();
   }
 
@@ -200,7 +205,7 @@
     method public boolean takeScreenshot(java.io.File);
     method public boolean takeScreenshot(java.io.File, float, int);
     method public void unfreezeRotation() throws android.os.RemoteException;
-    method public <U> U! wait(androidx.test.uiautomator.SearchCondition<U!>, long);
+    method public <U> U! wait(androidx.test.uiautomator.Condition<? super androidx.test.uiautomator.UiDevice,U!>, long);
     method public void waitForIdle();
     method public void waitForIdle(long);
     method public boolean waitForWindowUpdate(String?, long);
@@ -308,11 +313,10 @@
     method public void setText(String?);
     method public void swipe(androidx.test.uiautomator.Direction, float);
     method public void swipe(androidx.test.uiautomator.Direction, float, int);
-    method public <U> U! wait(androidx.test.uiautomator.UiObject2Condition<U!>, long);
-    method public <U> U! wait(androidx.test.uiautomator.SearchCondition<U!>, long);
+    method public <U> U! wait(androidx.test.uiautomator.Condition<? super androidx.test.uiautomator.UiObject2,U!>, long);
   }
 
-  public abstract class UiObject2Condition<U> {
+  public abstract class UiObject2Condition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.UiObject2,U> {
     ctor public UiObject2Condition();
   }
 
diff --git a/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt b/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt
index 6cb3309..152c347 100644
--- a/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt
+++ b/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt
@@ -75,6 +75,10 @@
     method public androidx.test.uiautomator.BySelector textStartsWith(String);
   }
 
+  public interface Condition<T, U> {
+    method public U! apply(T!);
+  }
+
   public final class Configurator {
     method public long getActionAcknowledgmentTimeout();
     method public static androidx.test.uiautomator.Configurator getInstance();
@@ -101,15 +105,16 @@
     enum_constant public static final androidx.test.uiautomator.Direction UP;
   }
 
-  public abstract class EventCondition<U> {
+  public abstract class EventCondition<U> implements android.app.UiAutomation.AccessibilityEventFilter {
     ctor public EventCondition();
+    method public abstract U! getResult();
   }
 
   public interface IAutomationSupport {
     method public void sendStatus(int, android.os.Bundle);
   }
 
-  public abstract class SearchCondition<U> {
+  public abstract class SearchCondition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.Searchable,U> {
     ctor public SearchCondition();
   }
 
@@ -200,7 +205,7 @@
     method public boolean takeScreenshot(java.io.File);
     method public boolean takeScreenshot(java.io.File, float, int);
     method public void unfreezeRotation() throws android.os.RemoteException;
-    method public <U> U! wait(androidx.test.uiautomator.SearchCondition<U!>, long);
+    method public <U> U! wait(androidx.test.uiautomator.Condition<? super androidx.test.uiautomator.UiDevice,U!>, long);
     method public void waitForIdle();
     method public void waitForIdle(long);
     method public boolean waitForWindowUpdate(String?, long);
@@ -308,11 +313,10 @@
     method public void setText(String?);
     method public void swipe(androidx.test.uiautomator.Direction, float);
     method public void swipe(androidx.test.uiautomator.Direction, float, int);
-    method public <U> U! wait(androidx.test.uiautomator.UiObject2Condition<U!>, long);
-    method public <U> U! wait(androidx.test.uiautomator.SearchCondition<U!>, long);
+    method public <U> U! wait(androidx.test.uiautomator.Condition<? super androidx.test.uiautomator.UiObject2,U!>, long);
   }
 
-  public abstract class UiObject2Condition<U> {
+  public abstract class UiObject2Condition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.UiObject2,U> {
     ctor public UiObject2Condition();
   }
 
diff --git a/test/uiautomator/uiautomator/api/restricted_current.txt b/test/uiautomator/uiautomator/api/restricted_current.txt
index 6cb3309..152c347 100644
--- a/test/uiautomator/uiautomator/api/restricted_current.txt
+++ b/test/uiautomator/uiautomator/api/restricted_current.txt
@@ -75,6 +75,10 @@
     method public androidx.test.uiautomator.BySelector textStartsWith(String);
   }
 
+  public interface Condition<T, U> {
+    method public U! apply(T!);
+  }
+
   public final class Configurator {
     method public long getActionAcknowledgmentTimeout();
     method public static androidx.test.uiautomator.Configurator getInstance();
@@ -101,15 +105,16 @@
     enum_constant public static final androidx.test.uiautomator.Direction UP;
   }
 
-  public abstract class EventCondition<U> {
+  public abstract class EventCondition<U> implements android.app.UiAutomation.AccessibilityEventFilter {
     ctor public EventCondition();
+    method public abstract U! getResult();
   }
 
   public interface IAutomationSupport {
     method public void sendStatus(int, android.os.Bundle);
   }
 
-  public abstract class SearchCondition<U> {
+  public abstract class SearchCondition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.Searchable,U> {
     ctor public SearchCondition();
   }
 
@@ -200,7 +205,7 @@
     method public boolean takeScreenshot(java.io.File);
     method public boolean takeScreenshot(java.io.File, float, int);
     method public void unfreezeRotation() throws android.os.RemoteException;
-    method public <U> U! wait(androidx.test.uiautomator.SearchCondition<U!>, long);
+    method public <U> U! wait(androidx.test.uiautomator.Condition<? super androidx.test.uiautomator.UiDevice,U!>, long);
     method public void waitForIdle();
     method public void waitForIdle(long);
     method public boolean waitForWindowUpdate(String?, long);
@@ -308,11 +313,10 @@
     method public void setText(String?);
     method public void swipe(androidx.test.uiautomator.Direction, float);
     method public void swipe(androidx.test.uiautomator.Direction, float, int);
-    method public <U> U! wait(androidx.test.uiautomator.UiObject2Condition<U!>, long);
-    method public <U> U! wait(androidx.test.uiautomator.SearchCondition<U!>, long);
+    method public <U> U! wait(androidx.test.uiautomator.Condition<? super androidx.test.uiautomator.UiObject2,U!>, long);
   }
 
-  public abstract class UiObject2Condition<U> {
+  public abstract class UiObject2Condition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.UiObject2,U> {
     ctor public UiObject2Condition();
   }
 
diff --git a/test/uiautomator/uiautomator/lint-baseline.xml b/test/uiautomator/uiautomator/lint-baseline.xml
index 1a52277..339bd83 100644
--- a/test/uiautomator/uiautomator/lint-baseline.xml
+++ b/test/uiautomator/uiautomator/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.4.0-alpha08" type="baseline" client="gradle" dependencies="false" name="AGP (7.4.0-alpha08)" variant="all" version="7.4.0-alpha08">
+<issues format="6" by="lint 8.0.0-alpha07" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-alpha07)" variant="all" version="8.0.0-alpha07">
 
     <issue
         id="BanUncheckedReflection"
@@ -12,6 +12,15 @@
 
     <issue
         id="LambdaLast"
+        message="Functional interface parameters (such as parameter 1, &quot;condition&quot;, in androidx.test.uiautomator.UiDevice.wait) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions"
+        errorLine1="    public &lt;U> U wait(@NonNull Condition&lt;? super UiDevice, U> condition, long timeout) {"
+        errorLine2="                                                                         ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/test/uiautomator/UiDevice.java"/>
+    </issue>
+
+    <issue
+        id="LambdaLast"
         message="Functional interface parameters (such as parameter 1, &quot;action&quot;, in androidx.test.uiautomator.UiDevice.performActionAndWait) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions"
         errorLine1="            @NonNull EventCondition&lt;U> condition, long timeout) {"
         errorLine2="                                                  ~~~~~~~~~~~~">
@@ -20,21 +29,12 @@
     </issue>
 
     <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected void initializeUiAutomatorTest(UiAutomatorTestCase test) {"
-        errorLine2="                                             ~~~~~~~~~~~~~~~~~~~">
+        id="LambdaLast"
+        message="Functional interface parameters (such as parameter 1, &quot;condition&quot;, in androidx.test.uiautomator.UiObject2.wait) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions"
+        errorLine1="    public &lt;U> U wait(@NonNull Condition&lt;? super UiObject2, U> condition, long timeout) {"
+        errorLine2="                                                                          ~~~~~~~~~~~~">
         <location
-            file="src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
-        errorLine1="    protected AndroidTestRunner getAndroidTestRunner() {"
-        errorLine2="              ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java"/>
+            file="src/main/java/androidx/test/uiautomator/UiObject2.java"/>
     </issue>
 
 </issues>
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Condition.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Condition.java
index 8958ddc..653d286 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Condition.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Condition.java
@@ -16,12 +16,12 @@
 
 package androidx.test.uiautomator;
 
-/** Abstract class which represents a condition to be satisfied. */
-abstract class Condition<T, U> {
+/** Represents a condition to be satisfied. */
+public interface Condition<T, U> {
 
     /**
      * Applies the given arguments against this condition. Returns a non-null, non-false result if
      * the condition is satisfied.
      */
-    abstract U apply(T args);
+    U apply(T args);
 }
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Configurator.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Configurator.java
index b082a80..6b7931d 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Configurator.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Configurator.java
@@ -35,9 +35,8 @@
     private long mWaitForSelector = 10 * 1000;
     private long mWaitForActionAcknowledgment = 3 * 1000;
 
-    // The events for a scroll typically complete even before touchUp occurs.
-    // This short timeout to make sure we get the very last in cases where the above isn't true.
-    private long mScrollEventWaitTimeout = 200; // ms
+    // Scroll timeout used only in InteractionController
+    private long mScrollEventWaitTimeout = 1_000; // ms
 
     // Default is inject as fast as we can
     private long mKeyInjectionDelay = 0; // ms
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/EventCondition.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/EventCondition.java
index 362e2b0..5d4cc69 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/EventCondition.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/EventCondition.java
@@ -16,14 +16,16 @@
 
 package androidx.test.uiautomator;
 
-import android.view.accessibility.AccessibilityEvent;
+import android.app.UiAutomation.AccessibilityEventFilter;
 
 /**
  * An {@link EventCondition} is a condition which depends on an event or series of events having
  * occurred.
  */
-public abstract class EventCondition<U> extends Condition<AccessibilityEvent, Boolean> {
+public abstract class EventCondition<U> implements AccessibilityEventFilter {
 
-    @SuppressWarnings("HiddenAbstractMethod")
-    abstract U getResult();
+    /**
+     * returns a value obtained after applying the condition to a series of events
+     */
+    public abstract U getResult();
 }
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
index 271926c..3d6eee1 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
@@ -36,8 +36,6 @@
 import android.view.ViewConfiguration;
 import android.view.accessibility.AccessibilityEvent;
 
-import java.util.ArrayList;
-import java.util.List;
 import java.util.concurrent.TimeoutException;
 
 /**
@@ -84,34 +82,6 @@
     }
 
     /**
-     * Predicate for waiting for all the events specified in the mask and populating
-     * a ctor passed list with matching events. User of this predicate must recycle
-     * all populated events in the events list.
-     */
-    static class EventCollectingPredicate implements AccessibilityEventFilter {
-        final int mMask;
-        final List<AccessibilityEvent> mEventsList;
-
-        EventCollectingPredicate(int mask, List<AccessibilityEvent> events) {
-            mMask = mask;
-            mEventsList = events;
-        }
-
-        @Override
-        public boolean accept(AccessibilityEvent t) {
-            // check current event in the list
-            if ((t.getEventType() & mMask) != 0) {
-                // For the events you need, always store a copy when returning false from
-                // predicates since the original will automatically be recycled after the call.
-                mEventsList.add(AccessibilityEvent.obtain(t));
-            }
-
-            // get more
-            return false;
-        }
-    }
-
-    /**
      * Predicate for waiting for every event specified in the mask to be matched at least once
      */
     static class WaitForAllEventPredicate implements AccessibilityEventFilter {
@@ -334,56 +304,21 @@
             final int steps) {
         Runnable command = () -> swipe(downX, downY, upX, upY, steps);
 
-        // Collect all accessibility events generated during the swipe command and get the
-        // last event
-        ArrayList<AccessibilityEvent> events = new ArrayList<>();
+        // Get scroll direction based on position.
+        Direction direction;
+        if (Math.abs(downX - upX) > Math.abs(downY - upY)) {
+            // Horizontal.
+            direction = downX > upX ? Direction.RIGHT : Direction.LEFT;
+        } else {
+            // Vertical.
+            direction = downY > upY ? Direction.DOWN : Direction.UP;
+        }
+        EventCondition<Boolean> condition = Until.scrollFinished(direction);
         runAndWaitForEvents(command,
-                new EventCollectingPredicate(AccessibilityEvent.TYPE_VIEW_SCROLLED, events),
+                condition,
                 Configurator.getInstance().getScrollAcknowledgmentTimeout());
 
-        AccessibilityEvent event = getLastMatchingEvent(events,
-                AccessibilityEvent.TYPE_VIEW_SCROLLED);
-
-        if (event == null) {
-            // end of scroll since no new scroll events received
-            recycleAccessibilityEvents(events);
-            return false;
-        }
-
-        // AdapterViews have indices we can use to check for the beginning.
-        boolean foundEnd = false;
-        if (event.getFromIndex() != -1 && event.getToIndex() != -1 && event.getItemCount() != -1) {
-            foundEnd = event.getFromIndex() == 0 ||
-                    (event.getItemCount() - 1) == event.getToIndex();
-        } else if (event.getScrollX() != -1 && event.getScrollY() != -1) {
-            // Determine if we are scrolling vertically or horizontally.
-            if (downX == upX) {
-                // Vertical
-                foundEnd = event.getScrollY() == 0 ||
-                        event.getScrollY() == event.getMaxScrollY();
-            } else if (downY == upY) {
-                // Horizontal
-                foundEnd = event.getScrollX() == 0 ||
-                        event.getScrollX() == event.getMaxScrollX();
-            }
-        }
-        recycleAccessibilityEvents(events);
-        return !foundEnd;
-    }
-
-    private AccessibilityEvent getLastMatchingEvent(List<AccessibilityEvent> events, int type) {
-        for (int x = events.size(); x > 0; x--) {
-            AccessibilityEvent event = events.get(x - 1);
-            if (event.getEventType() == type)
-                return event;
-        }
-        return null;
-    }
-
-    private void recycleAccessibilityEvents(List<AccessibilityEvent> events) {
-        for (AccessibilityEvent event : events)
-            event.recycle();
-        events.clear();
+        return !condition.getResult();
     }
 
     /**
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/SearchCondition.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/SearchCondition.java
index c3b2318..90761db 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/SearchCondition.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/SearchCondition.java
@@ -17,5 +17,5 @@
 package androidx.test.uiautomator;
 
 /** A {@link SearchCondition} is a condition that is satisfied by searching for UI elements. */
-public abstract class SearchCondition<U> extends Condition<Searchable, U> {
+public abstract class SearchCondition<U> implements Condition<Searchable, U> {
 }
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
index 1e5d3bd..786de295 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
@@ -163,12 +163,12 @@
     /**
      * Waits for given the {@code condition} to be met.
      *
-     * @param condition The {@link SearchCondition} to evaluate.
+     * @param condition The {@link Condition} to evaluate.
      * @param timeout Maximum amount of time to wait in milliseconds.
      * @return The final result returned by the {@code condition}, or null if the {@code condition}
      * was not met before the {@code timeout}.
      */
-    public <U> U wait(@NonNull SearchCondition<U> condition, long timeout) {
+    public <U> U wait(@NonNull Condition<? super UiDevice, U> condition, long timeout) {
         Log.d(TAG, String.format("Waiting %dms for %s.", timeout, condition));
         return mWaitMixin.wait(condition, timeout);
     }
@@ -188,7 +188,7 @@
                 condition));
         try {
             event = getUiAutomation().executeAndWaitForEvent(
-                action, new EventForwardingFilter(condition), timeout);
+                    action, condition, timeout);
         } catch (TimeoutException e) {
             // Ignore
             Log.w(TAG, String.format("Timed out waiting %dms on the condition.", timeout));
@@ -201,22 +201,6 @@
         return condition.getResult();
     }
 
-    /** Proxy class which acts as an {@link AccessibilityEventFilter} and forwards calls to an
-     * {@link EventCondition} instance. */
-    private static class EventForwardingFilter implements AccessibilityEventFilter {
-        private final EventCondition<?> mCondition;
-
-        public EventForwardingFilter(EventCondition<?> condition) {
-            mCondition = condition;
-        }
-
-        @Override
-        public boolean accept(AccessibilityEvent event) {
-            // Guard against nulls
-            return Boolean.TRUE.equals(mCondition.apply(event));
-        }
-    }
-
     /**
      * Enables or disables layout hierarchy compression.
      *
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
index 1bdd9dd..5fa391a 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
@@ -147,25 +147,12 @@
     /**
      * Waits for a {@code condition} to be met.
      *
-     * @param condition The {@link UiObject2Condition} to wait for.
+     * @param condition The {@link Condition} to evaluate.
      * @param timeout   The maximum time in milliseconds to wait for.
      * @return The final result returned by the {@code condition}, or {@code null} if the {@code
      * condition} was not met before the {@code timeout}.
      */
-    public <U> U wait(@NonNull UiObject2Condition<U> condition, long timeout) {
-        Log.d(TAG, String.format("Waiting %dms for %s.", timeout, condition));
-        return mWaitMixin.wait(condition, timeout);
-    }
-
-    /**
-     * Waits for a {@code condition} to be met.
-     *
-     * @param condition The {@link SearchCondition} to evaluate.
-     * @param timeout   The maximum time in milliseconds to wait for.
-     * @return The final result returned by the {@code condition}, or {@code null} if the {@code
-     * condition} was not met before the {@code timeout}.
-     */
-    public <U> U wait(@NonNull SearchCondition<U> condition, long timeout) {
+    public <U> U wait(@NonNull Condition<? super UiObject2, U> condition, long timeout) {
         Log.d(TAG, String.format("Waiting %dms for %s.", timeout, condition));
         return mWaitMixin.wait(condition, timeout);
     }
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2Condition.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2Condition.java
index d1fbbff..372f5db 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2Condition.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2Condition.java
@@ -20,5 +20,5 @@
  * A {@link UiObject2Condition} is a condition which is satisfied when a {@link UiObject2} is in a
  * particular state.
  */
-public abstract class UiObject2Condition<U> extends Condition<UiObject2, U> {
+public abstract class UiObject2Condition<U> implements Condition<UiObject2, U> {
 }
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
index e980a1c..e349d96 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
@@ -41,7 +41,7 @@
     public static SearchCondition<Boolean> gone(@NonNull BySelector selector) {
         return new SearchCondition<Boolean>() {
             @Override
-            Boolean apply(Searchable container) {
+            public Boolean apply(Searchable container) {
                 return !container.hasObject(selector);
             }
 
@@ -61,7 +61,7 @@
     public static SearchCondition<Boolean> hasObject(@NonNull BySelector selector) {
         return new SearchCondition<Boolean>() {
             @Override
-            Boolean apply(Searchable container) {
+            public Boolean apply(Searchable container) {
                 return container.hasObject(selector);
             }
 
@@ -81,7 +81,7 @@
     public static SearchCondition<UiObject2> findObject(@NonNull BySelector selector) {
         return new SearchCondition<UiObject2>() {
             @Override
-            UiObject2 apply(Searchable container) {
+            public UiObject2 apply(Searchable container) {
                 return container.findObject(selector);
             }
 
@@ -101,7 +101,7 @@
     public static SearchCondition<List<UiObject2>> findObjects(@NonNull BySelector selector) {
         return new SearchCondition<List<UiObject2>>() {
             @Override
-            List<UiObject2> apply(Searchable container) {
+            public List<UiObject2> apply(Searchable container) {
                 List<UiObject2> ret = container.findObjects(selector);
                 return ret.isEmpty() ? null : ret;
             }
@@ -126,7 +126,7 @@
     public static UiObject2Condition<Boolean> checkable(final boolean isCheckable) {
         return new UiObject2Condition<Boolean>() {
             @Override
-            Boolean apply(UiObject2 object) {
+            public Boolean apply(UiObject2 object) {
                 return object.isCheckable() == isCheckable;
             }
 
@@ -147,7 +147,7 @@
     public static UiObject2Condition<Boolean> checked(final boolean isChecked) {
         return new UiObject2Condition<Boolean>() {
             @Override
-            Boolean apply(UiObject2 object) {
+            public Boolean apply(UiObject2 object) {
                 return object.isChecked() == isChecked;
             }
 
@@ -168,7 +168,7 @@
     public static UiObject2Condition<Boolean> clickable(final boolean isClickable) {
         return new UiObject2Condition<Boolean>() {
             @Override
-            Boolean apply(UiObject2 object) {
+            public Boolean apply(UiObject2 object) {
                 return object.isClickable() == isClickable;
             }
 
@@ -189,7 +189,7 @@
     public static UiObject2Condition<Boolean> enabled(final boolean isEnabled) {
         return new UiObject2Condition<Boolean>() {
             @Override
-            Boolean apply(UiObject2 object) {
+            public Boolean apply(UiObject2 object) {
                 return object.isEnabled() == isEnabled;
             }
 
@@ -210,7 +210,7 @@
     public static UiObject2Condition<Boolean> focusable(final boolean isFocusable) {
         return new UiObject2Condition<Boolean>() {
             @Override
-            Boolean apply(UiObject2 object) {
+            public Boolean apply(UiObject2 object) {
                 return object.isFocusable() == isFocusable;
             }
 
@@ -231,7 +231,7 @@
     public static UiObject2Condition<Boolean> focused(final boolean isFocused) {
         return new UiObject2Condition<Boolean>() {
             @Override
-            Boolean apply(UiObject2 object) {
+            public Boolean apply(UiObject2 object) {
                 return object.isFocused() == isFocused;
             }
 
@@ -252,7 +252,7 @@
     public static UiObject2Condition<Boolean> longClickable(final boolean isLongClickable) {
         return new UiObject2Condition<Boolean>() {
             @Override
-            Boolean apply(UiObject2 object) {
+            public Boolean apply(UiObject2 object) {
                 return object.isLongClickable() == isLongClickable;
             }
 
@@ -273,7 +273,7 @@
     public static UiObject2Condition<Boolean> scrollable(final boolean isScrollable) {
         return new UiObject2Condition<Boolean>() {
             @Override
-            Boolean apply(UiObject2 object) {
+            public Boolean apply(UiObject2 object) {
                 return object.isScrollable() == isScrollable;
             }
 
@@ -294,7 +294,7 @@
     public static UiObject2Condition<Boolean> selected(final boolean isSelected) {
         return new UiObject2Condition<Boolean>() {
             @Override
-            Boolean apply(UiObject2 object) {
+            public Boolean apply(UiObject2 object) {
                 return object.isSelected() == isSelected;
             }
 
@@ -314,7 +314,7 @@
     public static UiObject2Condition<Boolean> descMatches(@NonNull Pattern regex) {
         return new UiObject2Condition<Boolean>() {
             @Override
-            Boolean apply(UiObject2 object) {
+            public Boolean apply(UiObject2 object) {
                 String desc = object.getContentDescription();
                 return regex.matcher(desc != null ? desc : "").matches();
             }
@@ -379,7 +379,7 @@
     public static UiObject2Condition<Boolean> textMatches(@NonNull Pattern regex) {
         return new UiObject2Condition<Boolean>() {
             @Override
-            Boolean apply(UiObject2 object) {
+            public Boolean apply(UiObject2 object) {
                 String text = object.getText();
                 return regex.matcher(text != null ? text : "").matches();
             }
@@ -408,7 +408,7 @@
     public static UiObject2Condition<Boolean> textNotEquals(@NonNull String text) {
         return new UiObject2Condition<Boolean>() {
             @Override
-            Boolean apply(UiObject2 object) {
+            public Boolean apply(UiObject2 object) {
                 return !text.equals(object.getText());
             }
 
@@ -467,13 +467,13 @@
                     AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
 
             @Override
-            Boolean apply(AccessibilityEvent event) {
+            public boolean accept(AccessibilityEvent event) {
                 mMask &= ~event.getEventType();
                 return mMask == 0;
             }
 
             @Override
-            Boolean getResult() {
+            public Boolean getResult() {
                 return mMask == 0;
             }
 
@@ -497,7 +497,7 @@
             private Boolean mResult = null;
 
             @Override
-            Boolean apply(AccessibilityEvent event) {
+            public boolean accept(AccessibilityEvent event) {
                 if (event.getEventType() != AccessibilityEvent.TYPE_VIEW_SCROLLED) {
                     return false; // Ignore non-scrolling events.
                 }
@@ -540,7 +540,7 @@
             }
 
             @Override
-            Boolean getResult() {
+            public Boolean getResult() {
                 // If we didn't recieve any scroll events (mResult == null), assume we're already at
                 // the end and return true.
                 return mResult == null || mResult;
diff --git a/testutils/testutils-gradle-plugin/src/main/java/androidx/testutils/gradle/ProjectSetupRule.kt b/testutils/testutils-gradle-plugin/src/main/java/androidx/testutils/gradle/ProjectSetupRule.kt
index 23fc19d..7bd12a1c 100644
--- a/testutils/testutils-gradle-plugin/src/main/java/androidx/testutils/gradle/ProjectSetupRule.kt
+++ b/testutils/testutils-gradle-plugin/src/main/java/androidx/testutils/gradle/ProjectSetupRule.kt
@@ -28,8 +28,8 @@
  *
  * It should be used along side with SdkResourceGenerator in your build.gradle file
  */
-class ProjectSetupRule : ExternalResource() {
-    val testProjectDir = TemporaryFolder()
+class ProjectSetupRule(parentFolder: File? = null) : ExternalResource() {
+    val testProjectDir = TemporaryFolder(parentFolder)
 
     val props: ProjectProps by lazy { ProjectProps.load() }
 
diff --git a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/FailingOrdered.kt b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/FailingOrdered.kt
new file mode 100644
index 0000000..559e0e6
--- /dev/null
+++ b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/FailingOrdered.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.kruth
+
+import kotlin.test.fail
+
+/**
+ * Always fails with the provided error message.
+ */
+internal class FailingOrdered(
+    private val message: () -> String,
+) : Ordered {
+
+    override fun inOrder() {
+        fail(message())
+    }
+}
\ No newline at end of file
diff --git a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/IterableSubject.kt b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/IterableSubject.kt
index f366d55..b255881 100644
--- a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/IterableSubject.kt
+++ b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/IterableSubject.kt
@@ -151,6 +151,100 @@
         containsAnyIn(requireNonNull(expected).asList())
     }
 
+    fun containsAtLeast(
+        firstExpected: Any?,
+        secondExpected: Any?,
+        vararg restOfExpected: Any?,
+    ): Ordered =
+        containsAtLeastElementsIn(listOf(firstExpected, secondExpected, *restOfExpected))
+
+    fun containsAtLeastElementsIn(expected: Iterable<*>?): Ordered {
+        requireNonNull(expected)
+        val actualList = requireNonNull(actual).toMutableList()
+
+        val missing = ArrayList<Any?>()
+        val actualNotInOrder = ArrayList<Any?>()
+
+        var ordered = true
+        // step through the expected elements...
+        for (e in expected) {
+            val index = actualList.indexOf(e)
+            if (index != -1) { // if we find the element in the actual list...
+                // drain all the elements that come before that element into actualNotInOrder
+                repeat(index) {
+                    actualNotInOrder += actualList.removeAt(0)
+                }
+
+                // and remove the element from the actual list
+                actualList.removeAt(0)
+            } else { // otherwise try removing it from actualNotInOrder...
+                if (actualNotInOrder.remove(e)) {
+                    // if it was in actualNotInOrder, we're not in order
+                    ordered = false
+                } else {
+                    // if it's not in actualNotInOrder, we're missing an expected element
+                    missing.add(e)
+                }
+            }
+        }
+
+        // if we have any missing expected elements, fail
+        if (missing.isNotEmpty()) {
+            val nearMissing = actualList.retainMatchingToString(missing)
+
+            fail(
+                """
+                    Expected to contain at least $expected, but did not.
+                    Missing $missing, though it did contain $nearMissing.
+                """.trimIndent()
+            )
+        }
+
+        if (ordered) {
+            return NoopOrdered
+        }
+
+        return FailingOrdered {
+            buildString {
+                append("Required elements were all found, but order was wrong.")
+                append("Expected order: $expected.")
+
+                if (actualList.any { it !in expected }) {
+                    append("Actual order: $actualList.")
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks that the actual iterable contains at least all of the expected elements or fails. If
+     * an element appears more than once in the expected elements then it must appear at least that
+     * number of times in the actual elements.
+     *
+     *
+     * To also test that the contents appear in the given order, make a call to `inOrder()`
+     * on the object returned by this method. The expected elements must appear in the given order
+     * within the actual elements, but they are not required to be consecutive.
+     */
+    fun containsAtLeastElementsIn(expected: Array<out Any?>?): Ordered =
+        containsAtLeastElementsIn(expected?.asList())
+
+    /**
+     * Checks that a subject contains exactly the provided objects or fails.
+     *
+     * Multiplicity is respected. For example, an object duplicated exactly 3 times in the
+     * parameters asserts that the object must likewise be duplicated exactly 3 times in the subject.
+     *
+     * To also test that the contents appear in the given order, make a call to [Ordered.inOrder]
+     * on the object returned by this method.
+     *
+     * To test that the iterable contains the same elements as an array, prefer
+     * [containsExactlyElementsIn]. It makes clear that the given array is a list of elements, not
+     * an element itself. This helps human readers and avoids a compiler warning.
+     */
+    fun containsExactly(vararg expected: Any?): Ordered =
+        containsExactlyElementsIn(expected.asList())
+
     /**
      * Checks that the subject contains exactly the provided objects or fails.
      *
@@ -233,16 +327,13 @@
                      * This containsExactly() call is a success. But the iterables were not in the same order,
                      * so return an object that will fail the test if the user calls inOrder().
                      */
-                    return object : Ordered {
-                        override fun inOrder() {
-                            fail(
-                                """
-                                    Contents match. Expected the order to also match, but was not.
-                                    Expected: $required.
-                                    Actual: $actual.
-                                """.trimIndent()
-                            )
-                        }
+
+                    return FailingOrdered {
+                        """
+                             Contents match. Expected the order to also match, but was not.
+                             Expected: $required.
+                             Actual: $actual.
+                        """.trimIndent()
                     }
                 }
 
@@ -290,4 +381,142 @@
         // order, so inOrder() can just succeed.
         return NoopOrdered
     }
+
+    /**
+     * Checks that a subject contains exactly the [expected] objects or fails.
+     *
+     *
+     * Multiplicity is respected. For example, an object duplicated exactly 3 times in the array
+     * parameter asserts that the object must likewise be duplicated exactly 3 times in the subject.
+     *
+     *
+     * To also test that the contents appear in the given order, make a call to `inOrder()`
+     * on the object returned by this method.
+     */
+    fun containsExactlyElementsIn(expected: Array<out Any?>?): Ordered =
+        containsExactlyElementsIn(expected?.asList())
+
+    /**
+     * Checks that a actual iterable contains none of the excluded objects or fails. (Duplicates are
+     * irrelevant to this test, which fails if any of the actual elements equal any of the excluded)
+     */
+    fun containsNoneOf(
+        firstExcluded: Any?,
+        secondExcluded: Any?,
+        vararg restOfExcluded: Any?,
+    ) {
+        containsNoneIn(listOf(firstExcluded, secondExcluded, *restOfExcluded))
+    }
+
+    /**
+     * Checks that the actual iterable contains none of the elements contained in the [excluded]
+     * iterable or fails. (Duplicates are irrelevant to this test, which fails if any of the actual
+     * elements equal any of the excluded)
+     */
+    fun containsNoneIn(excluded: Iterable<*>?) {
+        requireNonNull(excluded)
+        val actual = requireNonNull(actual).toSet()
+        val present = excluded.intersect(actual)
+
+        if (present.isNotEmpty()) {
+            fail(
+                """
+                    Expected not to contain any of $excluded but contained $present.
+                    Actual: $actual.
+                """.trimIndent()
+            )
+        }
+    }
+
+    /**
+     * Checks that the actual iterable contains none of the elements contained in the [excluded]
+     * array or fails. (Duplicates are irrelevant to this test, which fails if any of the actual
+     * elements equal any of the excluded)
+     */
+    fun containsNoneIn(excluded: Array<Any?>?) {
+        containsNoneIn(excluded?.asList())
+    }
+
+    /**
+     * Fails if the iterable is not strictly ordered, according to the natural ordering of its
+     * elements. Strictly ordered means that each element in the iterable is <i>strictly</i> greater
+     * than the element that preceded it.
+     *
+     * @throws ClassCastException if any pair of elements is not mutually Comparable
+     * @throws NullPointerException if any element is null
+     */
+    fun isInStrictOrder() {
+        isInStrictOrder(compareBy<Comparable<Any>> { it })
+    }
+
+    /**
+     * Fails if the iterable is not strictly ordered, according to the given [comparator]. Strictly
+     * ordered means that each element in the iterable is *strictly* greater than the element
+     * that preceded it.
+     *
+     * Note: star-projection in `Comparator<*>` is for compatibility with Truth.
+     *
+     * @throws ClassCastException if any pair of elements is not mutually Comparable
+     */
+    fun isInStrictOrder(comparator: Comparator<*>?) {
+        @Suppress("UNCHECKED_CAST")
+        val cmp = requireNonNull(comparator) as Comparator<in Any?>
+
+        verifyInOrder(
+            predicate = { a, b -> cmp.compare(a, b) < 0 },
+            message = { a, b ->
+                """
+                    Expected to be in strict order but contained $a followed by $b.
+                    Actual: $actual.
+                """.trimIndent()
+            }
+        )
+    }
+
+    /**
+     * Fails if the iterable is not ordered, according to the natural ordering of its elements.
+     * Ordered means that each element in the iterable is greater than or equal to the element that
+     * preceded it.
+     *
+     * @throws ClassCastException if any pair of elements is not mutually Comparable
+     * @throws NullPointerException if any element is null
+     */
+    fun isInOrder() {
+        isInOrder(compareBy<Comparable<Any>> { it })
+    }
+
+    /**
+     * Fails if the iterable is not ordered, according to the given [comparator]. Ordered means that
+     * each element in the iterable is greater than or equal to the element that preceded it.
+     *
+     * @throws ClassCastException if any pair of elements is not mutually Comparable
+     */
+    fun isInOrder(comparator: Comparator<*>?) {
+        @Suppress("UNCHECKED_CAST")
+        val cmp = requireNonNull(comparator) as Comparator<in Any?>
+
+        verifyInOrder(
+            predicate = { a, b -> cmp.compare(a, b) <= 0 },
+            message = { a, b ->
+                """
+                    Expected to be in order but contained $a followed by $b.
+                    Actual: $actual.
+                """.trimIndent()
+            }
+        )
+    }
+
+    private inline fun verifyInOrder(
+        predicate: (a: Any?, b: Any?) -> Boolean,
+        message: (a: Any?, b: Any?) -> String,
+    ) {
+        requireNonNull(actual)
+            .asSequence()
+            .zipWithNext(::Pair)
+            .forEach { (a, b) ->
+                if (!predicate(a, b)) {
+                    fail(message(a, b))
+                }
+            }
+    }
 }
diff --git a/testutils/testutils-kmp/src/commonTest/kotlin/androidx/kruth/IterableSubjectTest.kt b/testutils/testutils-kmp/src/commonTest/kotlin/androidx/kruth/IterableSubjectTest.kt
index 60ecd75..e08ecda 100644
--- a/testutils/testutils-kmp/src/commonTest/kotlin/androidx/kruth/IterableSubjectTest.kt
+++ b/testutils/testutils-kmp/src/commonTest/kotlin/androidx/kruth/IterableSubjectTest.kt
@@ -192,378 +192,349 @@
         }
     }
 
-//    @Test
-//    fun iterableContainsAtLeast() {
-//        assertThat(listOf(1, 2, 3)).containsAtLeast(1, 2)
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastWithMany() {
-//        assertThat(listOf(1, 2, 3)).containsAtLeast(1, 2)
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastWithDuplicates() {
-//        assertThat(listOf(1, 2, 2, 2, 3)).containsAtLeast(2, 2)
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastWithNull() {
-//        assertThat(listOf(1, null, 3)).containsAtLeast(3, null as Int?)
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastWithNullAtThirdAndFinalPosition() {
-//        assertThat(listOf(1, null, 3)).containsAtLeast(1, 3, null as Any?)
-//    }
-//
-//    /*
-//   * Test that we only call toString() if the assertion fails -- that is, not just if the elements
-//   * are out of order, but only if someone actually calls inOrder(). There are 2 reasons for this:
-//   *
-//   * 1. Calling toString() uses extra time and space. (To be fair, Iterable assertions often use a
-//   * lot of those already.)
-//   *
-//   * 2. Some toString() methods are buggy. Arguably we shouldn't accommodate these, especially since
-//   * those users are in for a nasty surprise if their tests actually fail someday, but I don't want
-//   * to bite that off now. (Maybe Fact should catch exceptions from toString()?)
-//   */
-//    @Test
-//    fun iterableContainsAtLeastElementsInOutOfOrderDoesNotStringify() {
-//        val o = CountsToStringCalls()
-//        val actual: List<Any> = listOf(o, 1)
-//        val expected: List<Any> = listOf(1, o)
-//        assertThat(actual).containsAtLeastElementsIn(expected)
-//        assertThat(o.calls).isEqualTo(0)
-//        expectFailureWhenTestingThat(actual).containsAtLeastElementsIn(expected).inOrder()
-//        assertThat(o.calls).isGreaterThan(0)
-//    }
+    @Test
+    fun iterableContainsAtLeast() {
+        assertThat(listOf(1, 2, 3)).containsAtLeast(1, 2)
+    }
 
-//    @Test
-//    fun iterableContainsAtLeastFailure() {
-//        expectFailureWhenTestingThat(listOf(1, 2, 3)).containsAtLeast(1, 2, 4)
-//        assertFailureKeys("missing (1)", "---", "expected to contain at least", "but was")
-//        assertFailureValue("missing (1)", "4")
-//        assertFailureValue("expected to contain at least", "[1, 2, 4]")
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastWithExtras() {
-//        expectFailureWhenTestingThat(listOf("y", "x")).containsAtLeast("x", "y", "z")
-//        assertFailureValue("missing (1)", "z")
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastWithExtraCopiesOfOutOfOrder() {
-//        expectFailureWhenTestingThat(listOf("y", "x")).containsAtLeast("x", "y", "y")
-//        assertFailureValue("missing (1)", "y")
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastWithDuplicatesFailure() {
-//        expectFailureWhenTestingThat(listOf(1, 2, 3)).containsAtLeast(1, 2, 2, 2, 3, 4)
-//        assertFailureValue("missing (3)", "2 [2 copies], 4")
-//    }
-//
-//    /*
-//   * Slightly subtle test to ensure that if multiple equal elements are found
-//   * to be missing we only reference it once in the output message.
-//   */
-//    @Test
-//    fun iterableContainsAtLeastWithDuplicateMissingElements() {
-//        expectFailureWhenTestingThat(listOf(1, 2)).containsAtLeast(4, 4, 4)
-//        assertFailureValue("missing (3)", "4 [3 copies]")
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastWithNullFailure() {
-//        expectFailureWhenTestingThat(listOf(1, null, 3)).containsAtLeast(1, null, null, 3)
-//        assertFailureValue("missing (1)", "null")
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastFailsWithSameToStringAndHomogeneousList() {
-//        expectFailureWhenTestingThat(listOf(1L, 2L)).containsAtLeast(1, 2)
-//        assertFailureValue("missing (2)", "1, 2 (java.lang.Integer)")
-//        assertFailureValue("though it did contain (2)", "1, 2 (java.lang.Long)")
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastFailsWithSameToStringAndHomogeneousListWithDuplicates() {
-//        expectFailureWhenTestingThat(listOf(1L, 2L, 2L)).containsAtLeast(1, 1, 2)
-//        assertFailureValue("missing (3)", "1 [2 copies], 2 (java.lang.Integer)")
-//        assertFailureValue("though it did contain (3)", "1, 2 [2 copies] (java.lang.Long)")
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastFailsWithSameToStringAndHomogeneousListWithNull() {
-//        expectFailureWhenTestingThat(listOf("null", "abc")).containsAtLeast("abc", null)
-//        assertFailureValue("missing (1)", "null (null type)")
-//        assertFailureValue("though it did contain (1)", "null (java.lang.String)")
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastFailsWithSameToStringAndHeterogeneousListWithDuplicates() {
-//        expectFailureWhenTestingThat(listOf(1, 2, 2L, 3L, 3L)).containsAtLeast(2L, 2L, 3, 3)
-//        assertFailureValue("missing (3)", "2 (java.lang.Long), 3 (java.lang.Integer) [2 copies]")
-//        assertFailureValue(
-//            "though it did contain (3)", "2 (java.lang.Integer), 3 (java.lang.Long) [2 copies]"
-//        )
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastFailsWithEmptyString() {
-//        expectFailureWhenTestingThat(listOf("a", null)).containsAtLeast("", null)
-//        assertFailureKeys("missing (1)", "---", "expected to contain at least", "but was")
-//        assertFailureValue("missing (1)", "")
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastInOrder() {
-//        assertThat(listOf(3, 2, 5)).containsAtLeast(3, 2, 5).inOrder()
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastInOrderWithGaps() {
-//        assertThat(listOf(3, 2, 5)).containsAtLeast(3, 5).inOrder()
-//        assertThat(listOf(3, 2, 2, 4, 5)).containsAtLeast(3, 2, 2, 5).inOrder()
-//        assertThat(listOf(3, 1, 4, 1, 5)).containsAtLeast(3, 1, 5).inOrder()
-//        assertThat(listOf("x", "y", "y", "z")).containsAtLeast("x", "y", "z").inOrder()
-//        assertThat(listOf("x", "x", "y", "z")).containsAtLeast("x", "y", "z").inOrder()
-//        assertThat(listOf("z", "x", "y", "z")).containsAtLeast("x", "y", "z").inOrder()
-//        assertThat(listOf("x", "x", "y", "z", "x")).containsAtLeast("x", "y", "z", "x").inOrder()
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastInOrderWithNull() {
-//        assertThat(listOf(3, null, 5)).containsAtLeast(3, null, 5).inOrder()
-//        assertThat(listOf(3, null, 7, 5)).containsAtLeast(3, null, 5).inOrder()
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastInOrderWithFailure() {
-//        expectFailureWhenTestingThat(listOf(1, null, 3)).containsAtLeast(null, 1, 3).inOrder()
-//        assertFailureKeys(
-//            "required elements were all found, but order was wrong",
-//            "expected order for required elements",
-//            "but was"
-//        )
-//        assertFailureValue("expected order for required elements", "[null, 1, 3]")
-//        assertFailureValue("but was", "[1, null, 3]")
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastInOrderWithFailureWithActualOrder() {
-//        expectFailureWhenTestingThat(listOf(1, 2, null, 3, 4)).containsAtLeast(null, 1, 3).inOrder()
-//        assertFailureKeys(
-//            "required elements were all found, but order was wrong",
-//            "expected order for required elements",
-//            "but order was",
-//            "full contents"
-//        )
-//        assertFailureValue("expected order for required elements", "[null, 1, 3]")
-//        assertFailureValue("but order was", "[1, null, 3]")
-//        assertFailureValue("full contents", "[1, 2, null, 3, 4]")
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastInOrderWithOneShotIterable() {
-//        val iterable: Iterable<Any> =
-//            Arrays.< Object > asList < kotlin . Any ? > 2, 1, null, 4, "a", 3, "b")
-//        val iterator = iterable.iterator()
-//        val oneShot: Iterable<Any> = object : Iterable<Any?> {
-//            override fun iterator(): Iterator<Any> {
-//                return iterator
-//            }
-//
-//            override fun toString(): String {
-//                return Iterables.toString(iterable)
-//            }
-//        }
-//        assertThat(oneShot).containsAtLeast(1, null, 3).inOrder()
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastInOrderWithOneShotIterableWrongOrder() {
-//        val iterator: Iterator<Any> = listOf(2 as Any, 1, null, 4, "a", 3, "b").iterator()
-//        val iterable: Iterable<Any> = object : Iterable<Any?> {
-//            override fun iterator(): Iterator<Any> {
-//                return iterator
-//            }
-//
-//            override fun toString(): String {
-//                return "BadIterable"
-//            }
-//        }
-//        expectFailureWhenTestingThat(iterable).containsAtLeast(1, 3, null as Any?).inOrder()
-//        assertFailureKeys(
-//            "required elements were all found, but order was wrong",
-//            "expected order for required elements",
-//            "but was"
-//        )
-//        assertFailureValue("expected order for required elements", "[1, 3, null]")
-//        assertFailureValue("but was", "BadIterable") // TODO(b/231966021): Output its elements.
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastInOrderWrongOrderAndMissing() {
-//        expectFailureWhenTestingThat(listOf(1, 2)).containsAtLeast(2, 1, 3).inOrder()
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastElementsInIterable() {
-//        assertThat(listOf(1, 2, 3)).containsAtLeastElementsIn(listOf(1, 2))
-//        expectFailureWhenTestingThat(listOf(1, 2, 3)).containsAtLeastElementsIn(listOf(1, 2, 4))
-//        assertFailureKeys("missing (1)", "---", "expected to contain at least", "but was")
-//        assertFailureValue("missing (1)", "4")
-//        assertFailureValue("expected to contain at least", "[1, 2, 4]")
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastElementsInCanUseFactPerElement() {
-//        expectFailureWhenTestingThat(listOf("abc"))
-//            .containsAtLeastElementsIn(listOf("123\n456", "789"))
-//        assertFailureKeys(
-//            "missing (2)",
-//            "#1",
-//            "#2",
-//            "---",
-//            "expected to contain at least",
-//            "but was"
-//        )
-//        assertFailureValue("#1", "123\n456")
-//        assertFailureValue("#2", "789")
-//    }
-//
-//    @Test
-//    fun iterableContainsAtLeastElementsInArray() {
-//        assertThat(listOf(1, 2, 3)).containsAtLeastElementsIn(arrayOf(1, 2))
-//        expectFailureWhenTestingThat(listOf(1, 2, 3))
-//            .containsAtLeastElementsIn(arrayOf(1, 2, 4))
-//        assertFailureKeys("missing (1)", "---", "expected to contain at least", "but was")
-//        assertFailureValue("missing (1)", "4")
-//        assertFailureValue("expected to contain at least", "[1, 2, 4]")
-//    }
-//
-//    @Test
-//    fun iterableContainsNoneOf() {
-//        assertThat(listOf(1, 2, 3)).containsNoneOf(4, 5, 6)
-//    }
-//
-//    @Test
-//    fun iterableContainsNoneOfFailure() {
-//        expectFailureWhenTestingThat(listOf(1, 2, 3)).containsNoneOf(1, 2, 4)
-//        assertFailureKeys("expected not to contain any of", "but contained", "full contents")
-//        assertFailureValue("expected not to contain any of", "[1, 2, 4]")
-//        assertFailureValue("but contained", "[1, 2]")
-//        assertFailureValue("full contents", "[1, 2, 3]")
-//    }
-//
-//    @Test
-//    fun iterableContainsNoneOfFailureWithDuplicateInSubject() {
-//        expectFailureWhenTestingThat(listOf(1, 2, 2, 3)).containsNoneOf(1, 2, 4)
-//        assertFailureValue("but contained", "[1, 2]")
-//    }
-//
-//    @Test
-//    fun iterableContainsNoneOfFailureWithDuplicateInExpected() {
-//        expectFailureWhenTestingThat(listOf(1, 2, 3)).containsNoneOf(1, 2, 2, 4)
-//        assertFailureValue("but contained", "[1, 2]")
-//    }
-//
-//    @Test
-//    fun iterableContainsNoneOfFailureWithEmptyString() {
-//        expectFailureWhenTestingThat(listOf("")).containsNoneOf("", null)
-//        assertFailureKeys("expected not to contain any of", "but contained", "full contents")
-//        assertFailureValue("expected not to contain any of", "[\"\" (empty String), null]")
-//        assertFailureValue("but contained", "[\"\" (empty String)]")
-//        assertFailureValue("full contents", "[]")
-//    }
-//
-//    @Test
-//    fun iterableContainsNoneInIterable() {
-//        assertThat(listOf(1, 2, 3)).containsNoneIn(listOf(4, 5, 6))
-//        expectFailureWhenTestingThat(listOf(1, 2, 3)).containsNoneIn(listOf(1, 2, 4))
-//        assertFailureKeys("expected not to contain any of", "but contained", "full contents")
-//        assertFailureValue("expected not to contain any of", "[1, 2, 4]")
-//        assertFailureValue("but contained", "[1, 2]")
-//        assertFailureValue("full contents", "[1, 2, 3]")
-//    }
-//
-//    @Test
-//    fun iterableContainsNoneInArray() {
-//        assertThat(listOf(1, 2, 3)).containsNoneIn(arrayOf(4, 5, 6))
-//        expectFailureWhenTestingThat(listOf(1, 2, 3)).containsNoneIn(arrayOf(1, 2, 4))
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyArray() {
-//        val stringArray = arrayOf("a", "b")
-//        val iterable: ImmutableList<Array<String>> = listOf(stringArray)
-//        // This test fails w/o the explicit cast
-//        assertThat(iterable).containsExactly(stringArray as Any)
-//    }
-//
-//    @Test
-//    fun arrayContainsExactly() {
-//        val iterable: ImmutableList<String> = listOf("a", "b")
-//        val array = arrayOf("a", "b")
-//        assertThat(iterable).containsExactly(array as Array<Any>)
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithMany() {
-//        assertThat(listOf(1, 2, 3)).containsExactly(1, 2, 3)
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyOutOfOrder() {
-//        assertThat(listOf(1, 2, 3, 4)).containsExactly(3, 1, 4, 2)
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithDuplicates() {
-//        assertThat(listOf(1, 2, 2, 2, 3)).containsExactly(1, 2, 2, 2, 3)
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithDuplicatesOutOfOrder() {
-//        assertThat(listOf(1, 2, 2, 2, 3)).containsExactly(2, 1, 2, 3, 2)
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithOnlyNullPassedAsNullArray() {
-//        // Truth is tolerant of this erroneous varargs call.
-//        val actual: Iterable<Any> = listOf(null as Any?)
-//        assertThat(actual).containsExactly(null as Array<Any?>?)
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithOnlyNull() {
-//        val actual: Iterable<Any> = listOf(null as Any?)
-//        assertThat(actual).containsExactly(null as Any?)
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithNullSecond() {
-//        assertThat(listOf(1, null)).containsExactly(1, null)
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithNullThird() {
-//        assertThat(listOf(1, 2, null)).containsExactly(1, 2, null)
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithNull() {
-//        assertThat(listOf(1, null, 3)).containsExactly(1, null, 3)
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithNullOutOfOrder() {
-//        assertThat(listOf(1, null, 3)).containsExactly(1, 3, null as Int?)
-//    }
-//
+    @Test
+    fun iterableContainsAtLeastWithMany() {
+        assertThat(listOf(1, 2, 3)).containsAtLeast(1, 2)
+    }
+
+    @Test
+    fun iterableContainsAtLeastWithDuplicates() {
+        assertThat(listOf(1, 2, 2, 2, 3)).containsAtLeast(2, 2)
+    }
+
+    @Test
+    fun iterableContainsAtLeastWithNull() {
+        assertThat(listOf(1, null, 3)).containsAtLeast(3, null as Int?)
+    }
+
+    @Test
+    fun iterableContainsAtLeastWithNullAtThirdAndFinalPosition() {
+        assertThat(listOf(1, null, 3)).containsAtLeast(1, 3, null as Any?)
+    }
+
+    /*
+     * Test that we only call toString() if the assertion fails -- that is, not just if the elements
+     * are out of order, but only if someone actually calls inOrder(). There are 2 reasons for this:
+     *
+     * 1. Calling toString() uses extra time and space. (To be fair, Iterable assertions often use a
+     * lot of those already.)
+     *
+     * 2. Some toString() methods are buggy. Arguably we shouldn't accommodate these, especially since
+     * those users are in for a nasty surprise if their tests actually fail someday, but I don't want
+     * to bite that off now. (Maybe Fact should catch exceptions from toString()?)
+     */
+    @Test
+    fun iterableContainsAtLeastElementsInOutOfOrderDoesNotStringify() {
+        val o = CountsToStringCalls()
+        val actual: List<Any> = listOf(o, 1)
+        val expected: List<Any> = listOf(1, o)
+        assertThat(actual).containsAtLeastElementsIn(expected)
+        assertThat(o.calls).isEqualTo(0)
+
+        assertFailsWith<AssertionError> {
+            assertThat(actual).containsAtLeastElementsIn(expected).inOrder()
+        }
+
+        assertThat(o.calls > 0)
+    }
+
+    @Test
+    fun iterableContainsAtLeastFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 3)).containsAtLeast(1, 2, 4)
+        }
+    }
+
+    @Test
+    fun iterableContainsAtLeastWithExtras() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf("y", "x")).containsAtLeast("x", "y", "z")
+        }
+    }
+
+    @Test
+    fun iterableContainsAtLeastWithExtraCopiesOfOutOfOrder() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf("y", "x")).containsAtLeast("x", "y", "y")
+        }
+    }
+
+    @Test
+    fun iterableContainsAtLeastWithDuplicatesFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 3)).containsAtLeast(1, 2, 2, 2, 3, 4)
+        }
+    }
+
+    /*
+     * Slightly subtle test to ensure that if multiple equal elements are found
+     * to be missing we only reference it once in the output message.
+     */
+    @Test
+    fun iterableContainsAtLeastWithDuplicateMissingElements() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2)).containsAtLeast(4, 4, 4)
+        }
+    }
+
+    @Test
+    fun iterableContainsAtLeastWithNullFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, null, 3)).containsAtLeast(1, null, null, 3)
+        }
+    }
+
+    @Test
+    fun iterableContainsAtLeastFailsWithSameToStringAndHomogeneousList() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1L, 2L)).containsAtLeast(1, 2)
+        }
+    }
+
+    @Test
+    fun iterableContainsAtLeastFailsWithSameToStringAndHomogeneousListWithDuplicates() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1L, 2L, 2L)).containsAtLeast(1, 1, 2)
+        }
+    }
+
+    @Test
+    fun iterableContainsAtLeastFailsWithSameToStringAndHomogeneousListWithNull() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf("null", "abc")).containsAtLeast("abc", null)
+        }
+    }
+
+    @Test
+    fun iterableContainsAtLeastFailsWithSameToStringAndHeterogeneousListWithDuplicates() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 2L, 3L, 3L)).containsAtLeast(2L, 2L, 3, 3)
+        }
+    }
+
+    @Test
+    fun iterableContainsAtLeastFailsWithEmptyString() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf("a", null)).containsAtLeast("", null)
+        }
+    }
+
+    @Test
+    fun iterableContainsAtLeastInOrder() {
+        assertThat(listOf(3, 2, 5)).containsAtLeast(3, 2, 5).inOrder()
+    }
+
+    @Test
+    fun iterableContainsAtLeastInOrderWithGaps() {
+        assertThat(listOf(3, 2, 5)).containsAtLeast(3, 5).inOrder()
+        assertThat(listOf(3, 2, 2, 4, 5)).containsAtLeast(3, 2, 2, 5).inOrder()
+        assertThat(listOf(3, 1, 4, 1, 5)).containsAtLeast(3, 1, 5).inOrder()
+        assertThat(listOf("x", "y", "y", "z")).containsAtLeast("x", "y", "z").inOrder()
+        assertThat(listOf("x", "x", "y", "z")).containsAtLeast("x", "y", "z").inOrder()
+        assertThat(listOf("z", "x", "y", "z")).containsAtLeast("x", "y", "z").inOrder()
+        assertThat(listOf("x", "x", "y", "z", "x")).containsAtLeast("x", "y", "z", "x").inOrder()
+    }
+
+    @Test
+    fun iterableContainsAtLeastInOrderWithNull() {
+        assertThat(listOf(3, null, 5)).containsAtLeast(3, null, 5).inOrder()
+        assertThat(listOf(3, null, 7, 5)).containsAtLeast(3, null, 5).inOrder()
+    }
+
+    @Test
+    fun iterableContainsAtLeastInOrderWithFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, null, 3)).containsAtLeast(null, 1, 3).inOrder()
+        }
+    }
+
+    @Test
+    fun iterableContainsAtLeastInOrderWithFailureWithActualOrder() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, null, 3, 4)).containsAtLeast(null, 1, 3).inOrder()
+        }
+    }
+
+    @Test
+    fun iterableContainsAtLeastInOrderWithOneShotIterable() {
+        val iterable = listOf(2, 1, null, 4, "a", 3, "b")
+        val iterator = iterable.iterator()
+
+        val oneShot =
+            object : Iterable<Any?> {
+                override fun iterator(): Iterator<Any?> = iterator
+            }
+
+        assertThat(oneShot).containsAtLeast(1, null, 3).inOrder()
+    }
+
+    @Test
+    fun iterableContainsAtLeastInOrderWithOneShotIterableWrongOrder() {
+        val iterator = listOf(2, 1, null, 4, "a", 3, "b").iterator()
+
+        val iterable =
+            object : Iterable<Any?> {
+                override fun iterator(): Iterator<Any?> = iterator
+            }
+        assertFailsWith<AssertionError> {
+            assertThat(iterable).containsAtLeast(1, 3, null as Any?).inOrder()
+        }
+    }
+
+    @Test
+    fun iterableContainsAtLeastInOrderWrongOrderAndMissing() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2)).containsAtLeast(2, 1, 3).inOrder()
+        }
+    }
+
+    @Test
+    fun iterableContainsAtLeastElementsInIterable() {
+        assertThat(listOf(1, 2, 3)).containsAtLeastElementsIn(listOf(1, 2))
+
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 3)).containsAtLeastElementsIn(listOf(1, 2, 4))
+        }
+    }
+
+    @Test
+    fun iterableContainsAtLeastElementsInCanUseFactPerElement() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf("abc")).containsAtLeastElementsIn(listOf("123\n456", "789"))
+        }
+    }
+
+    @Test
+    fun iterableContainsAtLeastElementsInArray() {
+        assertThat(listOf(1, 2, 3)).containsAtLeastElementsIn(arrayOf(1, 2))
+
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 3)).containsAtLeastElementsIn(arrayOf(1, 2, 4))
+        }
+    }
+
+    @Test
+    fun iterableContainsNoneOf() {
+        assertThat(listOf(1, 2, 3)).containsNoneOf(4, 5, 6)
+    }
+
+    @Test
+    fun iterableContainsNoneOfFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 3)).containsNoneOf(1, 2, 4)
+        }
+    }
+
+    @Test
+    fun iterableContainsNoneOfFailureWithDuplicateInSubject() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 2, 3)).containsNoneOf(1, 2, 4)
+        }
+    }
+
+    @Test
+    fun iterableContainsNoneOfFailureWithDuplicateInExpected() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 3)).containsNoneOf(1, 2, 2, 4)
+        }
+    }
+
+    @Test
+    fun iterableContainsNoneOfFailureWithEmptyString() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf("")).containsNoneOf("", null)
+        }
+    }
+
+    @Test
+    fun iterableContainsNoneInIterable() {
+        assertThat(listOf(1, 2, 3)).containsNoneIn(listOf(4, 5, 6))
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 3)).containsNoneIn(listOf(1, 2, 4))
+        }
+    }
+
+    @Test
+    fun iterableContainsNoneInArray() {
+        assertThat(listOf(1, 2, 3)).containsNoneIn(arrayOf(4, 5, 6))
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 3)).containsNoneIn(arrayOf(1, 2, 4))
+        }
+    }
+
+        @Test
+    fun iterableContainsExactlyArray() {
+        val stringArray = arrayOf("a", "b")
+        val iterable = listOf(stringArray)
+        // This test fails w/o the explicit cast
+        assertThat(iterable).containsExactly(stringArray as Any)
+    }
+
+    @Test
+    fun arrayContainsExactly() {
+        val iterable = listOf("a", "b")
+        val array = arrayOf("a", "b")
+        assertThat(iterable).containsExactly(*array)
+    }
+
+    @Test
+    fun iterableContainsExactlyWithMany() {
+        assertThat(listOf(1, 2, 3)).containsExactly(1, 2, 3)
+    }
+
+    @Test
+    fun iterableContainsExactlyOutOfOrder() {
+        assertThat(listOf(1, 2, 3, 4)).containsExactly(3, 1, 4, 2)
+    }
+
+    @Test
+    fun iterableContainsExactlyWithDuplicates() {
+        assertThat(listOf(1, 2, 2, 2, 3)).containsExactly(1, 2, 2, 2, 3)
+    }
+
+    @Test
+    fun iterableContainsExactlyWithDuplicatesOutOfOrder() {
+        assertThat(listOf(1, 2, 2, 2, 3)).containsExactly(2, 1, 2, 3, 2)
+    }
+
+    @Test
+    fun iterableContainsExactlyWithOnlyNullPassedAsNullArray() {
+        // Truth is tolerant of this erroneous varargs call.
+        val actual = listOf(null as Any?)
+        assertThat(actual).containsExactly(null as Array<Any?>?)
+    }
+
+    @Test
+    fun iterableContainsExactlyWithOnlyNull() {
+        val actual = listOf(null as Any?)
+        assertThat(actual).containsExactly(null as Any?)
+    }
+
+    @Test
+    fun iterableContainsExactlyWithNullSecond() {
+        assertThat(listOf(1, null)).containsExactly(1, null)
+    }
+
+    @Test
+    fun iterableContainsExactlyWithNullThird() {
+        assertThat(listOf(1, 2, null)).containsExactly(1, 2, null)
+    }
+
+    @Test
+    fun iterableContainsExactlyWithNull() {
+        assertThat(listOf(1, null, 3)).containsExactly(1, null, 3)
+    }
+
+    @Test
+    fun iterableContainsExactlyWithNullOutOfOrder() {
+        assertThat(listOf(1, null, 3)).containsExactly(1, 3, null as Int?)
+    }
+
     @Test
     fun iterableContainsExactlyOutOfOrderDoesNotStringify() {
         val o = CountsToStringCalls()
@@ -579,90 +550,77 @@
         assertTrue(o.calls > 0)
     }
 
-//    @Test
-//    fun iterableContainsExactlyWithEmptyString() {
-//        expectFailureWhenTestingThat(listOf()).containsExactly("")
-//        assertFailureValue("missing (1)", "")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithEmptyStringAndUnexpectedItem() {
-//        expectFailureWhenTestingThat(listOf("a", null)).containsExactly("")
-//        assertFailureKeys("missing (1)", "unexpected (2)", "---", "expected", "but was")
-//        assertFailureValue("missing (1)", "")
-//        assertFailureValue("unexpected (2)", "a, null")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithEmptyStringAndMissingItem() {
-//        expectFailureWhenTestingThat(listOf("")).containsExactly("a", null)
-//        assertFailureValue("missing (2)", "a, null")
-//        assertFailureValue("unexpected (1)", "")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithEmptyStringAmongMissingItems() {
-//        expectFailureWhenTestingThat(listOf("a")).containsExactly("", "b")
-//        assertFailureKeys(
-//            "missing (2)", "#1", "#2", "", "unexpected (1)", "#1", "---", "expected", "but was"
-//        )
-//        assertFailureValueIndexed("#1", 0, "")
-//        assertFailureValueIndexed("#2", 0, "b")
-//        assertFailureValueIndexed("#1", 1, "a")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlySingleElement() {
-//        assertThat(listOf(1)).containsExactly(1)
-//        expectFailureWhenTestingThat(listOf(1)).containsExactly(2)
-//        assertFailureKeys("value of", "expected", "but was")
-//        assertFailureValue("value of", "iterable.onlyElement()")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlySingleElementNoEqualsMagic() {
-//        expectFailureWhenTestingThat(listOf(1)).containsExactly(1L)
-//        assertFailureValueIndexed("an instance of", 0, "java.lang.Long")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithElementsThatThrowWhenYouCallHashCode() {
-//        val one = HashCodeThrower()
-//        val two = HashCodeThrower()
-//        assertThat(listOf(one, two)).containsExactly(two, one)
-//        assertThat(listOf(one, two)).containsExactly(one, two).inOrder()
-//        assertThat(listOf(one, two)).containsExactlyElementsIn(listOf(two, one))
-//        assertThat(listOf(one, two)).containsExactlyElementsIn(listOf(one, two)).inOrder()
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithElementsThatThrowWhenYouCallHashCodeFailureTooMany() {
-//        val one = HashCodeThrower()
-//        val two = HashCodeThrower()
-//        expectFailureWhenTestingThat(listOf(one, two)).containsExactly(one)
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithElementsThatThrowWhenYouCallHashCodeOneMismatch() {
-//        val one = HashCodeThrower()
-//        val two = HashCodeThrower()
-//        expectFailureWhenTestingThat(listOf(one, one)).containsExactly(one, two)
-//    }
-//
-//    private class HashCodeThrower() {
-//        override fun equals(other: Any?): Boolean {
-//            return this === other
-//        }
-//
-//        override fun hashCode(): Int {
-//            throw java.lang.UnsupportedOperationException()
-//        }
-//
-//        override fun toString(): String {
-//            return "HCT"
-//        }
-//    }
-//
+    @Test
+    fun iterableContainsExactlyWithEmptyString() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf<Any?>()).containsExactly("")
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyWithEmptyStringAndUnexpectedItem() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf("a", null)).containsExactly("")
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyWithEmptyStringAndMissingItem() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf("")).containsExactly("a", null)
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyWithEmptyStringAmongMissingItems() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf("a")).containsExactly("", "b")
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlySingleElement() {
+        assertThat(listOf(1)).containsExactly(1)
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1)).containsExactly(2)
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlySingleElementNoEqualsMagic() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1)).containsExactly(1L)
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyWithElementsThatThrowWhenYouCallHashCode() {
+        val one = HashCodeThrower()
+        val two = HashCodeThrower()
+        assertThat(listOf(one, two)).containsExactly(two, one)
+        assertThat(listOf(one, two)).containsExactly(one, two).inOrder()
+        assertThat(listOf(one, two)).containsExactlyElementsIn(listOf(two, one))
+        assertThat(listOf(one, two)).containsExactlyElementsIn(listOf(one, two)).inOrder()
+    }
+
+    @Test
+    fun iterableContainsExactlyWithElementsThatThrowWhenYouCallHashCodeFailureTooMany() {
+        val one = HashCodeThrower()
+        val two = HashCodeThrower()
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(one, two)).containsExactly(one)
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyWithElementsThatThrowWhenYouCallHashCodeOneMismatch() {
+        val one = HashCodeThrower()
+        val two = HashCodeThrower()
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(one, one)).containsExactly(one, two)
+        }
+    }
+
     @Test
     fun iterableContainsExactlyElementsInInOrderPassesWithEmptyExpectedAndActual() {
         assertThat(emptyList<Any>()).containsExactlyElementsIn(emptyList<Any>()).inOrder()
@@ -682,152 +640,137 @@
         }
     }
 
-//    @Test
-//    fun iterableContainsExactlyMissingItemFailure() {
-//        expectFailureWhenTestingThat(listOf(1, 2)).containsExactly(1, 2, 4)
-//        assertFailureValue("missing (1)", "4")
-//    }
+    @Test
+    fun iterableContainsExactlyMissingItemFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2)).containsExactly(1, 2, 4)
+        }
+    }
 
-//    @Test
-//    fun iterableContainsExactlyUnexpectedItemFailure() {
-//        expectFailureWhenTestingThat(listOf(1, 2, 3)).containsExactly(1, 2)
-//        assertFailureValue("unexpected (1)", "3")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithDuplicatesNotEnoughItemsFailure() {
-//        expectFailureWhenTestingThat(listOf(1, 2, 3)).containsExactly(1, 2, 2, 2, 3)
-//        assertFailureValue("missing (2)", "2 [2 copies]")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithDuplicatesMissingItemFailure() {
-//        expectFailureWhenTestingThat(listOf(1, 2, 3)).containsExactly(1, 2, 2, 2, 3, 4)
-//        assertFailureValue("missing (3)", "2 [2 copies], 4")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithDuplicatesMissingItemsWithNewlineFailure() {
-//        expectFailureWhenTestingThat(listOf("a", "b", "foo\nbar"))
-//            .containsExactly("a", "b", "foo\nbar", "foo\nbar", "foo\nbar")
-//        assertFailureKeys("missing (2)", "#1 [2 copies]", "---", "expected", "but was")
-//        assertFailureValue("#1 [2 copies]", "foo\nbar")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithDuplicatesMissingAndExtraItemsWithNewlineFailure() {
-//        expectFailureWhenTestingThat(listOf("a\nb", "a\nb")).containsExactly("foo\nbar", "foo\nbar")
-//        assertFailureKeys(
-//            "missing (2)",
-//            "#1 [2 copies]",
-//            "",
-//            "unexpected (2)",
-//            "#1 [2 copies]",
-//            "---",
-//            "expected",
-//            "but was"
-//        )
-//        assertFailureValueIndexed("#1 [2 copies]", 0, "foo\nbar")
-//        assertFailureValueIndexed("#1 [2 copies]", 1, "a\nb")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithDuplicatesUnexpectedItemFailure() {
-//        expectFailureWhenTestingThat(listOf(1, 2, 2, 2, 2, 3)).containsExactly(1, 2, 2, 3)
-//        assertFailureValue("unexpected (2)", "2 [2 copies]")
-//    }
-//
-//    /*
-//   * Slightly subtle test to ensure that if multiple equal elements are found
-//   * to be missing we only reference it once in the output message.
-//   */
-//    @Test
-//    fun iterableContainsExactlyWithDuplicateMissingElements() {
-//        expectFailureWhenTestingThat(listOf()).containsExactly(4, 4, 4)
-//        assertFailureValue("missing (3)", "4 [3 copies]")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithNullFailure() {
-//        expectFailureWhenTestingThat(listOf(1, null, 3)).containsExactly(1, null, null, 3)
-//        assertFailureValue("missing (1)", "null")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithMissingAndExtraElements() {
-//        expectFailureWhenTestingThat(listOf(1, 2, 3)).containsExactly(1, 2, 4)
-//        assertFailureValue("missing (1)", "4")
-//        assertFailureValue("unexpected (1)", "3")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithDuplicateMissingAndExtraElements() {
-//        expectFailureWhenTestingThat(listOf(1, 2, 3, 3)).containsExactly(1, 2, 4, 4)
-//        assertFailureValue("missing (2)", "4 [2 copies]")
-//        assertFailureValue("unexpected (2)", "3 [2 copies]")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithCommaSeparatedVsIndividual() {
-//        expectFailureWhenTestingThat(listOf("a, b")).containsExactly("a", "b")
-//        assertFailureKeys(
-//            "missing (2)", "#1", "#2", "", "unexpected (1)", "#1", "---", "expected", "but was"
-//        )
-//        assertFailureValueIndexed("#1", 0, "a")
-//        assertFailureValueIndexed("#2", 0, "b")
-//        assertFailureValueIndexed("#1", 1, "a, b")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyFailsWithSameToStringAndHomogeneousList() {
-//        expectFailureWhenTestingThat(listOf(1L, 2L)).containsExactly(1, 2)
-//        assertFailureValue("missing (2)", "1, 2 (java.lang.Integer)")
-//        assertFailureValue("unexpected (2)", "1, 2 (java.lang.Long)")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyFailsWithSameToStringAndListWithNull() {
-//        expectFailureWhenTestingThat(listOf(1L, 2L)).containsExactly(null, 1, 2)
-//        assertFailureValue(
-//            "missing (3)", "null (null type), 1 (java.lang.Integer), 2 (java.lang.Integer)"
-//        )
-//        assertFailureValue("unexpected (2)", "1, 2 (java.lang.Long)")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyFailsWithSameToStringAndHeterogeneousList() {
-//        expectFailureWhenTestingThat(listOf(1L, 2)).containsExactly(1, null, 2L)
-//        assertFailureValue(
-//            "missing (3)", "1 (java.lang.Integer), null (null type), 2 (java.lang.Long)"
-//        )
-//        assertFailureValue("unexpected (2)", "1 (java.lang.Long), 2 (java.lang.Integer)")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyFailsWithSameToStringAndHomogeneousListWithDuplicates() {
-//        expectFailureWhenTestingThat(listOf(1L, 2L)).containsExactly(1, 2, 2)
-//        assertFailureValue("missing (3)", "1, 2 [2 copies] (java.lang.Integer)")
-//        assertFailureValue("unexpected (2)", "1, 2 (java.lang.Long)")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyFailsWithSameToStringAndHeterogeneousListWithDuplicates() {
-//        expectFailureWhenTestingThat(listOf(1L, 2)).containsExactly(1, null, null, 2L, 2L)
-//        assertFailureValue(
-//            "missing (5)",
-//            "1 (java.lang.Integer), null (null type) [2 copies], 2 (java.lang.Long) [2 copies]"
-//        )
-//        assertFailureValue("unexpected (2)", "1 (java.lang.Long), 2 (java.lang.Integer)")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyWithOneIterableGivesWarning() {
-//        expectFailureWhenTestingThat(listOf(1, 2, 3, 4)).containsExactly(listOf(1, 2, 3, 4))
-//        assertThat(expectFailure.getFailure())
-//            .hasMessageThat()
-//            .contains(CONTAINS_EXACTLY_ITERABLE_WARNING)
-//    }
-//
+    @Test
+    fun iterableContainsExactlyUnexpectedItemFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 3)).containsExactly(1, 2)
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyWithDuplicatesNotEnoughItemsFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 3)).containsExactly(1, 2, 2, 2, 3)
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyWithDuplicatesMissingItemFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 3)).containsExactly(1, 2, 2, 2, 3, 4)
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyWithDuplicatesMissingItemsWithNewlineFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf("a", "b", "foo\nbar"))
+                .containsExactly("a", "b", "foo\nbar", "foo\nbar", "foo\nbar")
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyWithDuplicatesMissingAndExtraItemsWithNewlineFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf("a\nb", "a\nb")).containsExactly("foo\nbar", "foo\nbar")
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyWithDuplicatesUnexpectedItemFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 2, 2, 2, 3)).containsExactly(1, 2, 2, 3)
+        }
+    }
+
+    /*
+   * Slightly subtle test to ensure that if multiple equal elements are found
+   * to be missing we only reference it once in the output message.
+   */
+    @Test
+    fun iterableContainsExactlyWithDuplicateMissingElements() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf<Any?>()).containsExactly(4, 4, 4)
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyWithNullFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, null, 3)).containsExactly(1, null, null, 3)
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyWithMissingAndExtraElements() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 3)).containsExactly(1, 2, 4)
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyWithDuplicateMissingAndExtraElements() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 3, 3)).containsExactly(1, 2, 4, 4)
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyWithCommaSeparatedVsIndividual() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf("a, b")).containsExactly("a", "b")
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyFailsWithSameToStringAndHomogeneousList() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1L, 2L)).containsExactly(1, 2)
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyFailsWithSameToStringAndListWithNull() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1L, 2L)).containsExactly(null, 1, 2)
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyFailsWithSameToStringAndHeterogeneousList() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1L, 2)).containsExactly(1, null, 2L)
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyFailsWithSameToStringAndHomogeneousListWithDuplicates() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1L, 2L)).containsExactly(1, 2, 2)
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyFailsWithSameToStringAndHeterogeneousListWithDuplicates() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1L, 2)).containsExactly(1, null, null, 2L, 2L)
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyWithOneIterableGivesWarning() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 3, 4)).containsExactly(listOf(1, 2, 3, 4))
+        }
+    }
+
     @Test
     fun iterableContainsExactlyElementsInWithOneIterableDoesNotGiveWarning() {
         assertFailsWith<AssertionError> {
@@ -835,81 +778,71 @@
         }
     }
 
-//    @Test
-//    fun iterableContainsExactlyWithTwoIterableDoesNotGivesWarning() {
-//        expectFailureWhenTestingThat(listOf(1, 2, 3, 4)).containsExactly(listOf(1, 2), listOf(3, 4))
-//        assertThat(expectFailure.getFailure())
-//            .hasMessageThat()
-//            .doesNotContain(CONTAINS_EXACTLY_ITERABLE_WARNING)
-//    }
-//
-//    private val CONTAINS_EXACTLY_ITERABLE_WARNING =
-//        ("Passing an iterable to the varargs method containsExactly(Object...) is "
-//            + "often not the correct thing to do. Did you mean to call "
-//            + "containsExactlyElementsIn(Iterable) instead?")
-//
-//    @Test
-//    fun iterableContainsExactlyWithOneNonIterableDoesNotGiveWarning() {
-//        expectFailureWhenTestingThat(listOf(1, 2, 3, 4)).containsExactly(1)
-//        assertFailureValue("unexpected (3)", "2, 3, 4")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyInOrder() {
-//        assertThat(listOf(3, 2, 5)).containsExactly(3, 2, 5).inOrder()
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyInOrderWithNull() {
-//        assertThat(listOf(3, null, 5)).containsExactly(3, null, 5).inOrder()
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyInOrderWithFailure() {
-//        expectFailureWhenTestingThat(listOf(1, null, 3)).containsExactly(null, 1, 3).inOrder()
-//        assertFailureKeys("contents match, but order was wrong", "expected", "but was")
-//        assertFailureValue("expected", "[null, 1, 3]")
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyInOrderWithOneShotIterable() {
-//        val iterator: Iterator<Any> = listOf(1 as Any, null, 3).iterator()
-//        val iterable: Iterable<Any> = object : Iterable<Any?> {
-//            override fun iterator(): Iterator<Any> {
-//                return iterator
-//            }
-//        }
-//        assertThat(iterable).containsExactly(1, null, 3).inOrder()
-//    }
-//
-//    @Test
-//    fun iterableContainsExactlyInOrderWithOneShotIterableWrongOrder() {
-//        val iterator: Iterator<Any> = listOf(1 as Any, null, 3).iterator()
-//        val iterable: Iterable<Any> = object : Iterable<Any?> {
-//            override fun iterator(): Iterator<Any> {
-//                return iterator
-//            }
-//
-//            override fun toString(): String {
-//                return "BadIterable"
-//            }
-//        }
-//        expectFailureWhenTestingThat(iterable).containsExactly(1, 3, null).inOrder()
-//        assertFailureKeys("contents match, but order was wrong", "expected", "but was")
-//        assertFailureValue("expected", "[1, 3, null]")
-//    }
-//
-//    @Test
-//    fun iterableWithNoToStringOverride() {
-//        val iterable: Iterable<Int> = object : Iterable<Int?> {
-//            override fun iterator(): Iterator<Int> {
-//                return Iterators.forArray(1, 2, 3)
-//            }
-//        }
-//        expectFailureWhenTestingThat(iterable).containsExactly(1, 2).inOrder()
-//        assertFailureValue("but was", "[1, 2, 3]")
-//    }
-//
+    @Test
+    fun iterableContainsExactlyWithTwoIterableDoesNotGivesWarning() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 3, 4)).containsExactly(
+                listOf(1, 2),
+                listOf(3, 4)
+            )
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyWithOneNonIterableDoesNotGiveWarning() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 3, 4)).containsExactly(1)
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyInOrder() {
+        assertThat(listOf(3, 2, 5)).containsExactly(3, 2, 5).inOrder()
+    }
+
+    @Test
+    fun iterableContainsExactlyInOrderWithNull() {
+        assertThat(listOf(3, null, 5)).containsExactly(3, null, 5).inOrder()
+    }
+
+    @Test
+    fun iterableContainsExactlyInOrderWithFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, null, 3)).containsExactly(null, 1, 3).inOrder()
+        }
+    }
+
+    @Test
+    fun iterableContainsExactlyInOrderWithOneShotIterable() {
+        val iterator = listOf(1 as Any, null, 3).iterator()
+        val iterable = object : Iterable<Any?> {
+            override fun iterator(): Iterator<Any?> = iterator
+        }
+        assertThat(iterable).containsExactly(1, null, 3).inOrder()
+    }
+
+    @Test
+    fun iterableContainsExactlyInOrderWithOneShotIterableWrongOrder() {
+        val iterator = listOf(1 as Any, null, 3).iterator()
+        val iterable = object : Iterable<Any?> {
+            override fun iterator(): Iterator<Any?> = iterator
+            override fun toString(): String = "BadIterable"
+        }
+        assertFailsWith<AssertionError> {
+            assertThat(iterable).containsExactly(1, 3, null).inOrder()
+        }
+    }
+
+    @Test
+    fun iterableWithNoToStringOverride() {
+        val iterable = object : Iterable<Int?> {
+            override fun iterator(): Iterator<Int> = listOf(1, 2, 3).iterator()
+        }
+        assertFailsWith<AssertionError> {
+            assertThat(iterable).containsExactly(1, 2).inOrder()
+        }
+    }
+
     @Test
     fun iterableContainsExactlyElementsInIterable() {
         assertThat(listOf(1, 2)).containsExactlyElementsIn(listOf(1, 2))
@@ -919,14 +852,14 @@
         }
     }
 
-//    @Test
-//    fun iterableContainsExactlyElementsInArray() {
-//        assertThat(listOf(1, 2)).containsExactlyElementsIn(arrayOf(1, 2))
-//
-//        assertFailsWith<AssertionError> {
-//            assertThat(listOf(1, 2)).containsExactlyElementsIn(arrayOf<Int?>(1, 2, 4))
-//        }
-//    }
+    @Test
+    fun iterableContainsExactlyElementsInArray() {
+        assertThat(listOf(1, 2)).containsExactlyElementsIn(arrayOf<Any?>(1, 2))
+
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2)).containsExactlyElementsIn(arrayOf<Int?>(1, 2, 4))
+        }
+    }
 
     @Test
     fun nullEqualToNull() {
@@ -1001,121 +934,72 @@
         }
     }
 
-//    @Test
-//    fun iterableIsInStrictOrder() {
-//        assertThat(emptyList<Any>()).isInStrictOrder()
-//        assertThat(listOf(1)).isInStrictOrder()
-//        assertThat(listOf(1, 2, 3, 4)).isInStrictOrder()
-//    }
+    @Test
+    fun iterableIsInStrictOrder() {
+        assertThat(emptyList<Any>()).isInStrictOrder()
+        assertThat(listOf(1)).isInStrictOrder()
+        assertThat(listOf(1, 2, 3, 4)).isInStrictOrder()
+    }
 
-//    @Test
-//    fun isInStrictOrderFailure() {
-//        expectFailureWhenTestingThat(listOf(1, 2, 2, 4)).isInStrictOrder()
-//        assertFailureKeys(
-//            "expected to be in strict order", "but contained", "followed by", "full contents"
-//        )
-//        assertFailureValue("but contained", "2")
-//        assertFailureValue("followed by", "2")
-//        assertFailureValue("full contents", "[1, 2, 2, 4]")
-//    }
-//
-//    @Test
-//    fun isInStrictOrderWithNonComparableElementsFailure() {
-//        try {
-//            assertThat(listOf(1 as Any, "2", 3, "4")).isInStrictOrder()
-//            fail("Should have thrown.")
-//        } catch (expected: java.lang.ClassCastException) {
-//        }
-//    }
-//
-//    @Test
-//    fun iterableIsInOrder() {
-//        assertThat(listOf()).isInOrder()
-//        assertThat(listOf(1)).isInOrder()
-//        assertThat(listOf(1, 1, 2, 3, 3, 3, 4)).isInOrder()
-//    }
-//
-//    @Test
-//    fun isInOrderFailure() {
-//        expectFailureWhenTestingThat(listOf(1, 3, 2, 4)).isInOrder()
-//        assertFailureKeys(
-//            "expected to be in order",
-//            "but contained",
-//            "followed by",
-//            "full contents"
-//        )
-//        assertFailureValue("but contained", "3")
-//        assertFailureValue("followed by", "2")
-//        assertFailureValue("full contents", "[1, 3, 2, 4]")
-//    }
-//
-//    @Test
-//    fun isInOrderMultipleFailures() {
-//        expectFailureWhenTestingThat(listOf(1, 3, 2, 4, 0)).isInOrder()
-//    }
-//
-//    @Test
-//    fun isInOrderWithNonComparableElementsFailure() {
-//        try {
-//            assertThat(listOf(1 as Any, "2", 2, "3")).isInOrder()
-//            fail("Should have thrown.")
-//        } catch (expected: java.lang.ClassCastException) {
-//        }
-//    }
-//
-//    @Test
-//    fun iterableIsInStrictOrderWithComparator() {
-//        val emptyStrings: Iterable<String> = listOf()
-//        assertThat(emptyStrings).isInStrictOrder(COMPARE_AS_DECIMAL)
-//        assertThat(listOf("1")).isInStrictOrder(COMPARE_AS_DECIMAL)
-//        // Note: Use "10" and "20" to distinguish numerical and lexicographical ordering.
-//        assertThat(listOf("1", "2", "10", "20")).isInStrictOrder(COMPARE_AS_DECIMAL)
-//    }
-//
-//    @Test
-//    fun iterableIsInStrictOrderWithComparatorFailure() {
-//        expectFailureWhenTestingThat(
-//            listOf(
-//                "1",
-//                "2",
-//                "2",
-//                "10"
-//            )
-//        ).isInStrictOrder(COMPARE_AS_DECIMAL)
-//        assertFailureKeys(
-//            "expected to be in strict order", "but contained", "followed by", "full contents"
-//        )
-//        assertFailureValue("but contained", "2")
-//        assertFailureValue("followed by", "2")
-//        assertFailureValue("full contents", "[1, 2, 2, 10]")
-//    }
-//
-//    @Test
-//    fun iterableIsInOrderWithComparator() {
-//        val emptyStrings: Iterable<String> = listOf()
-//        assertThat(emptyStrings).isInOrder(COMPARE_AS_DECIMAL)
-//        assertThat(listOf("1")).isInOrder(COMPARE_AS_DECIMAL)
-//        assertThat(listOf("1", "1", "2", "10", "10", "10", "20")).isInOrder(COMPARE_AS_DECIMAL)
-//    }
-//
-//    @Test
-//    fun iterableIsInOrderWithComparatorFailure() {
-//        expectFailureWhenTestingThat(listOf("1", "10", "2", "20")).isInOrder(COMPARE_AS_DECIMAL)
-//        assertFailureKeys(
-//            "expected to be in order",
-//            "but contained",
-//            "followed by",
-//            "full contents"
-//        )
-//        assertFailureValue("but contained", "10")
-//        assertFailureValue("followed by", "2")
-//        assertFailureValue("full contents", "[1, 10, 2, 20]")
-//    }
-//
-//    private val COMPARE_AS_DECIMAL: Comparator<String> =
-//        Comparator<String?> { a, b ->
-//            java.lang.Integer.valueOf(a).compareTo(java.lang.Integer.valueOf(b))
-//        }
+    @Test
+    fun isInStrictOrderFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 2, 2, 4)).isInStrictOrder()
+        }
+    }
+
+    @Test
+    fun iterableIsInOrder() {
+        assertThat(listOf<Any?>()).isInOrder()
+        assertThat(listOf(1)).isInOrder()
+        assertThat(listOf(1, 1, 2, 3, 3, 3, 4)).isInOrder()
+    }
+
+    @Test
+    fun isInOrderFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 3, 2, 4)).isInOrder()
+        }
+    }
+
+    @Test
+    fun isInOrderMultipleFailures() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf(1, 3, 2, 4, 0)).isInOrder()
+        }
+    }
+
+    @Test
+    fun iterableIsInStrictOrderWithComparator() {
+        val emptyStrings: Iterable<String> = listOf()
+        assertThat(emptyStrings).isInStrictOrder(COMPARE_AS_DECIMAL)
+        assertThat(listOf("1")).isInStrictOrder(COMPARE_AS_DECIMAL)
+        // Note: Use "10" and "20" to distinguish numerical and lexicographical ordering.
+        assertThat(listOf("1", "2", "10", "20")).isInStrictOrder(COMPARE_AS_DECIMAL)
+    }
+
+    @Test
+    fun iterableIsInStrictOrderWithComparatorFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf("1", "2", "2", "10")).isInStrictOrder(COMPARE_AS_DECIMAL)
+        }
+    }
+
+    @Test
+    fun iterableIsInOrderWithComparator() {
+        val emptyStrings: Iterable<String> = listOf()
+        assertThat(emptyStrings).isInOrder(COMPARE_AS_DECIMAL)
+        assertThat(listOf("1")).isInOrder(COMPARE_AS_DECIMAL)
+        assertThat(listOf("1", "1", "2", "10", "10", "10", "20")).isInOrder(COMPARE_AS_DECIMAL)
+    }
+
+    @Test
+    fun iterableIsInOrderWithComparatorFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(listOf("1", "10", "2", "20")).isInOrder(COMPARE_AS_DECIMAL)
+        }
+    }
+
 //
 //    private class Foo private constructor(val x: Int)
 //
@@ -1180,7 +1064,12 @@
 //                    + "containsNoneOf(...)/containsNoneIn(...) instead. Non-iterables: [a, b]")
 //            )
 //    }
-//
+
+    private companion object {
+        private val COMPARE_AS_DECIMAL: Comparator<String?> =
+            Comparator { a, b -> a!!.toInt().compareTo(b!!.toInt()) }
+    }
+
     private class CountsToStringCalls {
         var calls = 0
 
@@ -1189,4 +1078,14 @@
             return super.toString()
         }
     }
+
+    private class HashCodeThrower {
+        override fun equals(other: Any?): Boolean = this === other
+
+        override fun hashCode(): Int {
+            throw UnsupportedOperationException()
+        }
+
+        override fun toString(): String = "HCT"
+    }
 }
diff --git a/testutils/testutils-kmp/src/jvmTest/kotlin/androidx/kruth/IterableSubjectJvmTest.kt b/testutils/testutils-kmp/src/jvmTest/kotlin/androidx/kruth/IterableSubjectJvmTest.kt
new file mode 100644
index 0000000..c1adf10
--- /dev/null
+++ b/testutils/testutils-kmp/src/jvmTest/kotlin/androidx/kruth/IterableSubjectJvmTest.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.kruth
+
+import kotlin.test.Test
+import kotlin.test.assertFailsWith
+
+class IterableSubjectJvmTest {
+
+    // The test fails on native, see https://youtrack.jetbrains.com/issue/KT-56089
+    @Test
+    fun isInStrictOrderWithNonComparableElementsFailure() {
+        assertFailsWith<ClassCastException> {
+            assertThat(listOf(1 as Any, "2", 3, "4")).isInStrictOrder()
+        }
+    }
+
+    // The test fails on native, see https://youtrack.jetbrains.com/issue/KT-56089
+    @Test
+    fun isInOrderWithNonComparableElementsFailure() {
+        assertFailsWith<ClassCastException> {
+            assertThat(listOf(1 as Any, "2", 2, "3")).isInOrder()
+        }
+    }
+}
diff --git a/text/text/src/androidTest/java/androidx/compose/ui/text/android/FontPaddingTest.kt b/text/text/src/androidTest/java/androidx/compose/ui/text/android/FontPaddingTest.kt
index 550fb6f..f7e466f 100644
--- a/text/text/src/androidTest/java/androidx/compose/ui/text/android/FontPaddingTest.kt
+++ b/text/text/src/androidTest/java/androidx/compose/ui/text/android/FontPaddingTest.kt
@@ -18,6 +18,7 @@
 
 import android.graphics.Typeface
 import android.text.TextPaint
+import android.os.Build
 import androidx.core.content.res.ResourcesCompat
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
@@ -182,6 +183,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun tallTypefaceTextIsTwiceTheHeightOfLatinTypefaceTextMultiLine() {
+        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
+            return // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         val latinLayout = TextLayout(latinTextMultiLine, typeface = latinTypeface)
         val tallLayout = TextLayout(tallTextMultiLine, typeface = tallTypeface)
 
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/LayoutIntrinsics.kt b/text/text/src/main/java/androidx/compose/ui/text/android/LayoutIntrinsics.kt
index af9cb35..2463908 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/LayoutIntrinsics.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/LayoutIntrinsics.kt
@@ -144,9 +144,8 @@
     textPaint: TextPaint
 ): Boolean {
     return desiredWidth != 0f &&
-        charSequence is Spanned && (
-        textPaint.letterSpacing != 0f ||
+        (charSequence is Spanned && (
             charSequence.hasSpan(LetterSpacingSpanPx::class.java) ||
             charSequence.hasSpan(LetterSpacingSpanEm::class.java)
-        )
+        ) || textPaint.letterSpacing != 0f)
 }
\ No newline at end of file
diff --git a/tracing/OWNERS b/tracing/OWNERS
index 5db1872..9b3f90f 100644
--- a/tracing/OWNERS
+++ b/tracing/OWNERS
@@ -1,3 +1,4 @@
+# Bug component: 873508
 ccraik@google.com
 jgielzak@google.com
 rahulrav@google.com
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/App.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/App.kt
index 7898201..00ea81e 100644
--- a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/App.kt
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/App.kt
@@ -26,16 +26,24 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.MaterialTheme
+import androidx.tv.material3.darkColorScheme
 
+@OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 fun App() {
     var selectedTab by remember { mutableStateOf(Navigation.FeaturedCarousel) }
 
-    Column(
-        modifier = Modifier.padding(20.dp),
-        verticalArrangement = Arrangement.spacedBy(20.dp),
+    MaterialTheme(
+        colorScheme = darkColorScheme()
     ) {
-        TopNavigation(updateSelectedTab = { selectedTab = it })
-        selectedTab.action.invoke()
+        Column(
+            modifier = Modifier.padding(20.dp),
+            verticalArrangement = Arrangement.spacedBy(20.dp)
+        ) {
+            TopNavigation(updateSelectedTab = { selectedTab = it })
+            selectedTab.action.invoke()
+        }
     }
-}
\ No newline at end of file
+}
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt
index 5cecda2..15d409d 100644
--- a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt
@@ -42,11 +42,11 @@
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
-import androidx.tv.material.ExperimentalTvMaterialApi
-import androidx.tv.material.carousel.Carousel
-import androidx.tv.material.carousel.CarouselDefaults
-import androidx.tv.material.carousel.CarouselItem
-import androidx.tv.material.carousel.CarouselState
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.Carousel
+import androidx.tv.material3.CarouselDefaults
+import androidx.tv.material3.CarouselItem
+import androidx.tv.material3.CarouselState
 
 @Composable
 fun FeaturedCarouselContent() {
@@ -66,7 +66,21 @@
                         )
                     }
                 }
-                FeaturedCarousel()
+
+                FeaturedCarousel(Modifier.weight(1f))
+
+                Column(verticalArrangement = Arrangement.spacedBy(20.dp)) {
+                    repeat(3) {
+                        Box(
+                            modifier = Modifier
+                                .background(Color.Magenta.copy(alpha = 0.3f))
+                                .width(50.dp)
+                                .height(50.dp)
+                                .drawBorderOnFocus()
+                                .focusable()
+                        )
+                    }
+                }
             }
         }
         items(2) { SampleLazyRow() }
@@ -81,20 +95,25 @@
         .onFocusChanged { isFocused = it.isFocused }
 }
 
-@OptIn(ExperimentalTvMaterialApi::class)
+@OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
-internal fun FeaturedCarousel() {
+internal fun FeaturedCarousel(modifier: Modifier = Modifier) {
     val backgrounds = listOf(
         Color.Red.copy(alpha = 0.3f),
         Color.Yellow.copy(alpha = 0.3f),
-        Color.Green.copy(alpha = 0.3f)
+        Color.Green.copy(alpha = 0.3f),
+        Color.Blue.copy(alpha = 0.3f),
+        Color.LightGray.copy(alpha = 0.3f),
+        Color.Magenta.copy(alpha = 0.3f),
+        Color.DarkGray.copy(alpha = 0.3f),
+        Color.LightGray.copy(alpha = 0.3f),
     )
 
     val carouselState = remember { CarouselState() }
     Carousel(
         slideCount = backgrounds.size,
         carouselState = carouselState,
-        modifier = Modifier
+        modifier = modifier
             .height(300.dp)
             .fillMaxWidth(),
         carouselIndicator = {
@@ -113,7 +132,6 @@
                 Box(
                     modifier = Modifier
                         .background(backgrounds[itemIndex])
-                        .border(2.dp, Color.White.copy(alpha = 0.5f))
                         .fillMaxSize()
                 )
             }
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/ImmersiveList.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/ImmersiveList.kt
index a3bcc25..a690908 100644
--- a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/ImmersiveList.kt
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/ImmersiveList.kt
@@ -16,8 +16,10 @@
 
 package androidx.tv.integration.demos
 
+import android.util.Log
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
@@ -34,8 +36,8 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
 import androidx.tv.foundation.lazy.list.TvLazyColumn
-import androidx.tv.material.ExperimentalTvMaterialApi
-import androidx.tv.material.immersivelist.ImmersiveList
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.ImmersiveList
 
 @Composable
 fun ImmersiveListContent() {
@@ -46,7 +48,7 @@
     }
 }
 
-@OptIn(ExperimentalTvMaterialApi::class)
+@OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 private fun SampleImmersiveList() {
     val immersiveListHeight = 300.dp
@@ -60,7 +62,9 @@
     )
 
     ImmersiveList(
-        modifier = Modifier.height(immersiveListHeight + cardHeight / 2).fillMaxWidth(),
+        modifier = Modifier
+            .height(immersiveListHeight + cardHeight / 2)
+            .fillMaxWidth(),
         background = { index, _ ->
             Box(
                 modifier = Modifier
@@ -81,7 +85,10 @@
                         .height(cardHeight)
                         .border(5.dp, Color.White.copy(alpha = if (isFocused) 1f else 0.3f))
                         .onFocusChanged { isFocused = it.isFocused }
-                        .focusableItem(index)
+                        .immersiveListItem(index)
+                        .clickable {
+                            Log.d("ImmersiveList", "Item $index was clicked")
+                        }
                 )
             }
         }
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TextField.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TextField.kt
new file mode 100644
index 0000000..ff5793b
--- /dev/null
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TextField.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.integration.demos
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.material3.Button
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun TextFieldContent() {
+    LazyRow(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
+        item {
+            Column(verticalArrangement = Arrangement.spacedBy(20.dp)) {
+                repeat(4) { SampleCardItem() }
+            }
+        }
+        item {
+            LazyColumn(verticalArrangement = Arrangement.spacedBy(20.dp)) {
+                item { SampleTextField(label = "Email") }
+                item { SampleTextField(label = "Password") }
+                item { SampleButton(text = "Submit") }
+            }
+        }
+        item {
+            Column(verticalArrangement = Arrangement.spacedBy(20.dp)) {
+                repeat(4) { SampleCardItem() }
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SampleTextField(label: String) {
+    var text by remember { mutableStateOf("") }
+
+    OutlinedTextField(
+        value = text,
+        onValueChange = { text = it },
+        label = {
+            Text(label)
+        },
+        singleLine = true,
+        placeholder = {
+            Text("$label...")
+        },
+        colors = TextFieldDefaults.outlinedTextFieldColors(
+            focusedBorderColor = Color.Cyan,
+            focusedLabelColor = Color.Cyan,
+            cursorColor = Color.White
+        )
+    )
+}
+
+@Composable
+fun SampleButton(text: String) {
+    Button(
+        onClick = { }
+    ) {
+        Text(text)
+    }
+}
+
+@Composable
+private fun SampleCardItem() {
+    Box(
+        modifier = Modifier
+            .background(Color.Magenta.copy(alpha = 0.3f))
+            .width(50.dp)
+            .height(50.dp)
+            .drawBorderOnFocus()
+            .focusable()
+    )
+}
\ No newline at end of file
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TopNavigation.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TopNavigation.kt
index a7c2da4..ba1423e 100644
--- a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TopNavigation.kt
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TopNavigation.kt
@@ -29,9 +29,10 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
-import androidx.tv.material.LocalContentColor
-import androidx.tv.material.Tab
-import androidx.tv.material.TabRow
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.LocalContentColor
+import androidx.tv.material3.Tab
+import androidx.tv.material3.TabRow
 import kotlinx.coroutines.delay
 
 enum class Navigation(val displayName: String, val action: @Composable () -> Unit) {
@@ -39,6 +40,7 @@
   FeaturedCarousel("Featured Carousel", { FeaturedCarouselContent() }),
   ImmersiveList("Immersive List", { ImmersiveListContent() }),
   StickyHeader("Sticky Header", { StickyHeaderContent() }),
+  TextField("Text Field", { TextFieldContent() }),
 }
 
 @Composable
@@ -66,6 +68,7 @@
 /**
  * Pill indicator tab row for reference
  */
+@OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 fun PillIndicatorTabRow(
   tabs: List<String>,
diff --git a/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt
index 3916c9f..b235403 100644
--- a/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt
+++ b/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt
@@ -39,13 +39,13 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.unit.dp
-import androidx.tv.material.ExperimentalTvMaterialApi
-import androidx.tv.material.carousel.Carousel
-import androidx.tv.material.carousel.CarouselDefaults
-import androidx.tv.material.carousel.CarouselItem
-import androidx.tv.material.carousel.CarouselState
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.Carousel
+import androidx.tv.material3.CarouselDefaults
+import androidx.tv.material3.CarouselItem
+import androidx.tv.material3.CarouselState
 
-@OptIn(ExperimentalTvMaterialApi::class)
+@OptIn(ExperimentalTvMaterial3Api::class)
 @Sampled
 @Composable
 fun SimpleCarousel() {
@@ -92,7 +92,7 @@
     }
 }
 
-@OptIn(ExperimentalTvMaterialApi::class)
+@OptIn(ExperimentalTvMaterial3Api::class)
 @Sampled
 @Composable
 fun CarouselIndicatorWithRectangleShape() {
diff --git a/tv/samples/src/main/java/androidx/tv/samples/ImmersiveListSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/ImmersiveListSamples.kt
new file mode 100644
index 0000000..9654fd7
--- /dev/null
+++ b/tv/samples/src/main/java/androidx/tv/samples/ImmersiveListSamples.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.tv.samples
+
+import android.util.Log
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.ImmersiveList
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Sampled
+@Composable
+private fun SampleImmersiveList() {
+    val immersiveListHeight = 300.dp
+    val cardSpacing = 10.dp
+    val cardWidth = 200.dp
+    val cardHeight = 150.dp
+    val backgrounds = listOf(
+        Color.Red,
+        Color.Blue,
+        Color.Magenta,
+    )
+
+    ImmersiveList(
+        modifier = Modifier
+            .height(immersiveListHeight + cardHeight / 2)
+            .fillMaxWidth(),
+        background = { index, _ ->
+            Box(
+                modifier = Modifier
+                    .background(backgrounds[index].copy(alpha = 0.3f))
+                    .height(immersiveListHeight)
+                    .fillMaxWidth()
+            )
+        }
+    ) {
+        Row(horizontalArrangement = Arrangement.spacedBy(cardSpacing)) {
+            backgrounds.forEachIndexed { index, backgroundColor ->
+                var isFocused by remember { mutableStateOf(false) }
+
+                Box(
+                    modifier = Modifier
+                        .background(backgroundColor)
+                        .width(cardWidth)
+                        .height(cardHeight)
+                        .border(5.dp, Color.White.copy(alpha = if (isFocused) 1f else 0.3f))
+                        .onFocusChanged { isFocused = it.isFocused }
+                        .immersiveListItem(index)
+                        .clickable {
+                            Log.d("ImmersiveList", "Item $index was clicked")
+                        }
+                )
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt
index c8d85ab..126c1e8 100644
--- a/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt
+++ b/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt
@@ -34,17 +34,19 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
-import androidx.tv.material.LocalContentColor
-import androidx.tv.material.Tab
-import androidx.tv.material.TabDefaults
-import androidx.tv.material.TabRow
-import androidx.tv.material.TabRowDefaults
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.LocalContentColor
+import androidx.tv.material3.Tab
+import androidx.tv.material3.TabDefaults
+import androidx.tv.material3.TabRow
+import androidx.tv.material3.TabRowDefaults
 import kotlin.time.Duration.Companion.microseconds
 import kotlinx.coroutines.delay
 
 /**
  * Tab row with a Pill indicator
  */
+@OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 @Sampled
 fun PillIndicatorTabRow() {
@@ -74,6 +76,7 @@
 /**
  * Tab row with an Underlined indicator
  */
+@OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 @Sampled
 fun UnderlinedIndicatorTabRow() {
@@ -109,6 +112,7 @@
 /**
  * Tab row with delay between tab changes
  */
+@OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 @Sampled
 fun TabRowWithDebounce() {
@@ -147,6 +151,7 @@
 /**
  * Tab changes onClick instead of onFocus
  */
+@OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 @Sampled
 fun OnClickNavigation() {
diff --git a/tv/tv-foundation/api/current.txt b/tv/tv-foundation/api/current.txt
index 4b65ced..3940a5b 100644
--- a/tv/tv-foundation/api/current.txt
+++ b/tv/tv-foundation/api/current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.tv.foundation {
 
-  public final class PivotOffsets {
+  @androidx.compose.runtime.Immutable public final class PivotOffsets {
     ctor public PivotOffsets(optional float parentFraction, optional float childFraction);
     method public float getChildFraction();
     method public float getParentFraction();
diff --git a/tv/tv-foundation/api/public_plus_experimental_current.txt b/tv/tv-foundation/api/public_plus_experimental_current.txt
index b61a6fc..d9f30f7 100644
--- a/tv/tv-foundation/api/public_plus_experimental_current.txt
+++ b/tv/tv-foundation/api/public_plus_experimental_current.txt
@@ -4,7 +4,7 @@
   @kotlin.RequiresOptIn(message="This tv-foundation API is experimental and likely to change or be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTvFoundationApi {
   }
 
-  public final class PivotOffsets {
+  @androidx.compose.runtime.Immutable public final class PivotOffsets {
     ctor public PivotOffsets(optional float parentFraction, optional float childFraction);
     method public float getChildFraction();
     method public float getParentFraction();
diff --git a/tv/tv-foundation/api/restricted_current.txt b/tv/tv-foundation/api/restricted_current.txt
index 4b65ced..3940a5b 100644
--- a/tv/tv-foundation/api/restricted_current.txt
+++ b/tv/tv-foundation/api/restricted_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.tv.foundation {
 
-  public final class PivotOffsets {
+  @androidx.compose.runtime.Immutable public final class PivotOffsets {
     ctor public PivotOffsets(optional float parentFraction, optional float childFraction);
     method public float getChildFraction();
     method public float getParentFraction();
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 7c3174f..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
@@ -38,7 +38,6 @@
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 
@@ -245,13 +244,31 @@
     }
 
     @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()
         assertThat(state.canScrollBackward).isFalse()
     }
 
-    @Ignore("b/259608530")
     @Test
     fun canScrollBackward() = runBlocking {
         withContext(Dispatchers.Main + AutoTestFrameClock()) {
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/PivotOffsets.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/PivotOffsets.kt
index 3e23973..459edad 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/PivotOffsets.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/PivotOffsets.kt
@@ -16,6 +16,8 @@
 
 package androidx.tv.foundation
 
+import androidx.compose.runtime.Immutable
+
 /**
  * Holds the offsets needed for scrolling-with-offset.
  *
@@ -25,6 +27,7 @@
  * {@property childFraction} defines the offset of the starting edge of the child from
  * the pivot defined by parentFraction. This value should be between 0 and 1.
  */
+@Immutable
 class PivotOffsets constructor(
     val parentFraction: Float = 0.3f,
     val childFraction: Float = 0f
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/grid/LazyGridMeasure.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
index 93719d4..1d2de6dc 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
@@ -250,10 +250,12 @@
             spanLayoutProvider = spanLayoutProvider
         )
 
+        val lastVisibleItemIndex = visibleLines.lastOrNull()?.items?.lastOrNull()?.index?.value ?: 0
         return TvLazyGridMeasureResult(
             firstVisibleLine = firstLine,
             firstVisibleLineScrollOffset = currentFirstLineScrollOffset,
-            canScrollForward = index.value < itemsCount || currentMainAxisOffset > maxOffset,
+            canScrollForward =
+            lastVisibleItemIndex != itemsCount - 1 || currentMainAxisOffset > maxOffset,
             consumedScroll = consumedScroll,
             measureResult = layout(layoutWidth, layoutHeight) {
                 positionedItems.fastForEach { it.place(this) }
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 0c788a4..3e782bc 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -1,57 +1,126 @@
 // Signature format: 4.0
-package androidx.tv.material {
+package androidx.tv.material3 {
 
   public final class BringIntoViewIfChildrenAreFocusedKt {
   }
 
-  public final class ContentColorKt {
-    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
-    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
-  }
-
-  public final class TabColors {
-  }
-
-  public final class TabDefaults {
-    method @androidx.compose.runtime.Composable public androidx.tv.material.TabColors pillIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
-    method @androidx.compose.runtime.Composable public androidx.tv.material.TabColors underlinedIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
-    field public static final androidx.tv.material.TabDefaults INSTANCE;
-  }
-
-  public final class TabKt {
-    method @androidx.compose.runtime.Composable public static void Tab(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onFocus, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional boolean enabled, optional androidx.tv.material.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
-  }
-
-  public final class TabRowDefaults {
-    method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
-    method @androidx.compose.runtime.Composable public void TabSeparator();
-    method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
-    method @androidx.compose.runtime.Composable public long contentColor();
-    method public long getContainerColor();
-    property public final long ContainerColor;
-    field public static final androidx.tv.material.TabRowDefaults INSTANCE;
-  }
-
-  public final class TabRowKt {
-    method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.unit.DpRect>,kotlin.Unit> indicator, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
-  }
-
-}
-
-package androidx.tv.material.carousel {
-
   public final class CarouselItemKt {
   }
 
   public final class CarouselKt {
   }
 
-}
+  public final class ColorSchemeKt {
+  }
 
-package androidx.tv.material.immersivelist {
+  public final class ContentColorKt {
+    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
+    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
+  }
 
   public final class ImmersiveListKt {
   }
 
+  public final class MaterialTheme {
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ColorScheme getColorScheme();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.Shapes getShapes();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.Typography getTypography();
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.tv.material3.Shapes shapes;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.tv.material3.Typography typography;
+    field public static final androidx.tv.material3.MaterialTheme INSTANCE;
+  }
+
+  public final class MaterialThemeKt {
+  }
+
+  public final class ShapeDefaults {
+    method public androidx.compose.foundation.shape.CornerBasedShape getExtraLarge();
+    method public androidx.compose.foundation.shape.CornerBasedShape getExtraSmall();
+    method public androidx.compose.foundation.shape.CornerBasedShape getLarge();
+    method public androidx.compose.foundation.shape.CornerBasedShape getMedium();
+    method public androidx.compose.foundation.shape.CornerBasedShape getSmall();
+    property public final androidx.compose.foundation.shape.CornerBasedShape ExtraLarge;
+    property public final androidx.compose.foundation.shape.CornerBasedShape ExtraSmall;
+    property public final androidx.compose.foundation.shape.CornerBasedShape Large;
+    property public final androidx.compose.foundation.shape.CornerBasedShape Medium;
+    property public final androidx.compose.foundation.shape.CornerBasedShape Small;
+    field public static final androidx.tv.material3.ShapeDefaults INSTANCE;
+  }
+
+  @androidx.compose.runtime.Immutable public final class Shapes {
+    ctor public Shapes(optional androidx.compose.foundation.shape.CornerBasedShape extraSmall, optional androidx.compose.foundation.shape.CornerBasedShape small, optional androidx.compose.foundation.shape.CornerBasedShape medium, optional androidx.compose.foundation.shape.CornerBasedShape large, optional androidx.compose.foundation.shape.CornerBasedShape extraLarge);
+    method public androidx.tv.material3.Shapes copy(optional androidx.compose.foundation.shape.CornerBasedShape extraSmall, optional androidx.compose.foundation.shape.CornerBasedShape small, optional androidx.compose.foundation.shape.CornerBasedShape medium, optional androidx.compose.foundation.shape.CornerBasedShape large, optional androidx.compose.foundation.shape.CornerBasedShape extraLarge);
+    method public androidx.compose.foundation.shape.CornerBasedShape getExtraLarge();
+    method public androidx.compose.foundation.shape.CornerBasedShape getExtraSmall();
+    method public androidx.compose.foundation.shape.CornerBasedShape getLarge();
+    method public androidx.compose.foundation.shape.CornerBasedShape getMedium();
+    method public androidx.compose.foundation.shape.CornerBasedShape getSmall();
+    property public final androidx.compose.foundation.shape.CornerBasedShape extraLarge;
+    property public final androidx.compose.foundation.shape.CornerBasedShape extraSmall;
+    property public final androidx.compose.foundation.shape.CornerBasedShape large;
+    property public final androidx.compose.foundation.shape.CornerBasedShape medium;
+    property public final androidx.compose.foundation.shape.CornerBasedShape small;
+  }
+
+  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 {
+  }
+
+  public final class TabRowKt {
+  }
+
+  public final class TextKt {
+    method @androidx.compose.runtime.Composable public static void ProvideTextStyle(androidx.compose.ui.text.TextStyle value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> getLocalTextStyle();
+    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
+  }
+
+  @androidx.compose.runtime.Immutable public final class Typography {
+    ctor public Typography(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall);
+    method public androidx.tv.material3.Typography copy(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall);
+    method public androidx.compose.ui.text.TextStyle getBodyLarge();
+    method public androidx.compose.ui.text.TextStyle getBodyMedium();
+    method public androidx.compose.ui.text.TextStyle getBodySmall();
+    method public androidx.compose.ui.text.TextStyle getDisplayLarge();
+    method public androidx.compose.ui.text.TextStyle getDisplayMedium();
+    method public androidx.compose.ui.text.TextStyle getDisplaySmall();
+    method public androidx.compose.ui.text.TextStyle getHeadlineLarge();
+    method public androidx.compose.ui.text.TextStyle getHeadlineMedium();
+    method public androidx.compose.ui.text.TextStyle getHeadlineSmall();
+    method public androidx.compose.ui.text.TextStyle getLabelLarge();
+    method public androidx.compose.ui.text.TextStyle getLabelMedium();
+    method public androidx.compose.ui.text.TextStyle getLabelSmall();
+    method public androidx.compose.ui.text.TextStyle getTitleLarge();
+    method public androidx.compose.ui.text.TextStyle getTitleMedium();
+    method public androidx.compose.ui.text.TextStyle getTitleSmall();
+    property public final androidx.compose.ui.text.TextStyle bodyLarge;
+    property public final androidx.compose.ui.text.TextStyle bodyMedium;
+    property public final androidx.compose.ui.text.TextStyle bodySmall;
+    property public final androidx.compose.ui.text.TextStyle displayLarge;
+    property public final androidx.compose.ui.text.TextStyle displayMedium;
+    property public final androidx.compose.ui.text.TextStyle displaySmall;
+    property public final androidx.compose.ui.text.TextStyle headlineLarge;
+    property public final androidx.compose.ui.text.TextStyle headlineMedium;
+    property public final androidx.compose.ui.text.TextStyle headlineSmall;
+    property public final androidx.compose.ui.text.TextStyle labelLarge;
+    property public final androidx.compose.ui.text.TextStyle labelMedium;
+    property public final androidx.compose.ui.text.TextStyle labelSmall;
+    property public final androidx.compose.ui.text.TextStyle titleLarge;
+    property public final androidx.compose.ui.text.TextStyle titleMedium;
+    property public final androidx.compose.ui.text.TextStyle titleSmall;
+  }
+
+  public final class TypographyKt {
+  }
+
 }
 
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index a086e3e..1e8c90f 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -1,109 +1,270 @@
 // Signature format: 4.0
-package androidx.tv.material {
+package androidx.tv.material3 {
 
   public final class BringIntoViewIfChildrenAreFocusedKt {
   }
 
+  @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselDefaults {
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public void IndicatorRow(int slideCount, int activeSlideIndex, optional androidx.compose.ui.Modifier modifier, optional float spacing, optional kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> indicator);
+    method public androidx.compose.animation.EnterTransition getEnterTransition();
+    method public androidx.compose.animation.ExitTransition getExitTransition();
+    property public final androidx.compose.animation.EnterTransition EnterTransition;
+    property public final androidx.compose.animation.ExitTransition ExitTransition;
+    field public static final androidx.tv.material3.CarouselDefaults INSTANCE;
+    field public static final long TimeToDisplaySlideMillis = 5000L; // 0x1388L
+  }
+
+  @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselItemDefaults {
+    method public androidx.compose.animation.EnterTransition getOverlayEnterTransition();
+    method public androidx.compose.animation.ExitTransition getOverlayExitTransition();
+    property public final androidx.compose.animation.EnterTransition OverlayEnterTransition;
+    property public final androidx.compose.animation.ExitTransition OverlayExitTransition;
+    field public static final androidx.tv.material3.CarouselItemDefaults INSTANCE;
+    field public static final long OverlayEnterTransitionStartDelayMillis = 200L; // 0xc8L
+  }
+
+  public final class CarouselItemKt {
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void CarouselItem(kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.compose.ui.Modifier modifier, optional long overlayEnterTransitionStartDelayMillis, optional androidx.compose.animation.EnterTransition overlayEnterTransition, optional androidx.compose.animation.ExitTransition overlayExitTransition, kotlin.jvm.functions.Function0<kotlin.Unit> overlay);
+  }
+
+  public final class CarouselKt {
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Carousel(int slideCount, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.CarouselState carouselState, optional long timeToDisplaySlideMillis, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> carouselIndicator, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> content);
+  }
+
+  @androidx.compose.runtime.Stable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselState {
+    ctor public CarouselState(optional int initialActiveSlideIndex);
+    method public int getActiveSlideIndex();
+    method public androidx.tv.material3.ScrollPauseHandle pauseAutoScroll(int slideIndex);
+    property public final int activeSlideIndex;
+  }
+
+  @androidx.compose.runtime.Stable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ColorScheme {
+    ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline, long outlineVariant, long scrim);
+    method public androidx.tv.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
+    method public long getBackground();
+    method public long getError();
+    method public long getErrorContainer();
+    method public long getInverseOnSurface();
+    method public long getInversePrimary();
+    method public long getInverseSurface();
+    method public long getOnBackground();
+    method public long getOnError();
+    method public long getOnErrorContainer();
+    method public long getOnPrimary();
+    method public long getOnPrimaryContainer();
+    method public long getOnSecondary();
+    method public long getOnSecondaryContainer();
+    method public long getOnSurface();
+    method public long getOnSurfaceVariant();
+    method public long getOnTertiary();
+    method public long getOnTertiaryContainer();
+    method public long getOutline();
+    method public long getOutlineVariant();
+    method public long getPrimary();
+    method public long getPrimaryContainer();
+    method public long getScrim();
+    method public long getSecondary();
+    method public long getSecondaryContainer();
+    method public long getSurface();
+    method public long getSurfaceTint();
+    method public long getSurfaceVariant();
+    method public long getTertiary();
+    method public long getTertiaryContainer();
+    property public final long background;
+    property public final long error;
+    property public final long errorContainer;
+    property public final long inverseOnSurface;
+    property public final long inversePrimary;
+    property public final long inverseSurface;
+    property public final long onBackground;
+    property public final long onError;
+    property public final long onErrorContainer;
+    property public final long onPrimary;
+    property public final long onPrimaryContainer;
+    property public final long onSecondary;
+    property public final long onSecondaryContainer;
+    property public final long onSurface;
+    property public final long onSurfaceVariant;
+    property public final long onTertiary;
+    property public final long onTertiaryContainer;
+    property public final long outline;
+    property public final long outlineVariant;
+    property public final long primary;
+    property public final long primaryContainer;
+    property public final long scrim;
+    property public final long secondary;
+    property public final long secondaryContainer;
+    property public final long surface;
+    property public final long surfaceTint;
+    property public final long surfaceVariant;
+    property public final long tertiary;
+    property public final long tertiaryContainer;
+  }
+
+  public final class ColorSchemeKt {
+    method @androidx.tv.material3.ExperimentalTvMaterial3Api public static long contentColorFor(androidx.tv.material3.ColorScheme, long backgroundColor);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static long contentColorFor(long backgroundColor);
+    method @androidx.tv.material3.ExperimentalTvMaterial3Api public static androidx.tv.material3.ColorScheme darkColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
+    method @androidx.tv.material3.ExperimentalTvMaterial3Api public static androidx.tv.material3.ColorScheme lightColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
+    method @androidx.tv.material3.ExperimentalTvMaterial3Api public static long surfaceColorAtElevation(androidx.tv.material3.ColorScheme, float elevation);
+  }
+
   public final class ContentColorKt {
     method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
   }
 
-  @kotlin.RequiresOptIn(message="This tv-material API is experimental and likely to change or be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTvMaterialApi {
+  @kotlin.RequiresOptIn(message="This tv-material API is experimental and likely to change or be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTvMaterial3Api {
   }
 
-  public final class TabColors {
+  @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ImmersiveListBackgroundScope implements androidx.compose.foundation.layout.BoxScope {
+    method @androidx.compose.animation.ExperimentalAnimationApi @androidx.compose.runtime.Composable public void AnimatedContent(int targetState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentScope<java.lang.Integer>,androidx.compose.animation.ContentTransform> transitionSpec, optional androidx.compose.ui.Alignment contentAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedVisibilityScope,? super java.lang.Integer,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public void AnimatedVisibility(boolean visible, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.animation.EnterTransition enter, optional androidx.compose.animation.ExitTransition exit, optional String label, kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedVisibilityScope,kotlin.Unit> content);
   }
 
-  public final class TabDefaults {
-    method @androidx.compose.runtime.Composable public androidx.tv.material.TabColors pillIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
-    method @androidx.compose.runtime.Composable public androidx.tv.material.TabColors underlinedIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
-    field public static final androidx.tv.material.TabDefaults INSTANCE;
+  @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ImmersiveListDefaults {
+    method public androidx.compose.animation.EnterTransition getEnterTransition();
+    method public androidx.compose.animation.ExitTransition getExitTransition();
+    property public final androidx.compose.animation.EnterTransition EnterTransition;
+    property public final androidx.compose.animation.ExitTransition ExitTransition;
+    field public static final androidx.tv.material3.ImmersiveListDefaults INSTANCE;
+  }
+
+  public final class ImmersiveListKt {
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void ImmersiveList(kotlin.jvm.functions.Function3<? super androidx.tv.material3.ImmersiveListBackgroundScope,? super java.lang.Integer,? super java.lang.Boolean,kotlin.Unit> background, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment listAlignment, kotlin.jvm.functions.Function1<? super androidx.tv.material3.ImmersiveListScope,kotlin.Unit> list);
+  }
+
+  @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ImmersiveListScope {
+    method public androidx.compose.ui.Modifier immersiveListItem(androidx.compose.ui.Modifier, int index);
+  }
+
+  public final class MaterialTheme {
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ColorScheme getColorScheme();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.Shapes getShapes();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.Typography getTypography();
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public final androidx.tv.material3.ColorScheme colorScheme;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.tv.material3.Shapes shapes;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.tv.material3.Typography typography;
+    field public static final androidx.tv.material3.MaterialTheme INSTANCE;
+  }
+
+  public final class MaterialThemeKt {
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void MaterialTheme(optional androidx.tv.material3.ColorScheme colorScheme, optional androidx.tv.material3.Shapes shapes, optional androidx.tv.material3.Typography typography, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
+  @androidx.tv.material3.ExperimentalTvMaterial3Api public sealed interface ScrollPauseHandle {
+    method public void resumeAutoScroll();
+  }
+
+  public final class ShapeDefaults {
+    method public androidx.compose.foundation.shape.CornerBasedShape getExtraLarge();
+    method public androidx.compose.foundation.shape.CornerBasedShape getExtraSmall();
+    method public androidx.compose.foundation.shape.CornerBasedShape getLarge();
+    method public androidx.compose.foundation.shape.CornerBasedShape getMedium();
+    method public androidx.compose.foundation.shape.CornerBasedShape getSmall();
+    property public final androidx.compose.foundation.shape.CornerBasedShape ExtraLarge;
+    property public final androidx.compose.foundation.shape.CornerBasedShape ExtraSmall;
+    property public final androidx.compose.foundation.shape.CornerBasedShape Large;
+    property public final androidx.compose.foundation.shape.CornerBasedShape Medium;
+    property public final androidx.compose.foundation.shape.CornerBasedShape Small;
+    field public static final androidx.tv.material3.ShapeDefaults INSTANCE;
+  }
+
+  @androidx.compose.runtime.Immutable public final class Shapes {
+    ctor public Shapes(optional androidx.compose.foundation.shape.CornerBasedShape extraSmall, optional androidx.compose.foundation.shape.CornerBasedShape small, optional androidx.compose.foundation.shape.CornerBasedShape medium, optional androidx.compose.foundation.shape.CornerBasedShape large, optional androidx.compose.foundation.shape.CornerBasedShape extraLarge);
+    method public androidx.tv.material3.Shapes copy(optional androidx.compose.foundation.shape.CornerBasedShape extraSmall, optional androidx.compose.foundation.shape.CornerBasedShape small, optional androidx.compose.foundation.shape.CornerBasedShape medium, optional androidx.compose.foundation.shape.CornerBasedShape large, optional androidx.compose.foundation.shape.CornerBasedShape extraLarge);
+    method public androidx.compose.foundation.shape.CornerBasedShape getExtraLarge();
+    method public androidx.compose.foundation.shape.CornerBasedShape getExtraSmall();
+    method public androidx.compose.foundation.shape.CornerBasedShape getLarge();
+    method public androidx.compose.foundation.shape.CornerBasedShape getMedium();
+    method public androidx.compose.foundation.shape.CornerBasedShape getSmall();
+    property public final androidx.compose.foundation.shape.CornerBasedShape extraLarge;
+    property public final androidx.compose.foundation.shape.CornerBasedShape extraSmall;
+    property public final androidx.compose.foundation.shape.CornerBasedShape large;
+    property public final androidx.compose.foundation.shape.CornerBasedShape medium;
+    property public final androidx.compose.foundation.shape.CornerBasedShape small;
+  }
+
+  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(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 androidx.compose.foundation.BorderStroke? border, optional float tonalElevation, optional androidx.compose.ui.semantics.Role? role, optional float shadowElevation, 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 {
+  }
+
+  @androidx.tv.material3.ExperimentalTvMaterial3Api public final class TabDefaults {
+    method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors pillIndicatorTabColors(optional long activeContentColor, optional long contentColor, optional long selectedContentColor, optional long focusedContentColor, optional long focusedSelectedContentColor, optional long disabledActiveContentColor, optional long disabledContentColor, optional long disabledSelectedContentColor);
+    method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors underlinedIndicatorTabColors(optional long activeContentColor, optional long contentColor, optional long selectedContentColor, optional long focusedContentColor, optional long focusedSelectedContentColor, optional long disabledActiveContentColor, optional long disabledContentColor, optional long disabledSelectedContentColor);
+    field public static final androidx.tv.material3.TabDefaults INSTANCE;
   }
 
   public final class TabKt {
-    method @androidx.compose.runtime.Composable public static void Tab(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onFocus, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional boolean enabled, optional androidx.tv.material.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Tab(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onFocus, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional boolean enabled, optional androidx.tv.material3.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
-  public final class TabRowDefaults {
+  @androidx.tv.material3.ExperimentalTvMaterial3Api public final class TabRowDefaults {
     method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
     method @androidx.compose.runtime.Composable public void TabSeparator();
     method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
     method @androidx.compose.runtime.Composable public long contentColor();
     method public long getContainerColor();
     property public final long ContainerColor;
-    field public static final androidx.tv.material.TabRowDefaults INSTANCE;
+    field public static final androidx.tv.material3.TabRowDefaults INSTANCE;
   }
 
   public final class TabRowKt {
-    method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.unit.DpRect>,kotlin.Unit> indicator, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.unit.DpRect>,kotlin.Unit> indicator, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
   }
 
-}
-
-package androidx.tv.material.carousel {
-
-  @androidx.tv.material.ExperimentalTvMaterialApi public final class CarouselDefaults {
-    method @androidx.compose.runtime.Composable @androidx.tv.material.ExperimentalTvMaterialApi public void IndicatorRow(int slideCount, int activeSlideIndex, optional androidx.compose.ui.Modifier modifier, optional float spacing, optional kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> indicator);
-    method public androidx.compose.animation.EnterTransition getEnterTransition();
-    method public androidx.compose.animation.ExitTransition getExitTransition();
-    property public final androidx.compose.animation.EnterTransition EnterTransition;
-    property public final androidx.compose.animation.ExitTransition ExitTransition;
-    field public static final androidx.tv.material.carousel.CarouselDefaults INSTANCE;
-    field public static final long TimeToDisplaySlideMillis = 5000L; // 0x1388L
+  public final class TextKt {
+    method @androidx.compose.runtime.Composable public static void ProvideTextStyle(androidx.compose.ui.text.TextStyle value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> getLocalTextStyle();
+    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
   }
 
-  @androidx.tv.material.ExperimentalTvMaterialApi public final class CarouselItemDefaults {
-    method public androidx.compose.animation.EnterTransition getOverlayEnterTransition();
-    method public androidx.compose.animation.ExitTransition getOverlayExitTransition();
-    property public final androidx.compose.animation.EnterTransition OverlayEnterTransition;
-    property public final androidx.compose.animation.ExitTransition OverlayExitTransition;
-    field public static final androidx.tv.material.carousel.CarouselItemDefaults INSTANCE;
-    field public static final long OverlayEnterTransitionStartDelayMillis = 200L; // 0xc8L
+  @androidx.compose.runtime.Immutable public final class Typography {
+    ctor public Typography(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall);
+    method public androidx.tv.material3.Typography copy(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall);
+    method public androidx.compose.ui.text.TextStyle getBodyLarge();
+    method public androidx.compose.ui.text.TextStyle getBodyMedium();
+    method public androidx.compose.ui.text.TextStyle getBodySmall();
+    method public androidx.compose.ui.text.TextStyle getDisplayLarge();
+    method public androidx.compose.ui.text.TextStyle getDisplayMedium();
+    method public androidx.compose.ui.text.TextStyle getDisplaySmall();
+    method public androidx.compose.ui.text.TextStyle getHeadlineLarge();
+    method public androidx.compose.ui.text.TextStyle getHeadlineMedium();
+    method public androidx.compose.ui.text.TextStyle getHeadlineSmall();
+    method public androidx.compose.ui.text.TextStyle getLabelLarge();
+    method public androidx.compose.ui.text.TextStyle getLabelMedium();
+    method public androidx.compose.ui.text.TextStyle getLabelSmall();
+    method public androidx.compose.ui.text.TextStyle getTitleLarge();
+    method public androidx.compose.ui.text.TextStyle getTitleMedium();
+    method public androidx.compose.ui.text.TextStyle getTitleSmall();
+    property public final androidx.compose.ui.text.TextStyle bodyLarge;
+    property public final androidx.compose.ui.text.TextStyle bodyMedium;
+    property public final androidx.compose.ui.text.TextStyle bodySmall;
+    property public final androidx.compose.ui.text.TextStyle displayLarge;
+    property public final androidx.compose.ui.text.TextStyle displayMedium;
+    property public final androidx.compose.ui.text.TextStyle displaySmall;
+    property public final androidx.compose.ui.text.TextStyle headlineLarge;
+    property public final androidx.compose.ui.text.TextStyle headlineMedium;
+    property public final androidx.compose.ui.text.TextStyle headlineSmall;
+    property public final androidx.compose.ui.text.TextStyle labelLarge;
+    property public final androidx.compose.ui.text.TextStyle labelMedium;
+    property public final androidx.compose.ui.text.TextStyle labelSmall;
+    property public final androidx.compose.ui.text.TextStyle titleLarge;
+    property public final androidx.compose.ui.text.TextStyle titleMedium;
+    property public final androidx.compose.ui.text.TextStyle titleSmall;
   }
 
-  public final class CarouselItemKt {
-    method @androidx.compose.runtime.Composable @androidx.tv.material.ExperimentalTvMaterialApi public static void CarouselItem(kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.compose.ui.Modifier modifier, optional long overlayEnterTransitionStartDelayMillis, optional androidx.compose.animation.EnterTransition overlayEnterTransition, optional androidx.compose.animation.ExitTransition overlayExitTransition, kotlin.jvm.functions.Function0<kotlin.Unit> overlay);
-  }
-
-  public final class CarouselKt {
-    method @androidx.compose.runtime.Composable @androidx.tv.material.ExperimentalTvMaterialApi public static void Carousel(int slideCount, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material.carousel.CarouselState carouselState, optional long timeToDisplaySlideMillis, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> carouselIndicator, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> content);
-  }
-
-  @androidx.compose.runtime.Stable @androidx.tv.material.ExperimentalTvMaterialApi public final class CarouselState {
-    ctor public CarouselState(optional int initialActiveSlideIndex);
-    method public int getActiveSlideIndex();
-    method public androidx.tv.material.carousel.ScrollPauseHandle pauseAutoScroll(int slideIndex);
-    property public final int activeSlideIndex;
-  }
-
-  @androidx.tv.material.ExperimentalTvMaterialApi public sealed interface ScrollPauseHandle {
-    method public void resumeAutoScroll();
-  }
-
-}
-
-package androidx.tv.material.immersivelist {
-
-  @androidx.compose.runtime.Immutable @androidx.tv.material.ExperimentalTvMaterialApi public final class ImmersiveListBackgroundScope implements androidx.compose.foundation.layout.BoxScope {
-    method @androidx.compose.animation.ExperimentalAnimationApi @androidx.compose.runtime.Composable public void AnimatedContent(int targetState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentScope<java.lang.Integer>,androidx.compose.animation.ContentTransform> transitionSpec, optional androidx.compose.ui.Alignment contentAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedVisibilityScope,? super java.lang.Integer,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public void AnimatedVisibility(boolean visible, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.animation.EnterTransition enter, optional androidx.compose.animation.ExitTransition exit, optional String label, kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedVisibilityScope,kotlin.Unit> content);
-  }
-
-  @androidx.tv.material.ExperimentalTvMaterialApi public final class ImmersiveListDefaults {
-    method public androidx.compose.animation.EnterTransition getEnterTransition();
-    method public androidx.compose.animation.ExitTransition getExitTransition();
-    property public final androidx.compose.animation.EnterTransition EnterTransition;
-    property public final androidx.compose.animation.ExitTransition ExitTransition;
-    field public static final androidx.tv.material.immersivelist.ImmersiveListDefaults INSTANCE;
-  }
-
-  public final class ImmersiveListKt {
-    method @androidx.compose.runtime.Composable @androidx.tv.material.ExperimentalTvMaterialApi public static void ImmersiveList(kotlin.jvm.functions.Function3<? super androidx.tv.material.immersivelist.ImmersiveListBackgroundScope,? super java.lang.Integer,? super java.lang.Boolean,kotlin.Unit> background, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment listAlignment, kotlin.jvm.functions.Function1<? super androidx.tv.material.immersivelist.ImmersiveListScope,kotlin.Unit> list);
-  }
-
-  @androidx.compose.runtime.Immutable @androidx.tv.material.ExperimentalTvMaterialApi public final class ImmersiveListScope {
-    method public androidx.compose.ui.Modifier focusableItem(androidx.compose.ui.Modifier, int index);
+  public final class TypographyKt {
   }
 
 }
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index 0c788a4..3e782bc 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -1,57 +1,126 @@
 // Signature format: 4.0
-package androidx.tv.material {
+package androidx.tv.material3 {
 
   public final class BringIntoViewIfChildrenAreFocusedKt {
   }
 
-  public final class ContentColorKt {
-    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
-    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
-  }
-
-  public final class TabColors {
-  }
-
-  public final class TabDefaults {
-    method @androidx.compose.runtime.Composable public androidx.tv.material.TabColors pillIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
-    method @androidx.compose.runtime.Composable public androidx.tv.material.TabColors underlinedIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
-    field public static final androidx.tv.material.TabDefaults INSTANCE;
-  }
-
-  public final class TabKt {
-    method @androidx.compose.runtime.Composable public static void Tab(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onFocus, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional boolean enabled, optional androidx.tv.material.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
-  }
-
-  public final class TabRowDefaults {
-    method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
-    method @androidx.compose.runtime.Composable public void TabSeparator();
-    method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
-    method @androidx.compose.runtime.Composable public long contentColor();
-    method public long getContainerColor();
-    property public final long ContainerColor;
-    field public static final androidx.tv.material.TabRowDefaults INSTANCE;
-  }
-
-  public final class TabRowKt {
-    method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.unit.DpRect>,kotlin.Unit> indicator, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
-  }
-
-}
-
-package androidx.tv.material.carousel {
-
   public final class CarouselItemKt {
   }
 
   public final class CarouselKt {
   }
 
-}
+  public final class ColorSchemeKt {
+  }
 
-package androidx.tv.material.immersivelist {
+  public final class ContentColorKt {
+    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
+    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
+  }
 
   public final class ImmersiveListKt {
   }
 
+  public final class MaterialTheme {
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ColorScheme getColorScheme();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.Shapes getShapes();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.Typography getTypography();
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.tv.material3.Shapes shapes;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.tv.material3.Typography typography;
+    field public static final androidx.tv.material3.MaterialTheme INSTANCE;
+  }
+
+  public final class MaterialThemeKt {
+  }
+
+  public final class ShapeDefaults {
+    method public androidx.compose.foundation.shape.CornerBasedShape getExtraLarge();
+    method public androidx.compose.foundation.shape.CornerBasedShape getExtraSmall();
+    method public androidx.compose.foundation.shape.CornerBasedShape getLarge();
+    method public androidx.compose.foundation.shape.CornerBasedShape getMedium();
+    method public androidx.compose.foundation.shape.CornerBasedShape getSmall();
+    property public final androidx.compose.foundation.shape.CornerBasedShape ExtraLarge;
+    property public final androidx.compose.foundation.shape.CornerBasedShape ExtraSmall;
+    property public final androidx.compose.foundation.shape.CornerBasedShape Large;
+    property public final androidx.compose.foundation.shape.CornerBasedShape Medium;
+    property public final androidx.compose.foundation.shape.CornerBasedShape Small;
+    field public static final androidx.tv.material3.ShapeDefaults INSTANCE;
+  }
+
+  @androidx.compose.runtime.Immutable public final class Shapes {
+    ctor public Shapes(optional androidx.compose.foundation.shape.CornerBasedShape extraSmall, optional androidx.compose.foundation.shape.CornerBasedShape small, optional androidx.compose.foundation.shape.CornerBasedShape medium, optional androidx.compose.foundation.shape.CornerBasedShape large, optional androidx.compose.foundation.shape.CornerBasedShape extraLarge);
+    method public androidx.tv.material3.Shapes copy(optional androidx.compose.foundation.shape.CornerBasedShape extraSmall, optional androidx.compose.foundation.shape.CornerBasedShape small, optional androidx.compose.foundation.shape.CornerBasedShape medium, optional androidx.compose.foundation.shape.CornerBasedShape large, optional androidx.compose.foundation.shape.CornerBasedShape extraLarge);
+    method public androidx.compose.foundation.shape.CornerBasedShape getExtraLarge();
+    method public androidx.compose.foundation.shape.CornerBasedShape getExtraSmall();
+    method public androidx.compose.foundation.shape.CornerBasedShape getLarge();
+    method public androidx.compose.foundation.shape.CornerBasedShape getMedium();
+    method public androidx.compose.foundation.shape.CornerBasedShape getSmall();
+    property public final androidx.compose.foundation.shape.CornerBasedShape extraLarge;
+    property public final androidx.compose.foundation.shape.CornerBasedShape extraSmall;
+    property public final androidx.compose.foundation.shape.CornerBasedShape large;
+    property public final androidx.compose.foundation.shape.CornerBasedShape medium;
+    property public final androidx.compose.foundation.shape.CornerBasedShape small;
+  }
+
+  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 {
+  }
+
+  public final class TabRowKt {
+  }
+
+  public final class TextKt {
+    method @androidx.compose.runtime.Composable public static void ProvideTextStyle(androidx.compose.ui.text.TextStyle value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> getLocalTextStyle();
+    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
+  }
+
+  @androidx.compose.runtime.Immutable public final class Typography {
+    ctor public Typography(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall);
+    method public androidx.tv.material3.Typography copy(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall);
+    method public androidx.compose.ui.text.TextStyle getBodyLarge();
+    method public androidx.compose.ui.text.TextStyle getBodyMedium();
+    method public androidx.compose.ui.text.TextStyle getBodySmall();
+    method public androidx.compose.ui.text.TextStyle getDisplayLarge();
+    method public androidx.compose.ui.text.TextStyle getDisplayMedium();
+    method public androidx.compose.ui.text.TextStyle getDisplaySmall();
+    method public androidx.compose.ui.text.TextStyle getHeadlineLarge();
+    method public androidx.compose.ui.text.TextStyle getHeadlineMedium();
+    method public androidx.compose.ui.text.TextStyle getHeadlineSmall();
+    method public androidx.compose.ui.text.TextStyle getLabelLarge();
+    method public androidx.compose.ui.text.TextStyle getLabelMedium();
+    method public androidx.compose.ui.text.TextStyle getLabelSmall();
+    method public androidx.compose.ui.text.TextStyle getTitleLarge();
+    method public androidx.compose.ui.text.TextStyle getTitleMedium();
+    method public androidx.compose.ui.text.TextStyle getTitleSmall();
+    property public final androidx.compose.ui.text.TextStyle bodyLarge;
+    property public final androidx.compose.ui.text.TextStyle bodyMedium;
+    property public final androidx.compose.ui.text.TextStyle bodySmall;
+    property public final androidx.compose.ui.text.TextStyle displayLarge;
+    property public final androidx.compose.ui.text.TextStyle displayMedium;
+    property public final androidx.compose.ui.text.TextStyle displaySmall;
+    property public final androidx.compose.ui.text.TextStyle headlineLarge;
+    property public final androidx.compose.ui.text.TextStyle headlineMedium;
+    property public final androidx.compose.ui.text.TextStyle headlineSmall;
+    property public final androidx.compose.ui.text.TextStyle labelLarge;
+    property public final androidx.compose.ui.text.TextStyle labelMedium;
+    property public final androidx.compose.ui.text.TextStyle labelSmall;
+    property public final androidx.compose.ui.text.TextStyle titleLarge;
+    property public final androidx.compose.ui.text.TextStyle titleMedium;
+    property public final androidx.compose.ui.text.TextStyle titleSmall;
+  }
+
+  public final class TypographyKt {
+  }
+
 }
 
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/TabRowTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/TabRowTest.kt
deleted file mode 100644
index 7698c60..0000000
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material/TabRowTest.kt
+++ /dev/null
@@ -1,322 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.material
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.text.BasicText
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.key.NativeKeyEvent
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsFocused
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.unit.DpRect
-import androidx.compose.ui.unit.dp
-import androidx.test.platform.app.InstrumentationRegistry
-import org.junit.Rule
-import org.junit.Test
-
-class TabRowTest {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    @Test
-    fun tabRow_shouldNotCrashWithOnly1Tab() {
-        val tabs = constructTabs(count = 1)
-
-        setContent(tabs)
-    }
-
-    @Test
-    fun tabRow_shouldNotCrashWithNoTabs() {
-        val tabs = constructTabs(count = 0)
-
-        setContent(tabs)
-    }
-
-    @Test
-    fun tabRow_firstTabIsSelected() {
-        val tabs = constructTabs()
-        val firstTab = tabs[0]
-
-        setContent(tabs)
-
-        rule.onNodeWithTag(firstTab).assertIsFocused()
-    }
-
-    @Test
-    fun tabRow_dPadRightMovesFocusToSecondTab() {
-        val tabs = constructTabs()
-        val firstTab = tabs[0]
-        val secondTab = tabs[1]
-
-        setContent(tabs)
-
-        // First tab should be focused
-        rule.onNodeWithTag(firstTab).assertIsFocused()
-
-        rule.waitForIdle()
-
-        // Move to next tab
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
-
-        rule.waitForIdle()
-
-        // Second tab should be focused
-        rule.onNodeWithTag(secondTab).assertIsFocused()
-    }
-
-    @Test
-    fun tabRow_dPadLeftMovesFocusToPreviousTab() {
-        val tabs = constructTabs()
-        val firstTab = tabs[0]
-        val secondTab = tabs[1]
-        val thirdTab = tabs[2]
-
-        setContent(tabs)
-
-        // First tab should be focused
-        rule.onNodeWithTag(firstTab).assertIsFocused()
-
-        rule.waitForIdle()
-
-        // Move to next tab
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
-
-        rule.waitForIdle()
-
-        // Second tab should be focused
-        rule.onNodeWithTag(secondTab).assertIsFocused()
-
-        // Move to next tab
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
-
-        rule.waitForIdle()
-
-        // Third tab should be focused
-        rule.onNodeWithTag(thirdTab).assertIsFocused()
-
-        // Move to previous tab
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
-
-        rule.waitForIdle()
-
-        // Second tab should be focused
-        rule.onNodeWithTag(secondTab).assertIsFocused()
-
-        // Move to previous tab
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
-
-        rule.waitForIdle()
-
-        // First tab should be focused
-        rule.onNodeWithTag(firstTab).assertIsFocused()
-    }
-
-    @Test
-    fun tabRow_changeActiveTabOnClick() {
-        val tabs = constructTabs(count = 2)
-
-        val firstPanel = "Panel 1"
-        val secondPanel = "Panel 2"
-
-        setContent(
-            tabs,
-            contentBuilder = @Composable {
-                var focusedTabIndex by remember { mutableStateOf(0) }
-                var activeTabIndex by remember { mutableStateOf(focusedTabIndex) }
-                TabRowSample(
-                    tabs = tabs,
-                    selectedTabIndex = activeTabIndex,
-                    onFocus = { focusedTabIndex = it },
-                    onClick = { activeTabIndex = it },
-                    buildTabPanel = @Composable { index, _ ->
-                        BasicText(text = "Panel ${index + 1}")
-                    },
-                    indicator = @Composable { tabPositions ->
-                        // FocusedTab's indicator
-                        TabRowDefaults.PillIndicator(
-                            currentTabPosition = tabPositions[focusedTabIndex],
-                            activeColor = Color.Blue.copy(alpha = 0.4f),
-                            inactiveColor = Color.Transparent,
-                        )
-
-                        // SelectedTab's indicator
-                        TabRowDefaults.PillIndicator(
-                            currentTabPosition = tabPositions[activeTabIndex]
-                        )
-                    }
-                )
-            }
-        )
-
-        rule.onNodeWithText(firstPanel).assertIsDisplayed()
-
-        // Move focus to next tab
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
-
-        rule.waitForIdle()
-
-        rule.onNodeWithText(firstPanel).assertIsDisplayed()
-        rule.onNodeWithText(secondPanel).assertDoesNotExist()
-
-        // Click on the new focused tab
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_CENTER)
-
-        rule.onNodeWithText(firstPanel).assertDoesNotExist()
-        rule.onNodeWithText(secondPanel).assertIsDisplayed()
-    }
-
-    private fun setContent(
-        tabs: List<String>,
-        contentBuilder: @Composable () -> Unit = {
-            var selectedTabIndex by remember { mutableStateOf(0) }
-            TabRowSample(
-                tabs = tabs,
-                selectedTabIndex = selectedTabIndex,
-                onFocus = { selectedTabIndex = it }
-            )
-        },
-    ) {
-        rule.setContent {
-            contentBuilder()
-        }
-
-        rule.waitForIdle()
-
-        // Move the focus TabRow
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN)
-
-        rule.waitForIdle()
-    }
-}
-
-@Composable
-private fun TabRowSample(
-    tabs: List<String>,
-    selectedTabIndex: Int,
-    onFocus: (index: Int) -> Unit = {},
-    onClick: (index: Int) -> Unit = onFocus,
-    buildTab: @Composable ((index: Int, tab: String) -> Unit) = @Composable { index, tab ->
-        TabSample(
-            selected = selectedTabIndex == index,
-            onFocus = { onFocus(index) },
-            onClick = { onClick(index) },
-            modifier = Modifier.testTag(tab),
-        )
-    },
-    indicator: @Composable ((tabPositions: List<DpRect>) -> Unit)? = null,
-    buildTabPanel: @Composable ((index: Int, tab: String) -> Unit) = @Composable { _, tab ->
-        BasicText(text = tab)
-    },
-) {
-    val fr = remember { FocusRequester() }
-
-    Column(
-        modifier = Modifier
-            .fillMaxSize()
-            .background(Color.Black)
-    ) {
-        // Added so that this can get focus and pass it to the tab row
-        Box(
-            modifier = Modifier
-                .size(50.dp)
-                .focusRequester(fr)
-                .background(Color.White)
-                .focusable()
-        )
-
-        // Send focus to button
-        LaunchedEffect(Unit) {
-            fr.requestFocus()
-        }
-
-        if (indicator != null) {
-            TabRow(
-                selectedTabIndex = selectedTabIndex,
-                indicator = indicator,
-                separator = { Spacer(modifier = Modifier.width(12.dp)) },
-            ) {
-                tabs.forEachIndexed { index, tab -> buildTab(index, tab) }
-            }
-        } else {
-            TabRow(
-                selectedTabIndex = selectedTabIndex,
-                separator = { Spacer(modifier = Modifier.width(12.dp)) },
-            ) {
-                tabs.forEachIndexed { index, tab -> buildTab(index, tab) }
-            }
-        }
-
-        tabs.elementAtOrNull(selectedTabIndex)?.let { buildTabPanel(selectedTabIndex, it) }
-    }
-}
-
-@Composable
-private fun TabSample(
-    selected: Boolean,
-    modifier: Modifier = Modifier,
-    onFocus: () -> Unit = {},
-    onClick: () -> Unit = {},
-    tag: String = "Tab",
-) {
-    Tab(
-        selected = selected,
-        onFocus = onFocus,
-        onClick = onClick,
-        modifier = modifier
-            .width(100.dp)
-            .height(50.dp)
-            .testTag(tag)
-            .border(2.dp, Color.White, RoundedCornerShape(50))
-    ) {}
-}
-
-private fun performKeyPress(keyCode: Int, count: Int = 1) {
-    for (i in 1..count) {
-        InstrumentationRegistry
-            .getInstrumentation()
-            .sendKeyDownUpSync(keyCode)
-    }
-}
-
-private fun constructTabs(
-    count: Int = 3,
-    buildTab: (index: Int) -> String = { "Season $it" }
-): List<String> = List(count, buildTab)
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselItemTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselItemTest.kt
deleted file mode 100644
index 25d05ef..0000000
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselItemTest.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.material.carousel
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.size
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.dp
-import androidx.tv.material.ExperimentalTvMaterialApi
-import org.junit.Rule
-import org.junit.Test
-
-class CarouselItemTest {
-    @get:Rule
-    val rule = createComposeRule()
-
-    @OptIn(ExperimentalTvMaterialApi::class)
-    @Test
-    fun carouselItem_overlayVisibleAfterRenderTime() {
-        val overlayEnterTransitionStartDelay: Long = 2000
-        val overlayTag = "overlay"
-        val backgroundTag = "background"
-        rule.setContent {
-            CarouselItem(
-                overlayEnterTransitionStartDelayMillis = overlayEnterTransitionStartDelay,
-                background = {
-                    Box(
-                        Modifier
-                            .testTag(backgroundTag)
-                            .size(200.dp)
-                            .background(Color.Blue)) }) {
-                Box(
-                    Modifier
-                        .testTag(overlayTag)
-                        .size(50.dp)
-                        .background(Color.Red))
-            }
-        }
-
-        // only background is visible
-        rule.onNodeWithTag(backgroundTag).assertExists()
-        rule.onNodeWithTag(overlayTag).assertDoesNotExist()
-
-        // advance clock by `overlayEnterTransitionStartDelay`
-        rule.mainClock.advanceTimeBy(overlayEnterTransitionStartDelay)
-
-        rule.onNodeWithTag(backgroundTag).assertExists()
-        rule.onNodeWithTag(overlayTag).assertExists()
-    }
-}
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
deleted file mode 100644
index a89cb06..0000000
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
+++ /dev/null
@@ -1,679 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.material.carousel
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.text.BasicText
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.key.NativeKeyEvent
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsActions
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsFocused
-import androidx.compose.ui.test.assertIsNotFocused
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-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.onNodeWithText
-import androidx.compose.ui.test.onParent
-import androidx.compose.ui.test.onRoot
-import androidx.compose.ui.test.performSemanticsAction
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.tv.material.ExperimentalTvMaterialApi
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-
-private const val delayBetweenSlides = 2500L
-private const val animationTime = 900L
-private const val overlayRenderWaitTime = 1500L
-
-@OptIn(ExperimentalTvMaterialApi::class)
-class CarouselTest {
-    @get:Rule
-    val rule = createComposeRule()
-
-    @Test
-    fun carousel_autoScrolls() {
-        rule.setContent {
-            SampleCarousel {
-                BasicText(text = "Text ${it + 1}")
-            }
-        }
-
-        rule.onNodeWithText("Text 1").assertIsDisplayed()
-
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-        rule.onNodeWithText("Text 2").assertIsDisplayed()
-
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-        rule.onNodeWithText("Text 3").assertIsDisplayed()
-    }
-
-    @Test
-    fun carousel_onFocus_stopsScroll() {
-        rule.setContent {
-            SampleCarousel {
-                BasicText(text = "Text ${it + 1}")
-            }
-        }
-
-        rule.onNodeWithText("Text 1").assertIsDisplayed()
-        rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
-
-        rule.onNodeWithText("Text 1")
-            .onParent()
-            .performSemanticsAction(SemanticsActions.RequestFocus)
-
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-
-        rule.onNodeWithText("Text 2").assertDoesNotExist()
-        rule.onNodeWithText("Text 1").onParent().assertIsFocused()
-    }
-
-    @Test
-    fun carousel_onUserTriggeredPause_stopsScroll() {
-        rule.setContent {
-            val carouselState = remember { CarouselState() }
-            SampleCarousel(carouselState = carouselState) {
-                BasicText(text = "Text ${it + 1}")
-                LaunchedEffect(carouselState) { carouselState.pauseAutoScroll(it) }
-            }
-        }
-
-        rule.onNodeWithText("Text 1").assertIsDisplayed()
-        rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
-
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-
-        rule.onNodeWithText("Text 2").assertDoesNotExist()
-        rule.onNodeWithText("Text 1").assertIsDisplayed()
-    }
-
-    @Test
-    fun carousel_onUserTriggeredPauseAndResume_resumeScroll() {
-        var pauseHandle: ScrollPauseHandle? = null
-        rule.setContent {
-            val carouselState = remember { CarouselState() }
-            SampleCarousel(carouselState = carouselState) {
-                BasicText(text = "Text ${it + 1}")
-                LaunchedEffect(carouselState) {
-                    pauseHandle = carouselState.pauseAutoScroll(it)
-                }
-            }
-        }
-
-        rule.mainClock.autoAdvance = false
-
-        rule.onNodeWithText("Text 1").assertIsDisplayed()
-        rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
-
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-
-        // pause handle has not been resumed, so Text 1 should still be on the screen.
-        rule.onNodeWithText("Text 2").assertDoesNotExist()
-        rule.onNodeWithText("Text 1").assertIsDisplayed()
-
-        rule.runOnIdle { pauseHandle?.resumeAutoScroll() }
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-
-        // pause handle has been resumed, so Text 2 should be on the screen after
-        // delayBetweenSlides + animationTime
-        rule.onNodeWithText("Text 1").assertDoesNotExist()
-        rule.onNodeWithText("Text 2").assertIsDisplayed()
-    }
-
-    @Test
-    fun carousel_onMultipleUserTriggeredPauseAndResume_resumesScroll() {
-        var pauseHandle1: ScrollPauseHandle? = null
-        var pauseHandle2: ScrollPauseHandle? = null
-        rule.setContent {
-            val carouselState = remember { CarouselState() }
-            SampleCarousel(carouselState = carouselState) {
-                BasicText(text = "Text ${it + 1}")
-                LaunchedEffect(carouselState) {
-                    if (pauseHandle1 == null) {
-                        pauseHandle1 = carouselState.pauseAutoScroll(it)
-                    }
-                    if (pauseHandle2 == null) {
-                        pauseHandle2 = carouselState.pauseAutoScroll(it)
-                    }
-                }
-            }
-        }
-
-        rule.mainClock.autoAdvance = false
-        rule.onNodeWithText("Text 1").assertIsDisplayed()
-        rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
-
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-
-        // pause handles have not been resumed, so Text 1 should still be on the screen.
-        rule.onNodeWithText("Text 2").assertDoesNotExist()
-        rule.onNodeWithText("Text 1").assertIsDisplayed()
-
-        rule.runOnIdle { pauseHandle1?.resumeAutoScroll() }
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-
-        // Second pause handle has not been resumed, so Text 1 should still be on the screen.
-        rule.onNodeWithText("Text 2").assertDoesNotExist()
-        rule.onNodeWithText("Text 1").assertIsDisplayed()
-
-        rule.runOnIdle { pauseHandle2?.resumeAutoScroll() }
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-        // All pause handles have been resumed, so Text 2 should be on the screen after
-        // delayBetweenSlides + animationTime
-        rule.onNodeWithText("Text 1").assertDoesNotExist()
-        rule.onNodeWithText("Text 2").assertIsDisplayed()
-    }
-
-    @Test
-    fun carousel_onRepeatedResumesOnSamePauseHandle_ignoresSubsequentResumeCalls() {
-        var pauseHandle1: ScrollPauseHandle? = null
-        rule.setContent {
-            val carouselState = remember { CarouselState() }
-            var pauseHandle2: ScrollPauseHandle? = null
-            SampleCarousel(carouselState = carouselState) {
-                BasicText(text = "Text ${it + 1}")
-                LaunchedEffect(carouselState) {
-                    if (pauseHandle1 == null) {
-                        pauseHandle1 = carouselState.pauseAutoScroll(it)
-                    }
-                    if (pauseHandle2 == null) {
-                        pauseHandle2 = carouselState.pauseAutoScroll(it)
-                    }
-                }
-            }
-        }
-
-        rule.mainClock.autoAdvance = false
-        rule.onNodeWithText("Text 1").assertIsDisplayed()
-        rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
-
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-
-        // pause handles have not been resumed, so Text 1 should still be on the screen.
-        rule.onNodeWithText("Text 2").assertDoesNotExist()
-        rule.onNodeWithText("Text 1").assertIsDisplayed()
-
-        rule.runOnIdle { pauseHandle1?.resumeAutoScroll() }
-        // subsequent call to resume should be ignored
-        rule.runOnIdle { pauseHandle1?.resumeAutoScroll() }
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-
-        // Second pause handle has not been resumed, so Text 1 should still be on the screen.
-        rule.onNodeWithText("Text 2").assertDoesNotExist()
-        rule.onNodeWithText("Text 1").assertIsDisplayed()
-    }
-
-    @Test
-    fun carousel_outOfFocus_resumesScroll() {
-        rule.setContent {
-            Column {
-                SampleCarousel {
-                    BasicText(text = "Text ${it + 1}")
-                }
-                BasicText(text = "Card", modifier = Modifier.focusable())
-            }
-        }
-
-        rule.onNodeWithText("Text 1")
-            .onParent()
-            .performSemanticsAction(SemanticsActions.RequestFocus)
-
-        rule.onNodeWithText("Card").performSemanticsAction(SemanticsActions.RequestFocus)
-        rule.onNodeWithText("Card").assertIsFocused()
-
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-        rule.onNodeWithText("Text 1").assertDoesNotExist()
-        rule.onNodeWithText("Text 2").assertIsDisplayed()
-    }
-
-    @Test
-    fun carousel_pagerIndicatorDisplayed() {
-        rule.setContent {
-            SampleCarousel {
-                SampleCarouselSlide(index = it)
-            }
-        }
-
-        rule.onNodeWithTag("indicator").assertIsDisplayed()
-    }
-
-    @Test
-    fun carousel_withAnimatedContent_successfulTransition() {
-        rule.setContent {
-            SampleCarousel {
-                SampleCarouselSlide(index = it) {
-                    Column {
-                        BasicText(text = "Text ${it + 1}")
-                        BasicText(text = "PLAY")
-                    }
-                }
-            }
-        }
-
-        rule.onNodeWithText("Text 1").assertDoesNotExist()
-
-        rule.mainClock.advanceTimeBy(overlayRenderWaitTime + animationTime, true)
-        rule.mainClock.advanceTimeByFrame()
-
-        rule.onNodeWithText("Text 1").assertIsDisplayed()
-        rule.onNodeWithText("PLAY").assertIsDisplayed()
-    }
-
-    @Test
-    fun carousel_withAnimatedContent_successfulFocusIn() {
-        rule.setContent {
-            SampleCarousel {
-                SampleCarouselSlide(index = it)
-            }
-        }
-
-        rule.mainClock.autoAdvance = false
-        rule.onNodeWithTag("pager")
-            .performSemanticsAction(SemanticsActions.RequestFocus)
-
-        // current slide overlay render delay
-        rule.mainClock.advanceTimeBy(animationTime + overlayRenderWaitTime, false)
-        rule.mainClock.advanceTimeBy(animationTime, false)
-        rule.mainClock.advanceTimeByFrame()
-
-        rule.onNodeWithText("Play 0").assertIsDisplayed()
-        rule.onNodeWithText("Play 0").assertIsFocused()
-    }
-
-    @Test
-    fun carousel_scrollToRegainFocus_checkBringIntoView() {
-        val focusRequester = FocusRequester()
-        rule.setContent {
-            LazyColumn {
-                items(3) {
-                    var isFocused by remember { mutableStateOf(false) }
-                    BasicText(
-                        text = "test-card-$it",
-                        modifier = Modifier
-                            .focusRequester(if (it == 0) focusRequester else FocusRequester.Default)
-                            .testTag("test-card-$it")
-                            .size(200.dp)
-                            .border(2.dp, if (isFocused) Color.Red else Color.Black)
-                            .onFocusChanged { fs ->
-                                isFocused = fs.isFocused
-                            }
-                            .focusable()
-                    )
-                }
-                item {
-                    Carousel(
-                        modifier = Modifier
-                            .height(500.dp)
-                            .fillMaxWidth()
-                            .testTag("featured-carousel")
-                            .border(2.dp, Color.Black),
-                        carouselState = remember { CarouselState() },
-                        slideCount = 3,
-                        timeToDisplaySlideMillis = delayBetweenSlides
-                    ) {
-                        SampleCarouselSlide(
-                            index = it,
-                            overlayRenderWaitTime = overlayRenderWaitTime,
-                        ) {
-                            Box {
-                                Column(modifier = Modifier.align(Alignment.BottomStart)) {
-                                    BasicText(text = "carousel-frame")
-                                    Row {
-                                        SampleButton(text = "PLAY")
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
-                items(2) {
-                    var isFocused by remember { mutableStateOf(false) }
-                    BasicText(
-                        text = "test-card-${it + 3}",
-                        modifier = Modifier
-                            .testTag("test-card-${it + 3}")
-                            .size(250.dp)
-                            .border(
-                                2.dp,
-                                if (isFocused) Color.Red else Color.Black
-                            )
-                            .onFocusChanged { fs ->
-                                isFocused = fs.isFocused
-                            }
-                            .focusable()
-                    )
-                }
-            }
-        }
-        rule.runOnIdle { focusRequester.requestFocus() }
-
-        // Initially first focusable element would be focused
-        rule.waitForIdle()
-        rule.onNodeWithTag("test-card-0").assertIsFocused()
-
-        // Scroll down to the Carousel and check if it's brought into view on gaining focus
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
-        rule.waitForIdle()
-        rule.onNodeWithTag("featured-carousel").assertIsDisplayed()
-        assertThat(checkNodeCompletelyVisible(rule, "featured-carousel")).isTrue()
-
-        // Scroll down to last element, making sure the carousel is partially visible
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
-        rule.waitForIdle()
-        rule.onNodeWithTag("test-card-4").assertIsFocused()
-        rule.onNodeWithTag("featured-carousel").assertIsDisplayed()
-
-        // Scroll back to the carousel to check if it's brought into view on regaining focus
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
-        rule.waitForIdle()
-        rule.onNodeWithTag("featured-carousel").assertIsDisplayed()
-        assertThat(checkNodeCompletelyVisible(rule, "featured-carousel")).isTrue()
-    }
-
-    @Test
-    fun carousel_zeroSlideCount_shouldNotCrash() {
-        val testTag = "emptyCarousel"
-        rule.setContent {
-            Carousel(slideCount = 0, modifier = Modifier.testTag(testTag)) {}
-        }
-
-        rule.onNodeWithTag(testTag).assertExists()
-    }
-
-    @Test
-    fun carousel_oneSlideCount_shouldNotCrash() {
-        val testTag = "emptyCarousel"
-        rule.setContent {
-            Carousel(slideCount = 1, modifier = Modifier.testTag(testTag)) {}
-        }
-
-        rule.onNodeWithTag(testTag).assertExists()
-    }
-
-    @Test
-    fun carousel_manualScrolling_withFocusableItemsOnTop() {
-        rule.setContent {
-            Column {
-                Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
-                    repeat(3) {
-                        SampleButton("Row-button-${it + 1}")
-                    }
-                }
-                SampleCarousel { index ->
-                    SampleButton("Button-${index + 1}")
-                }
-            }
-        }
-
-        rule.mainClock.autoAdvance = false
-        rule.onNodeWithTag("pager")
-            .performSemanticsAction(SemanticsActions.RequestFocus)
-
-        // trigger recomposition on requesting focus
-        rule.mainClock.advanceTimeByFrame()
-        rule.waitForIdle()
-
-        // Check that slide 1 is in view and button 1 has focus
-        rule.onNodeWithText("Button-1").assertIsDisplayed()
-        rule.onNodeWithText("Button-1").assertIsFocused()
-
-        // press dpad right to scroll to next slide
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
-
-        // Wait for slide to load
-        rule.mainClock.advanceTimeByFrame()
-        rule.waitForIdle()
-        rule.mainClock.advanceTimeBy(animationTime, false)
-        rule.waitForIdle()
-
-        // Check that slide 2 is in view and button 2 has focus
-        rule.onNodeWithText("Button-2").assertIsDisplayed()
-        // TODO: Fix button 2 isn't gaining focus
-        // rule.onNodeWithText("Button-2").assertIsFocused()
-
-        // Check if the first focusable element in parent has focus
-        rule.onNodeWithText("Row-button-1").assertIsNotFocused()
-
-        // press dpad left to scroll to previous slide
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
-
-        // Wait for slide to load
-        rule.mainClock.advanceTimeByFrame()
-        rule.waitForIdle()
-        rule.mainClock.advanceTimeBy(animationTime, false)
-        rule.waitForIdle()
-
-        // Check that slide 1 is in view and button 1 has focus
-        rule.onNodeWithText("Button-1").assertIsDisplayed()
-        rule.onNodeWithText("Button-1").assertIsFocused()
-    }
-
-    @Test
-    fun carousel_manualScrolling_ltr() {
-        rule.setContent {
-            SampleCarousel { index ->
-                SampleButton("Button ${index + 1}")
-            }
-        }
-
-        rule.mainClock.autoAdvance = false
-        rule.onNodeWithTag("pager")
-            .performSemanticsAction(SemanticsActions.RequestFocus)
-
-        // current slide overlay render delay
-        rule.mainClock.advanceTimeBy(animationTime + overlayRenderWaitTime, false)
-        rule.mainClock.advanceTimeBy(animationTime, false)
-        rule.mainClock.advanceTimeByFrame()
-
-        // Assert that slide 1 is in view
-        rule.onNodeWithText("Button 1").assertIsDisplayed()
-
-        // advance time
-        rule.mainClock.advanceTimeBy(delayBetweenSlides + animationTime, false)
-        rule.mainClock.advanceTimeByFrame()
-
-        // go right once
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
-
-        // Wait for slide to load
-        rule.mainClock.advanceTimeBy(animationTime)
-        rule.mainClock.advanceTimeByFrame()
-
-        // Assert that slide 2 is in view
-        rule.onNodeWithText("Button 2").assertIsDisplayed()
-
-        // go left once
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
-
-        // Wait for slide to load
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-        rule.mainClock.advanceTimeBy(animationTime)
-        rule.mainClock.advanceTimeByFrame()
-
-        // Assert that slide 1 is in view
-        rule.onNodeWithText("Button 1").assertIsDisplayed()
-    }
-
-    @Test
-    fun carousel_manualScrolling_rtl() {
-        rule.setContent {
-            CompositionLocalProvider(
-                LocalLayoutDirection provides LayoutDirection.Rtl
-            ) {
-                SampleCarousel {
-                    SampleButton("Button ${it + 1}")
-                }
-            }
-        }
-
-        rule.mainClock.autoAdvance = false
-        rule.onNodeWithTag("pager")
-            .performSemanticsAction(SemanticsActions.RequestFocus)
-
-        // current slide overlay render delay
-        rule.mainClock.advanceTimeBy(animationTime + overlayRenderWaitTime, false)
-        rule.mainClock.advanceTimeBy(animationTime, false)
-        rule.mainClock.advanceTimeByFrame()
-
-        // Assert that slide 1 is in view
-        rule.onNodeWithText("Button 1").assertIsDisplayed()
-
-        // advance time
-        rule.mainClock.advanceTimeBy(delayBetweenSlides + animationTime, false)
-        rule.mainClock.advanceTimeByFrame()
-
-        // go right once
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
-
-        // Wait for slide to load
-        rule.mainClock.advanceTimeBy(animationTime)
-        rule.mainClock.advanceTimeByFrame()
-
-        // Assert that slide 2 is in view
-        rule.onNodeWithText("Button 2").assertIsDisplayed()
-
-        // go left once
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
-
-        // Wait for slide to load
-        rule.mainClock.advanceTimeBy(delayBetweenSlides + animationTime, false)
-        rule.mainClock.advanceTimeByFrame()
-
-        // Assert that slide 1 is in view
-        rule.onNodeWithText("Button 1").assertIsDisplayed()
-    }
-}
-
-@OptIn(ExperimentalTvMaterialApi::class)
-@Composable
-private fun SampleCarousel(
-    carouselState: CarouselState = remember { CarouselState() },
-    slideCount: Int = 3,
-    timeToDisplaySlideMillis: Long = delayBetweenSlides,
-    content: @Composable (index: Int) -> Unit
-) {
-    Carousel(
-        modifier = Modifier
-            .padding(5.dp)
-            .fillMaxWidth()
-            .height(50.dp)
-            .testTag("pager"),
-        carouselState = carouselState,
-        slideCount = slideCount,
-        timeToDisplaySlideMillis = timeToDisplaySlideMillis,
-        carouselIndicator = {
-            CarouselDefaults.IndicatorRow(
-                modifier = Modifier
-                    .align(Alignment.BottomEnd)
-                    .padding(16.dp)
-                    .testTag("indicator"),
-                activeSlideIndex = carouselState.activeSlideIndex,
-                slideCount = slideCount
-            )
-        },
-        content = content,
-    )
-}
-
-@OptIn(ExperimentalTvMaterialApi::class)
-@Composable
-private fun SampleCarouselSlide(
-    index: Int,
-    overlayRenderWaitTime: Long = CarouselItemDefaults.OverlayEnterTransitionStartDelayMillis,
-    content: (@Composable () -> Unit) = { SampleButton("Play $index") },
-) {
-    CarouselItem(
-        overlayEnterTransitionStartDelayMillis = overlayRenderWaitTime,
-        background = {
-            Box(
-                modifier = Modifier
-                    .fillMaxSize()
-                    .background(Color.Red)
-                    .border(2.dp, Color.Blue)
-            )
-        },
-        overlay = content
-    )
-}
-
-@Composable
-private fun SampleButton(text: String = "Play") {
-    var isFocused by remember { mutableStateOf(false) }
-    BasicText(
-        text = text,
-        modifier = Modifier
-            .size(100.dp, 20.dp)
-            .background(Color.Yellow)
-            .onFocusChanged { isFocused = it.isFocused }
-            .border(2.dp, if (isFocused) Color.Green else Color.Transparent)
-            .focusable(),
-    )
-}
-
-private fun checkNodeCompletelyVisible(
-    rule: ComposeContentTestRule,
-    tag: String,
-): Boolean {
-    rule.waitForIdle()
-
-    val rootRect = rule.onRoot().getUnclippedBoundsInRoot()
-    val itemRect = rule.onNodeWithTag(tag).getUnclippedBoundsInRoot()
-
-    return itemRect.left >= rootRect.left &&
-        itemRect.right <= rootRect.right &&
-        itemRect.top >= rootRect.top &&
-        itemRect.bottom <= rootRect.bottom
-}
-
-private fun performKeyPress(keyCode: Int, count: Int = 1) {
-    for (i in 1..count) {
-        InstrumentationRegistry
-            .getInstrumentation()
-            .sendKeyDownUpSync(keyCode)
-    }
-}
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/immersivelist/ImmersiveListTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/immersivelist/ImmersiveListTest.kt
deleted file mode 100644
index d128958..0000000
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material/immersivelist/ImmersiveListTest.kt
+++ /dev/null
@@ -1,288 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.material.immersivelist
-
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.foundation.border
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.text.BasicText
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.RectangleShape
-import androidx.compose.ui.input.key.NativeKeyEvent
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsFocused
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onRoot
-import androidx.compose.ui.unit.dp
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.tv.foundation.lazy.list.TvLazyColumn
-import androidx.tv.foundation.lazy.list.TvLazyRow
-import androidx.tv.material.ExperimentalTvMaterialApi
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-
-class ImmersiveListTest {
-    @get:Rule
-    val rule = createComposeRule()
-
-    @OptIn(ExperimentalTvMaterialApi::class, ExperimentalAnimationApi::class)
-    @Test
-    fun immersiveList_scroll_backgroundChanges() {
-        val firstCard = FocusRequester()
-        val secondCard = FocusRequester()
-
-        rule.setContent {
-            ImmersiveList(
-                background = { index, _ ->
-                    AnimatedContent(targetState = index) {
-                        Box(
-                            Modifier
-                                .testTag("background-$it")
-                                .size(200.dp)) {
-                            BasicText("background-$it")
-                        }
-                    }
-                }) {
-                    TvLazyRow {
-                        items(3) { index ->
-                            var modifier = Modifier
-                                .testTag("card-$index")
-                                .size(100.dp)
-                            when (index) {
-                                0 -> modifier = modifier
-                                    .focusRequester(firstCard)
-                                1 -> modifier = modifier
-                                    .focusRequester(secondCard)
-                            }
-
-                            Box(modifier.focusableItem(index)) { BasicText("card-$index") }
-                        }
-                    }
-            }
-        }
-
-        rule.runOnIdle { firstCard.requestFocus() }
-
-        rule.onNodeWithTag("card-0").assertIsFocused()
-        rule.onNodeWithTag("background-0").assertIsDisplayed()
-        rule.onNodeWithTag("background-1").assertDoesNotExist()
-        rule.onNodeWithTag("background-2").assertDoesNotExist()
-
-        rule.waitForIdle()
-        keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
-
-        rule.onNodeWithTag("card-1").assertIsFocused()
-        rule.onNodeWithTag("background-1").assertIsDisplayed()
-        rule.onNodeWithTag("background-0").assertDoesNotExist()
-        rule.onNodeWithTag("background-2").assertDoesNotExist()
-
-        rule.waitForIdle()
-        keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
-
-        rule.onNodeWithTag("card-0").assertIsFocused()
-        rule.onNodeWithTag("background-0").assertIsDisplayed()
-        rule.onNodeWithTag("background-1").assertDoesNotExist()
-        rule.onNodeWithTag("background-2").assertDoesNotExist()
-    }
-
-    @Test
-    fun immersiveList_scrollToRegainFocusInLazyColumn_checkBringIntoView() {
-        val focusRequesterList = mutableListOf<FocusRequester>()
-        for (item in 0..2) { focusRequesterList.add(FocusRequester()) }
-        setupContent(focusRequesterList)
-
-        // Initially first focusable element would be focused
-        rule.waitForIdle()
-        rule.onNodeWithTag("test-card-0").assertIsFocused()
-
-        // Scroll down to the Immersive List's first card
-        keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
-        rule.waitForIdle()
-        rule.onNodeWithTag("list-card-0").assertIsFocused()
-        rule.onNodeWithTag("immersive-list").assertIsDisplayed()
-        assertThat(checkNodeCompletelyVisible("immersive-list")).isTrue()
-
-        // Scroll down to last element, making sure the immersive list is partially visible
-        keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
-        rule.waitForIdle()
-        rule.onNodeWithTag("test-card-4").assertIsFocused()
-        rule.onNodeWithTag("immersive-list").assertIsDisplayed()
-
-        // Scroll back to the immersive list to check if it's brought into view on regaining focus
-        keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
-        rule.waitForIdle()
-        rule.onNodeWithTag("immersive-list").assertIsDisplayed()
-        assertThat(checkNodeCompletelyVisible("immersive-list")).isTrue()
-    }
-
-    @Test
-    fun immersiveList_scrollToRegainFocusInTvLazyColumn_checkBringIntoView() {
-        val focusRequesterList = mutableListOf<FocusRequester>()
-        for (item in 0..2) { focusRequesterList.add(FocusRequester()) }
-        setupTvContent(focusRequesterList)
-
-        // Initially first focusable element would be focused
-        rule.waitForIdle()
-        rule.onNodeWithTag("test-card-0").assertIsFocused()
-
-        // Scroll down to the Immersive List's first card
-        keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
-        rule.waitForIdle()
-        rule.onNodeWithTag("list-card-0").assertIsFocused()
-        rule.onNodeWithTag("immersive-list").assertIsDisplayed()
-        assertThat(checkNodeCompletelyVisible("immersive-list")).isTrue()
-
-        // Scroll down to last element, making sure the immersive list is partially visible
-        keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
-        rule.waitForIdle()
-        rule.onNodeWithTag("test-card-4").assertIsFocused()
-        rule.onNodeWithTag("immersive-list").assertIsDisplayed()
-
-        // Scroll back to the immersive list to check if it's brought into view on regaining focus
-        keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
-        rule.waitForIdle()
-        rule.onNodeWithTag("immersive-list").assertIsDisplayed()
-        assertThat(checkNodeCompletelyVisible("immersive-list")).isTrue()
-    }
-
-    private fun checkNodeCompletelyVisible(tag: String): Boolean {
-        rule.waitForIdle()
-
-        val rootRect = rule.onRoot().getUnclippedBoundsInRoot()
-        val itemRect = rule.onNodeWithTag(tag).getUnclippedBoundsInRoot()
-
-        return itemRect.left >= rootRect.left &&
-            itemRect.right <= rootRect.right &&
-            itemRect.top >= rootRect.top &&
-            itemRect.bottom <= rootRect.bottom
-    }
-
-    private fun setupContent(focusRequesterList: List<FocusRequester>) {
-        val focusRequester = FocusRequester()
-        rule.setContent {
-            LazyColumn {
-                items(3) {
-                    val modifier =
-                        if (it == 0) Modifier.focusRequester(focusRequester)
-                        else Modifier
-                    BasicText(
-                        text = "test-card-$it",
-                        modifier = modifier
-                            .testTag("test-card-$it")
-                            .size(200.dp)
-                            .focusable()
-                    )
-                }
-                item { TestImmersiveList(focusRequesterList) }
-                items(2) {
-                    BasicText(
-                        text = "test-card-${it + 3}",
-                        modifier = Modifier
-                            .testTag("test-card-${it + 3}")
-                            .size(200.dp)
-                            .focusable()
-                    )
-                }
-            }
-        }
-        rule.runOnIdle { focusRequester.requestFocus() }
-    }
-
-    private fun setupTvContent(focusRequesterList: List<FocusRequester>) {
-        val focusRequester = FocusRequester()
-        rule.setContent {
-            TvLazyColumn {
-                items(3) {
-                    val modifier =
-                        if (it == 0) Modifier.focusRequester(focusRequester)
-                        else Modifier
-                    BasicText(
-                        text = "test-card-$it",
-                        modifier = modifier
-                            .testTag("test-card-$it")
-                            .size(200.dp)
-                            .focusable()
-                    )
-                }
-                item { TestImmersiveList(focusRequesterList) }
-                items(2) {
-                    BasicText(
-                        text = "test-card-${it + 3}",
-                        modifier = Modifier
-                            .testTag("test-card-${it + 3}")
-                            .size(200.dp)
-                            .focusable()
-                    )
-                }
-            }
-        }
-        rule.runOnIdle { focusRequester.requestFocus() }
-    }
-
-    @OptIn(ExperimentalTvMaterialApi::class, ExperimentalAnimationApi::class)
-    @Composable
-    private fun TestImmersiveList(focusRequesterList: List<FocusRequester>) {
-        val frList = remember { focusRequesterList }
-        ImmersiveList(
-            background = { index, _ ->
-                AnimatedContent(targetState = index) {
-                    Box(
-                        Modifier
-                            .testTag("background-$it")
-                            .fillMaxWidth()
-                            .height(400.dp)
-                            .border(2.dp, Color.Black, RectangleShape)
-                    ) {
-                        BasicText("background-$it")
-                    }
-                }
-            },
-            modifier = Modifier.testTag("immersive-list")
-        ) {
-            TvLazyRow {
-                items(frList.count()) { index ->
-                    var modifier = Modifier
-                        .testTag("list-card-$index")
-                        .size(50.dp)
-                    for (item in frList) {
-                        modifier = modifier.focusRequester(frList[index])
-                    }
-                    Box(modifier.focusableItem(index)) { BasicText("list-card-$index") }
-                }
-            }
-        }
-    }
-
-    private fun keyPress(keyCode: Int, numberOfPresses: Int = 1) {
-        for (index in 0 until numberOfPresses)
-            InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode)
-    }
-}
\ No newline at end of file
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselItemTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselItemTest.kt
new file mode 100644
index 0000000..bc41a0f
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselItemTest.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.unit.dp
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Rule
+import org.junit.Test
+
+class CarouselItemTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @OptIn(ExperimentalTvMaterial3Api::class)
+    @Test
+    fun carouselItem_overlayVisibleAfterRenderTime() {
+        val overlayEnterTransitionStartDelay: Long = 2000
+        val overlayTag = "overlay"
+        val backgroundTag = "background"
+        rule.setContent {
+            CarouselItem(
+                overlayEnterTransitionStartDelayMillis = overlayEnterTransitionStartDelay,
+                background = {
+                    Box(
+                        Modifier
+                            .testTag(backgroundTag)
+                            .size(200.dp)
+                            .background(Color.Blue)) }) {
+                Box(
+                    Modifier
+                        .testTag(overlayTag)
+                        .size(50.dp)
+                        .background(Color.Red))
+            }
+        }
+
+        // only background is visible
+        rule.onNodeWithTag(backgroundTag).assertExists()
+        rule.onNodeWithTag(overlayTag).assertDoesNotExist()
+
+        // advance clock by `overlayEnterTransitionStartDelay`
+        rule.mainClock.advanceTimeBy(overlayEnterTransitionStartDelay)
+
+        rule.onNodeWithTag(backgroundTag).assertExists()
+        rule.onNodeWithTag(overlayTag).assertExists()
+    }
+
+    @OptIn(ExperimentalTvMaterial3Api::class)
+    @Test
+    fun carouselItem_parentContainerGainsFocused_onBackPress() {
+        rule.setContent {
+            Box(modifier = Modifier
+                .testTag("box-container")
+                .fillMaxSize()
+                .focusable()) {
+                CarouselItem(
+                    overlayEnterTransitionStartDelayMillis = 0,
+                    modifier = Modifier.testTag("carousel-item"),
+                    background = { Box(Modifier.size(300.dp).background(Color.Cyan)) }
+                ) {
+                    SampleButton()
+                }
+            }
+        }
+
+        // Request focus for Carousel Item on start
+        rule.onNodeWithTag("carousel-item")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+        rule.waitForIdle()
+
+        // Check if overlay button in carousel item is focused
+        rule.onNodeWithTag("sample-button").assertIsFocused()
+
+        // Trigger back press
+        performKeyPress(NativeKeyEvent.KEYCODE_BACK)
+        rule.waitForIdle()
+
+        // Check if carousel item loses focus and parent container gains focus
+        rule.onNodeWithTag("box-container").assertIsFocused()
+    }
+
+    @Composable
+    private fun SampleButton(text: String = "sample-button") {
+        var isFocused by remember { mutableStateOf(false) }
+        BasicText(
+            text = text,
+            modifier = Modifier.testTag(text)
+                .size(100.dp, 20.dp)
+                .background(Color.Yellow)
+                .onFocusChanged { isFocused = it.isFocused }
+                .border(2.dp, if (isFocused) Color.Green else Color.Transparent)
+                .focusable()
+        )
+    }
+
+    private fun performKeyPress(keyCode: Int, count: Int = 1) {
+        repeat(count) {
+            InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode)
+        }
+    }
+}
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
new file mode 100644
index 0000000..5e2da37
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
@@ -0,0 +1,920 @@
+/*
+ * 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.tv.material3
+
+import android.os.SystemClock
+import android.view.KeyEvent
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.assertIsNotFocused
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+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.onNodeWithText
+import androidx.compose.ui.test.onParent
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performKeyPress
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.delay
+import org.junit.Rule
+import org.junit.Test
+
+private const val delayBetweenSlides = 2500L
+private const val animationTime = 900L
+private const val overlayRenderWaitTime = 1500L
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+class CarouselTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun carousel_autoScrolls() {
+        rule.setContent {
+            SampleCarousel {
+                BasicText(text = "Text ${it + 1}")
+            }
+        }
+
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.onNodeWithText("Text 2").assertIsDisplayed()
+
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.onNodeWithText("Text 3").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_onFocus_stopsScroll() {
+        rule.setContent {
+            SampleCarousel {
+                BasicText(text = "Text ${it + 1}")
+            }
+        }
+
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+        rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
+
+        rule.onNodeWithText("Text 1")
+            .onParent()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+
+        rule.onNodeWithText("Text 2").assertDoesNotExist()
+        rule.onNodeWithText("Text 1").onParent().assertIsFocused()
+    }
+
+    @Test
+    fun carousel_onUserTriggeredPause_stopsScroll() {
+        rule.setContent {
+            val carouselState = remember { CarouselState() }
+            SampleCarousel(carouselState = carouselState) {
+                BasicText(text = "Text ${it + 1}")
+                LaunchedEffect(carouselState) { carouselState.pauseAutoScroll(it) }
+            }
+        }
+
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+        rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
+
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+
+        rule.onNodeWithText("Text 2").assertDoesNotExist()
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_onUserTriggeredPauseAndResume_resumeScroll() {
+        var pauseHandle: ScrollPauseHandle? = null
+        rule.setContent {
+            val carouselState = remember { CarouselState() }
+            SampleCarousel(carouselState = carouselState) {
+                BasicText(text = "Text ${it + 1}")
+                LaunchedEffect(carouselState) {
+                    pauseHandle = carouselState.pauseAutoScroll(it)
+                }
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+        rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
+
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+
+        // pause handle has not been resumed, so Text 1 should still be on the screen.
+        rule.onNodeWithText("Text 2").assertDoesNotExist()
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+
+        rule.runOnIdle { pauseHandle?.resumeAutoScroll() }
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+
+        // pause handle has been resumed, so Text 2 should be on the screen after
+        // delayBetweenSlides + animationTime
+        rule.onNodeWithText("Text 1").assertDoesNotExist()
+        rule.onNodeWithText("Text 2").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_onMultipleUserTriggeredPauseAndResume_resumesScroll() {
+        var pauseHandle1: ScrollPauseHandle? = null
+        var pauseHandle2: ScrollPauseHandle? = null
+        rule.setContent {
+            val carouselState = remember { CarouselState() }
+            SampleCarousel(carouselState = carouselState) {
+                BasicText(text = "Text ${it + 1}")
+                LaunchedEffect(carouselState) {
+                    if (pauseHandle1 == null) {
+                        pauseHandle1 = carouselState.pauseAutoScroll(it)
+                    }
+                    if (pauseHandle2 == null) {
+                        pauseHandle2 = carouselState.pauseAutoScroll(it)
+                    }
+                }
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+        rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
+
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+
+        // pause handles have not been resumed, so Text 1 should still be on the screen.
+        rule.onNodeWithText("Text 2").assertDoesNotExist()
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+
+        rule.runOnIdle { pauseHandle1?.resumeAutoScroll() }
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+
+        // Second pause handle has not been resumed, so Text 1 should still be on the screen.
+        rule.onNodeWithText("Text 2").assertDoesNotExist()
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+
+        rule.runOnIdle { pauseHandle2?.resumeAutoScroll() }
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        // All pause handles have been resumed, so Text 2 should be on the screen after
+        // delayBetweenSlides + animationTime
+        rule.onNodeWithText("Text 1").assertDoesNotExist()
+        rule.onNodeWithText("Text 2").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_onRepeatedResumesOnSamePauseHandle_ignoresSubsequentResumeCalls() {
+        var pauseHandle1: ScrollPauseHandle? = null
+        rule.setContent {
+            val carouselState = remember { CarouselState() }
+            var pauseHandle2: ScrollPauseHandle? = null
+            SampleCarousel(carouselState = carouselState) {
+                BasicText(text = "Text ${it + 1}")
+                LaunchedEffect(carouselState) {
+                    if (pauseHandle1 == null) {
+                        pauseHandle1 = carouselState.pauseAutoScroll(it)
+                    }
+                    if (pauseHandle2 == null) {
+                        pauseHandle2 = carouselState.pauseAutoScroll(it)
+                    }
+                }
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+        rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
+
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+
+        // pause handles have not been resumed, so Text 1 should still be on the screen.
+        rule.onNodeWithText("Text 2").assertDoesNotExist()
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+
+        rule.runOnIdle { pauseHandle1?.resumeAutoScroll() }
+        // subsequent call to resume should be ignored
+        rule.runOnIdle { pauseHandle1?.resumeAutoScroll() }
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+
+        // Second pause handle has not been resumed, so Text 1 should still be on the screen.
+        rule.onNodeWithText("Text 2").assertDoesNotExist()
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_outOfFocus_resumesScroll() {
+        rule.setContent {
+            Column {
+                SampleCarousel {
+                    BasicText(text = "Text ${it + 1}")
+                }
+                BasicText(text = "Card", modifier = Modifier.focusable())
+            }
+        }
+
+        rule.onNodeWithText("Text 1")
+            .onParent()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        rule.onNodeWithText("Card").performSemanticsAction(SemanticsActions.RequestFocus)
+        rule.onNodeWithText("Card").assertIsFocused()
+
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.onNodeWithText("Text 1").assertDoesNotExist()
+        rule.onNodeWithText("Text 2").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_pagerIndicatorDisplayed() {
+        rule.setContent {
+            SampleCarousel {
+                SampleCarouselSlide(index = it)
+            }
+        }
+
+        rule.onNodeWithTag("indicator").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_withAnimatedContent_successfulTransition() {
+        rule.setContent {
+            SampleCarousel {
+                SampleCarouselSlide(index = it) {
+                    Column {
+                        BasicText(text = "Text ${it + 1}")
+                        BasicText(text = "PLAY")
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithText("Text 1").assertDoesNotExist()
+
+        rule.mainClock.advanceTimeBy(overlayRenderWaitTime + animationTime, true)
+        rule.mainClock.advanceTimeByFrame()
+
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+        rule.onNodeWithText("PLAY").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_withAnimatedContent_successfulFocusIn() {
+        rule.setContent {
+            SampleCarousel {
+                SampleCarouselSlide(index = it)
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag("pager")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        // current slide overlay render delay
+        rule.mainClock.advanceTimeBy(animationTime + overlayRenderWaitTime, false)
+        rule.mainClock.advanceTimeBy(animationTime, false)
+        rule.mainClock.advanceTimeByFrame()
+
+        rule.onNodeWithText("Play 0").assertIsDisplayed()
+        rule.onNodeWithText("Play 0").assertIsFocused()
+    }
+
+    @Test
+    fun carousel_parentContainerGainsFocus_onBackPress() {
+        rule.setContent {
+            Box(modifier = Modifier
+                .testTag("box-container")
+                .fillMaxSize()
+                .focusable()) {
+                SampleCarousel { index ->
+                    SampleButton("Button-${index + 1}")
+                }
+            }
+        }
+
+        // Request focus for Carousel on start
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag("pager")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        // Trigger recomposition after requesting focus
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+
+        // Check if the overlay button is focused
+        rule.onNodeWithText("Button-1").assertIsFocused()
+
+        // Trigger back press event to exit focus
+        performKeyPress(NativeKeyEvent.KEYCODE_BACK)
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+
+        // Check if carousel loses focus and parent container gains focus
+        rule.onNodeWithText("Button-1").assertIsNotFocused()
+        rule.onNodeWithTag("box-container").assertIsFocused()
+    }
+
+    @Test
+    fun carousel_withCarouselItem_parentContainerGainsFocus_onBackPress() {
+        rule.setContent {
+            Box(modifier = Modifier
+                .testTag("box-container")
+                .fillMaxSize()
+                .focusable()) {
+                SampleCarousel {
+                    SampleCarouselSlide(index = it)
+                }
+            }
+        }
+
+        // Request focus for Carousel on start
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag("pager")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        // Trigger recomposition after requesting focus and advance time to finish animations
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeBy(animationTime + overlayRenderWaitTime, false)
+        rule.waitForIdle()
+
+        // Check if the overlay button is focused
+        rule.onNodeWithText("Play 0").assertIsFocused()
+
+        // Trigger back press event to exit focus
+        performKeyPress(NativeKeyEvent.KEYCODE_BACK)
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+
+        // Check if carousel loses focus and parent container gains focus
+        rule.onNodeWithText("Play 0").assertIsNotFocused()
+        rule.onNodeWithTag("box-container").assertIsFocused()
+    }
+
+    @Test
+    fun carousel_scrollToRegainFocus_checkBringIntoView() {
+        val focusRequester = FocusRequester()
+        rule.setContent {
+            LazyColumn {
+                items(3) {
+                    var isFocused by remember { mutableStateOf(false) }
+                    BasicText(
+                        text = "test-card-$it",
+                        modifier = Modifier
+                            .focusRequester(if (it == 0) focusRequester else FocusRequester.Default)
+                            .testTag("test-card-$it")
+                            .size(200.dp)
+                            .border(2.dp, if (isFocused) Color.Red else Color.Black)
+                            .onFocusChanged { fs ->
+                                isFocused = fs.isFocused
+                            }
+                            .focusable()
+                    )
+                }
+                item {
+                    Carousel(
+                        modifier = Modifier
+                            .height(500.dp)
+                            .fillMaxWidth()
+                            .testTag("featured-carousel")
+                            .border(2.dp, Color.Black),
+                        carouselState = remember { CarouselState() },
+                        slideCount = 3,
+                        timeToDisplaySlideMillis = delayBetweenSlides
+                    ) {
+                        SampleCarouselSlide(
+                            index = it,
+                            overlayRenderWaitTime = overlayRenderWaitTime,
+                        ) {
+                            Box {
+                                Column(modifier = Modifier.align(Alignment.BottomStart)) {
+                                    BasicText(text = "carousel-frame")
+                                    Row {
+                                        SampleButton(text = "PLAY")
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+                items(2) {
+                    var isFocused by remember { mutableStateOf(false) }
+                    BasicText(
+                        text = "test-card-${it + 3}",
+                        modifier = Modifier
+                            .testTag("test-card-${it + 3}")
+                            .size(250.dp)
+                            .border(
+                                2.dp,
+                                if (isFocused) Color.Red else Color.Black
+                            )
+                            .onFocusChanged { fs ->
+                                isFocused = fs.isFocused
+                            }
+                            .focusable()
+                    )
+                }
+            }
+        }
+        rule.runOnIdle { focusRequester.requestFocus() }
+
+        // Initially first focusable element would be focused
+        rule.waitForIdle()
+        rule.onNodeWithTag("test-card-0").assertIsFocused()
+
+        // Scroll down to the Carousel and check if it's brought into view on gaining focus
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
+        rule.waitForIdle()
+        rule.onNodeWithTag("featured-carousel").assertIsDisplayed()
+        assertThat(checkNodeCompletelyVisible(rule, "featured-carousel")).isTrue()
+
+        // Scroll down to last element, making sure the carousel is partially visible
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
+        rule.waitForIdle()
+        rule.onNodeWithTag("test-card-4").assertIsFocused()
+        rule.onNodeWithTag("featured-carousel").assertIsDisplayed()
+
+        // Scroll back to the carousel to check if it's brought into view on regaining focus
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
+        rule.waitForIdle()
+        rule.onNodeWithTag("featured-carousel").assertIsDisplayed()
+        assertThat(checkNodeCompletelyVisible(rule, "featured-carousel")).isTrue()
+    }
+
+    @Test
+    fun carousel_zeroSlideCount_shouldNotCrash() {
+        val testTag = "emptyCarousel"
+        rule.setContent {
+            Carousel(slideCount = 0, modifier = Modifier.testTag(testTag)) {}
+        }
+
+        rule.onNodeWithTag(testTag).assertExists()
+    }
+
+    @Test
+    fun carousel_oneSlideCount_shouldNotCrash() {
+        val testTag = "emptyCarousel"
+        rule.setContent {
+            Carousel(slideCount = 1, modifier = Modifier.testTag(testTag)) {}
+        }
+
+        rule.onNodeWithTag(testTag).assertExists()
+    }
+
+    @Test
+    fun carousel_manualScrolling_withFocusableItemsOnTop() {
+        rule.setContent {
+            Column {
+                Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+                    repeat(3) {
+                        SampleButton("Row-button-${it + 1}")
+                    }
+                }
+                SampleCarousel { index ->
+                    SampleButton("Button-${index + 1}")
+                }
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag("pager")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        // trigger recomposition on requesting focus
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+
+        // Check that slide 1 is in view and button 1 has focus
+        rule.onNodeWithText("Button-1").assertIsDisplayed()
+        rule.onNodeWithText("Button-1").assertIsFocused()
+
+        // press dpad right to scroll to next slide
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeBy(animationTime, false)
+        rule.waitForIdle()
+
+        // Check that slide 2 is in view and button 2 has focus
+        rule.onNodeWithText("Button-2").assertIsDisplayed()
+        // TODO: Fix button 2 isn't gaining focus
+        // rule.onNodeWithText("Button-2").assertIsFocused()
+
+        // Check if the first focusable element in parent has focus
+        rule.onNodeWithText("Row-button-1").assertIsNotFocused()
+
+        // press dpad left to scroll to previous slide
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeBy(animationTime, false)
+        rule.waitForIdle()
+
+        // Check that slide 1 is in view and button 1 has focus
+        rule.onNodeWithText("Button-1").assertIsDisplayed()
+        rule.onNodeWithText("Button-1").assertIsFocused()
+    }
+
+    @Test
+    fun carousel_manualScrolling_fastMultipleKeyPresses() {
+        val carouselState = CarouselState()
+        val tabs = listOf("Tab 1", "Tab 2", "Tab 3")
+
+        rule.setContent {
+            var selectedTabIndex by remember { mutableStateOf(0) }
+
+            Column {
+                TabRow(selectedTabIndex = selectedTabIndex) {
+                    tabs.forEachIndexed { index, tab ->
+                        Tab(
+                            selected = index == selectedTabIndex,
+                            onFocus = { selectedTabIndex = index },
+                        ) {
+                            Text(text = tab)
+                        }
+                    }
+                }
+
+                SampleCarousel(carouselState = carouselState, slideCount = 20) {
+                    SampleCarouselSlide(modifier = Modifier.testTag("slide-$it"), index = it)
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        rule.onNodeWithTag("pager").performSemanticsAction(SemanticsActions.RequestFocus)
+        rule.waitForIdle()
+
+        val slideProgression = listOf(6, 3, -4, 3, -6, 5, 3)
+
+        slideProgression.forEach {
+            if (it < 0) {
+                performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, it * -1)
+            } else {
+                performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, it)
+            }
+        }
+
+        rule.mainClock.advanceTimeBy(animationTime + overlayRenderWaitTime)
+
+        val finalSlide = slideProgression.sum()
+        rule.onNodeWithText("Play $finalSlide").assertIsFocused()
+
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 3)
+
+        rule.mainClock.advanceTimeBy((animationTime + overlayRenderWaitTime) * 3)
+
+        rule.onNodeWithText("Play ${finalSlide + 3}").assertIsFocused()
+    }
+
+    @Test
+    fun carousel_manualScrolling_onDpadLongPress() {
+        rule.setContent {
+            SampleCarousel(slideCount = 6) { index ->
+                SampleButton("Button ${index + 1}")
+            }
+        }
+
+        // Request focus for Carousel on start
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag("pager")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        // Trigger recomposition after requesting focus
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+
+        // Assert that Button 1 from first slide is focused
+        rule.onNodeWithText("Button 1").assertIsFocused()
+
+        // Trigger dpad right key long press
+        performLongKeyPress(rule, NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        // Advance time and trigger recomposition to switch to next slide
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeBy(delayBetweenSlides, false)
+        rule.waitForIdle()
+
+        // Assert that Button 2 from second slide is focused
+        rule.onNodeWithText("Button 2").assertIsFocused()
+
+        // Trigger dpad left key long press
+        performLongKeyPress(rule, NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        // Advance time and trigger recomposition to switch to previous slide
+        rule.mainClock.advanceTimeBy(delayBetweenSlides, false)
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+
+        // Assert that Button 1 from first slide is focused
+        rule.onNodeWithText("Button 1").assertIsFocused()
+    }
+
+    @Test
+    fun carousel_manualScrolling_ltr() {
+        rule.setContent {
+            SampleCarousel { index ->
+                SampleButton("Button ${index + 1}")
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag("pager")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        // current slide overlay render delay
+        rule.mainClock.advanceTimeBy(animationTime + overlayRenderWaitTime, false)
+        rule.mainClock.advanceTimeBy(animationTime, false)
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert that slide 1 is in view
+        rule.onNodeWithText("Button 1").assertIsDisplayed()
+
+        // advance time
+        rule.mainClock.advanceTimeBy(delayBetweenSlides + animationTime, false)
+        rule.mainClock.advanceTimeByFrame()
+
+        // go right once
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeBy(animationTime)
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert that slide 2 is in view
+        rule.onNodeWithText("Button 2").assertIsDisplayed()
+
+        // go left once
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert that slide 1 is in view
+        rule.onNodeWithText("Button 1").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_manualScrolling_rtl() {
+        rule.setContent {
+            CompositionLocalProvider(
+                LocalLayoutDirection provides LayoutDirection.Rtl
+            ) {
+                SampleCarousel {
+                    SampleButton("Button ${it + 1}")
+                }
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag("pager")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        // current slide overlay render delay
+        rule.mainClock.advanceTimeBy(animationTime + overlayRenderWaitTime, false)
+        rule.mainClock.advanceTimeBy(animationTime, false)
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert that slide 1 is in view
+        rule.onNodeWithText("Button 1").assertIsDisplayed()
+
+        // advance time
+        rule.mainClock.advanceTimeBy(delayBetweenSlides + animationTime, false)
+        rule.mainClock.advanceTimeByFrame()
+
+        // go right once
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeBy(animationTime)
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert that slide 2 is in view
+        rule.onNodeWithText("Button 2").assertIsDisplayed()
+
+        // go left once
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeBy(delayBetweenSlides + animationTime, false)
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert that slide 1 is in view
+        rule.onNodeWithText("Button 1").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_slideCountChangesDuringAnimation_shouldNotCrash() {
+        val slideDisplayDurationMs: Long = 100
+        var slideChanges = 0
+        // number of slides will fall from 4 to 2, but 4 slide transitions should happen without a
+        // crash
+        val minSuccessfulSlideChanges = 4
+        rule.setContent {
+            var slideCount by remember { mutableStateOf(4) }
+            LaunchedEffect(Unit) {
+                while (slideCount >= 2) {
+                    delay(slideDisplayDurationMs)
+                    slideCount--
+                }
+            }
+            SampleCarousel(
+                slideCount = slideCount,
+                timeToDisplaySlideMillis = slideDisplayDurationMs
+            ) { index ->
+                if (index >= slideCount) {
+                    // slideIndex requested should not be greater than slideCount. User could be
+                    // using a data-structure that could throw an IndexOutOfBoundsException.
+                    // This can happen when the slideCount changes during the transition between
+                    // slides.
+                    throw Exception("Index is larger, index=$index, slideCount=$slideCount")
+                }
+                slideChanges++
+            }
+        }
+
+        rule.waitUntil(timeoutMillis = 5000) { slideChanges > minSuccessfulSlideChanges }
+    }
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun SampleCarousel(
+    carouselState: CarouselState = remember { CarouselState() },
+    slideCount: Int = 3,
+    timeToDisplaySlideMillis: Long = delayBetweenSlides,
+    content: @Composable (index: Int) -> Unit
+) {
+    Carousel(
+        modifier = Modifier
+            .padding(5.dp)
+            .fillMaxWidth()
+            .height(200.dp)
+            .testTag("pager"),
+        carouselState = carouselState,
+        slideCount = slideCount,
+        timeToDisplaySlideMillis = timeToDisplaySlideMillis,
+        carouselIndicator = {
+            CarouselDefaults.IndicatorRow(
+                modifier = Modifier
+                    .align(Alignment.BottomEnd)
+                    .padding(16.dp)
+                    .testTag("indicator"),
+                activeSlideIndex = carouselState.activeSlideIndex,
+                slideCount = slideCount
+            )
+        },
+        content = content,
+    )
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun SampleCarouselSlide(
+    index: Int,
+    modifier: Modifier = Modifier,
+    overlayRenderWaitTime: Long = CarouselItemDefaults.OverlayEnterTransitionStartDelayMillis,
+    content: (@Composable () -> Unit) = { SampleButton("Play $index") },
+) {
+
+    CarouselItem(
+        modifier = modifier,
+        overlayEnterTransitionStartDelayMillis = overlayRenderWaitTime,
+        background = {
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .background(Color.Red)
+                    .border(2.dp, Color.Blue)
+            )
+        },
+        overlay = content
+    )
+}
+
+@Composable
+private fun SampleButton(text: String = "Play") {
+    var isFocused by remember { mutableStateOf(false) }
+    BasicText(
+        text = text,
+        modifier = Modifier
+            .size(100.dp, 20.dp)
+            .background(Color.Yellow)
+            .onFocusChanged { isFocused = it.isFocused }
+            .border(2.dp, if (isFocused) Color.Green else Color.Transparent)
+            .focusable(),
+    )
+}
+
+private fun checkNodeCompletelyVisible(
+    rule: ComposeContentTestRule,
+    tag: String,
+): Boolean {
+    rule.waitForIdle()
+
+    val rootRect = rule.onRoot().getUnclippedBoundsInRoot()
+    val itemRect = rule.onNodeWithTag(tag).getUnclippedBoundsInRoot()
+
+    return itemRect.left >= rootRect.left &&
+        itemRect.right <= rootRect.right &&
+        itemRect.top >= rootRect.top &&
+        itemRect.bottom <= rootRect.bottom
+}
+
+private fun performKeyPress(keyCode: Int, count: Int = 1, afterEachPress: () -> Unit = { }) {
+    repeat(count) {
+        InstrumentationRegistry
+            .getInstrumentation()
+            .sendKeyDownUpSync(keyCode)
+        afterEachPress()
+    }
+}
+
+private fun performLongKeyPress(
+    rule: ComposeContentTestRule,
+    keyCode: Int,
+    count: Int = 1
+) {
+    repeat(count) {
+        // Trigger the first key down event to simulate key press
+        val firstKeyDownEvent = KeyEvent(
+            SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),
+            KeyEvent.ACTION_DOWN, keyCode, 0, 0, 0, 0
+        )
+        rule.onRoot().performKeyPress(androidx.compose.ui.input.key.KeyEvent(firstKeyDownEvent))
+        rule.waitForIdle()
+
+        // Trigger multiple key down events with repeat count (>0) to simulate key long press
+        val repeatedKeyDownEvent = KeyEvent(
+            SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),
+            KeyEvent.ACTION_DOWN, keyCode, 5, 0, 0, 0
+        )
+        rule.onRoot().performKeyPress(androidx.compose.ui.input.key.KeyEvent(repeatedKeyDownEvent))
+        rule.waitForIdle()
+
+        // Trigger the final key up event to simulate key release
+        val keyUpEvent = KeyEvent(
+            SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),
+            KeyEvent.ACTION_UP, keyCode, 0, 0, 0, 0
+        )
+        rule.onRoot().performKeyPress(androidx.compose.ui.input.key.KeyEvent(keyUpEvent))
+        rule.waitForIdle()
+    }
+}
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/ImmersiveListTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/ImmersiveListTest.kt
new file mode 100644
index 0000000..cf85887
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/ImmersiveListTest.kt
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.unit.dp
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.tv.foundation.lazy.list.TvLazyColumn
+import androidx.tv.foundation.lazy.list.TvLazyRow
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+
+class ImmersiveListTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
+    @Test
+    fun immersiveList_scroll_backgroundChanges() {
+        rule.setContent {
+            ImmersiveList(
+                background = { index, _ ->
+                    AnimatedContent(targetState = index) {
+                        Box(
+                            modifier = Modifier
+                                .testTag("background-$it")
+                                .size(200.dp)
+                        ) {
+                            BasicText("background-$it")
+                        }
+                    }
+                }) {
+                    TvLazyRow {
+                        items(3) { index ->
+                            var isFocused by remember { mutableStateOf(false) }
+
+                            Box(
+                                modifier = Modifier
+                                    .size(100.dp)
+                                    .testTag("card-$index")
+                                    .background(if (isFocused) Color.Red else Color.Transparent)
+                                    .size(100.dp)
+                                    .onFocusChanged { isFocused = it.isFocused }
+                                    .immersiveListItem(index)
+                                    .focusable(true)
+                            ) {
+                                BasicText("card-$index")
+                            }
+                        }
+                    }
+            }
+        }
+
+        rule.waitForIdle()
+        rule.onNodeWithTag("card-0").performSemanticsAction(SemanticsActions.RequestFocus)
+        rule.waitForIdle()
+
+        rule.onNodeWithTag("card-0").assertIsFocused()
+        rule.onNodeWithTag("background-0").assertIsDisplayed()
+        rule.onNodeWithTag("background-1").assertDoesNotExist()
+        rule.onNodeWithTag("background-2").assertDoesNotExist()
+
+        rule.waitForIdle()
+        keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        rule.onNodeWithTag("card-1").assertIsFocused()
+        rule.onNodeWithTag("background-1").assertIsDisplayed()
+        rule.onNodeWithTag("background-0").assertDoesNotExist()
+        rule.onNodeWithTag("background-2").assertDoesNotExist()
+
+        rule.waitForIdle()
+        keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        rule.onNodeWithTag("card-0").assertIsFocused()
+        rule.onNodeWithTag("background-0").assertIsDisplayed()
+        rule.onNodeWithTag("background-1").assertDoesNotExist()
+        rule.onNodeWithTag("background-2").assertDoesNotExist()
+    }
+
+    @Test
+    fun immersiveList_scrollToRegainFocusInLazyColumn_checkBringIntoView() {
+        val focusRequesterList = mutableListOf<FocusRequester>()
+        for (item in 0..2) { focusRequesterList.add(FocusRequester()) }
+        setupContent(focusRequesterList)
+
+        // Initially first focusable element would be focused
+        rule.waitForIdle()
+        rule.onNodeWithTag("test-card-0").assertIsFocused()
+
+        // Scroll down to the Immersive List's first card
+        keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
+        rule.waitForIdle()
+        rule.onNodeWithTag("list-card-0").assertIsFocused()
+        rule.onNodeWithTag("immersive-list").assertIsDisplayed()
+        assertThat(checkNodeCompletelyVisible("immersive-list")).isTrue()
+
+        // Scroll down to last element, making sure the immersive list is partially visible
+        keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
+        rule.waitForIdle()
+        rule.onNodeWithTag("test-card-4").assertIsFocused()
+        rule.onNodeWithTag("immersive-list").assertIsDisplayed()
+
+        // Scroll back to the immersive list to check if it's brought into view on regaining focus
+        keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
+        rule.waitForIdle()
+        rule.onNodeWithTag("immersive-list").assertIsDisplayed()
+        assertThat(checkNodeCompletelyVisible("immersive-list")).isTrue()
+    }
+
+    @Test
+    fun immersiveList_scrollToRegainFocusInTvLazyColumn_checkBringIntoView() {
+        val focusRequesterList = mutableListOf<FocusRequester>()
+        for (item in 0..2) { focusRequesterList.add(FocusRequester()) }
+        setupTvContent(focusRequesterList)
+
+        // Initially first focusable element would be focused
+        rule.waitForIdle()
+        rule.onNodeWithTag("test-card-0").assertIsFocused()
+
+        // Scroll down to the Immersive List's first card
+        keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
+        rule.waitForIdle()
+        rule.onNodeWithTag("list-card-0").assertIsFocused()
+        rule.onNodeWithTag("immersive-list").assertIsDisplayed()
+        assertThat(checkNodeCompletelyVisible("immersive-list")).isTrue()
+
+        // Scroll down to last element, making sure the immersive list is partially visible
+        keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
+        rule.waitForIdle()
+        rule.onNodeWithTag("test-card-4").assertIsFocused()
+        rule.onNodeWithTag("immersive-list").assertIsDisplayed()
+
+        // Scroll back to the immersive list to check if it's brought into view on regaining focus
+        keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
+        rule.waitForIdle()
+        rule.onNodeWithTag("immersive-list").assertIsDisplayed()
+        assertThat(checkNodeCompletelyVisible("immersive-list")).isTrue()
+    }
+
+    private fun checkNodeCompletelyVisible(tag: String): Boolean {
+        rule.waitForIdle()
+
+        val rootRect = rule.onRoot().getUnclippedBoundsInRoot()
+        val itemRect = rule.onNodeWithTag(tag).getUnclippedBoundsInRoot()
+
+        return itemRect.left >= rootRect.left &&
+            itemRect.right <= rootRect.right &&
+            itemRect.top >= rootRect.top &&
+            itemRect.bottom <= rootRect.bottom
+    }
+
+    private fun setupContent(focusRequesterList: List<FocusRequester>) {
+        val focusRequester = FocusRequester()
+        rule.setContent {
+            LazyColumn {
+                items(3) {
+                    val modifier =
+                        if (it == 0) Modifier.focusRequester(focusRequester)
+                        else Modifier
+                    BasicText(
+                        text = "test-card-$it",
+                        modifier = modifier
+                            .testTag("test-card-$it")
+                            .size(200.dp)
+                            .focusable()
+                    )
+                }
+                item { TestImmersiveList(focusRequesterList) }
+                items(2) {
+                    BasicText(
+                        text = "test-card-${it + 3}",
+                        modifier = Modifier
+                            .testTag("test-card-${it + 3}")
+                            .size(200.dp)
+                            .focusable()
+                    )
+                }
+            }
+        }
+        rule.runOnIdle { focusRequester.requestFocus() }
+    }
+
+    private fun setupTvContent(focusRequesterList: List<FocusRequester>) {
+        val focusRequester = FocusRequester()
+        rule.setContent {
+            TvLazyColumn {
+                items(3) {
+                    val modifier =
+                        if (it == 0) Modifier.focusRequester(focusRequester)
+                        else Modifier
+                    BasicText(
+                        text = "test-card-$it",
+                        modifier = modifier
+                            .testTag("test-card-$it")
+                            .size(200.dp)
+                            .focusable()
+                    )
+                }
+                item { TestImmersiveList(focusRequesterList) }
+                items(2) {
+                    BasicText(
+                        text = "test-card-${it + 3}",
+                        modifier = Modifier
+                            .testTag("test-card-${it + 3}")
+                            .size(200.dp)
+                            .focusable()
+                    )
+                }
+            }
+        }
+        rule.runOnIdle { focusRequester.requestFocus() }
+    }
+
+    @OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
+    @Composable
+    private fun TestImmersiveList(focusRequesterList: List<FocusRequester>) {
+        val frList = remember { focusRequesterList }
+        ImmersiveList(
+            background = { index, _ ->
+                AnimatedContent(targetState = index) {
+                    Box(
+                        Modifier
+                            .testTag("background-$it")
+                            .fillMaxWidth()
+                            .height(400.dp)
+                            .border(2.dp, Color.Black, RectangleShape)
+                    ) {
+                        BasicText("background-$it")
+                    }
+                }
+            },
+            modifier = Modifier.testTag("immersive-list")
+        ) {
+            TvLazyRow {
+                items(frList.count()) { index ->
+                    var modifier = Modifier
+                        .testTag("list-card-$index")
+                        .size(50.dp)
+                    for (item in frList) {
+                        modifier = modifier.focusRequester(frList[index])
+                    }
+                    Box(
+                        modifier
+                            .immersiveListItem(index)
+                            .focusable(true)) {
+                        BasicText("list-card-$index")
+                    }
+                }
+            }
+        }
+    }
+
+    private fun keyPress(keyCode: Int, numberOfPresses: Int = 1) {
+        for (index in 0 until numberOfPresses)
+            InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode)
+    }
+}
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..7ce78931
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt
@@ -0,0 +1,391 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import android.os.Build
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.interaction.FocusInteraction
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.foundation.interaction.collectIsPressedAsState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertShape
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertTextEquals
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performKeyInput
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.test.pressKey
+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 kotlin.math.abs
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private fun assertFloatPrecision(a: Float, b: Float) =
+    Truth.assertThat(abs(a - b)).isLessThan(0.0001f)
+
+@OptIn(
+    ExperimentalComposeUiApi::class,
+    ExperimentalTestApi::class,
+    ExperimentalTvMaterial3Api::class
+)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class SurfaceTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private fun Int.toDp(): Dp = with(rule.density) { toDp() }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun originalOrderingWhenTheDefaultElevationIsUsed() {
+        rule.setContent {
+            Box(
+                Modifier
+                    .size(10.toDp())
+                    .semantics(mergeDescendants = true) {}
+                    .testTag("box")
+            ) {
+                Surface(
+                    onClick = {},
+                    shape = RectangleShape,
+                    color = Color.Yellow
+                ) {
+                    Box(Modifier.fillMaxSize())
+                }
+                Surface(
+                    onClick = {},
+                    shape = RectangleShape,
+                    color = Color.Green
+                ) {
+                    Box(Modifier.fillMaxSize())
+                }
+            }
+        }
+
+        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.setContent {
+            Surface(onClick = {}, tonalElevation = 2.toDp()) {
+                outerElevation = LocalAbsoluteTonalElevation.current
+                Surface(onClick = {}, tonalElevation = 4.toDp()) {
+                    innerElevation = LocalAbsoluteTonalElevation.current
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            innerElevation?.let { nnInnerElevation ->
+                assertFloatPrecision(nnInnerElevation.value, 6.toDp().value)
+            }
+            outerElevation?.let { nnOuterElevation ->
+                assertFloatPrecision(nnOuterElevation.value, 2.toDp().value)
+            }
+        }
+    }
+
+    /**
+     * Tests that composed modifiers applied to TvSurface 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.setContent {
+            CompositionLocalProvider(LocalContentColor provides Color.Red) {
+                Surface(
+                    modifier = Modifier.composed {
+                        contentColor = LocalContentColor.current
+                        Modifier
+                    },
+                    onClick = {},
+                    tonalElevation = 2.toDp(),
+                    contentColor = expectedColor
+                ) {}
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(contentColor).isEqualTo(expectedColor)
+        }
+    }
+
+    @Test
+    fun tvClickableOverload_semantics() {
+        val count = mutableStateOf(0)
+        rule.setContent {
+            Surface(
+                modifier = Modifier
+                    .testTag("tvSurface"),
+                onClick = { count.value += 1 }
+            ) {
+                Text("${count.value}")
+                Spacer(Modifier.size(30.toDp()))
+            }
+        }
+        rule.onNodeWithTag("tvSurface")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .assertHasClickAction()
+            .assertIsEnabled()
+            // since we merge descendants we should have text on the same node
+            .assertTextEquals("0")
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .assertTextEquals("1")
+    }
+
+    @Test
+    fun tvClickableOverload_customSemantics() {
+        val count = mutableStateOf(0)
+        rule.setContent {
+            Surface(
+                modifier = Modifier
+                    .testTag("tvSurface"),
+                onClick = { count.value += 1 },
+                role = Role.Checkbox
+            ) {
+                Text("${count.value}")
+                Spacer(Modifier.size(30.toDp()))
+            }
+        }
+        rule.onNodeWithTag("tvSurface")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .assertHasClickAction()
+            .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Checkbox))
+            .assertIsEnabled()
+            // since we merge descendants we should have text on the same node
+            .assertTextEquals("0")
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .assertTextEquals("1")
+    }
+
+    @Test
+    fun tvClickableOverload_clickAction() {
+        val count = mutableStateOf(0)
+
+        rule.setContent {
+            Surface(
+                modifier = Modifier
+                    .testTag("tvSurface"),
+                onClick = { count.value += 1 }
+            ) {
+                Spacer(modifier = Modifier.size(30.toDp()))
+            }
+        }
+        rule.onNodeWithTag("tvSurface")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        Truth.assertThat(count.value).isEqualTo(1)
+
+        rule.onNodeWithTag("tvSurface").performKeyInput { pressKey(Key.DirectionCenter) }
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        Truth.assertThat(count.value).isEqualTo(3)
+    }
+
+    @Test
+    fun tvSurface_onDisable_clickFails() {
+        val count = mutableStateOf(0f)
+        val enabled = mutableStateOf(true)
+
+        rule.setContent {
+            Surface(
+                modifier = Modifier
+                    .testTag("tvSurface"),
+                onClick = { count.value += 1 },
+                enabled = enabled.value
+            ) {
+                Spacer(Modifier.size(30.toDp()))
+            }
+        }
+        rule.onNodeWithTag("tvSurface")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .assertIsEnabled()
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+
+        Truth.assertThat(count.value).isEqualTo(1)
+        rule.runOnIdle {
+            enabled.value = false
+        }
+
+        rule.onNodeWithTag("tvSurface")
+            .assertIsNotEnabled()
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        Truth.assertThat(count.value).isEqualTo(1)
+    }
+
+    @Test
+    fun tvClickableOverload_interactionSource() {
+        val interactionSource = MutableInteractionSource()
+
+        lateinit var scope: CoroutineScope
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Surface(
+                modifier = Modifier
+                    .testTag("tvSurface"),
+                onClick = {},
+                interactionSource = interactionSource
+            ) {
+                Spacer(Modifier.size(30.toDp()))
+            }
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(interactions).isEmpty()
+        }
+
+        rule.onNodeWithTag("tvSurface")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .performKeyInput { keyDown(Key.DirectionCenter) }
+
+        rule.runOnIdle {
+            Truth.assertThat(interactions).hasSize(2)
+            Truth.assertThat(interactions[1]).isInstanceOf(PressInteraction.Press::class.java)
+        }
+
+        rule.onNodeWithTag("tvSurface").performKeyInput { keyUp(Key.DirectionCenter) }
+
+        rule.runOnIdle {
+            Truth.assertThat(interactions).hasSize(3)
+            Truth.assertThat(interactions.first()).isInstanceOf(FocusInteraction.Focus::class.java)
+            Truth.assertThat(interactions[1]).isInstanceOf(PressInteraction.Press::class.java)
+            Truth.assertThat(interactions[2]).isInstanceOf(PressInteraction.Release::class.java)
+            Truth.assertThat((interactions[2] as PressInteraction.Release).press)
+                .isEqualTo(interactions[1])
+        }
+    }
+
+    @Test
+    fun tvSurface_allowsFinalPassChildren() {
+        val hitTested = mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier.fillMaxSize()) {
+                Surface(
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .testTag("tvSurface"),
+                    onClick = {}
+                ) {
+                    Box(
+                        Modifier
+                            .fillMaxSize()
+                            .testTag("pressable")
+                            .pointerInput(Unit) {
+                                awaitEachGesture {
+                                    hitTested.value = true
+                                    val event = awaitPointerEvent(PointerEventPass.Final)
+                                    Truth
+                                        .assertThat(event.changes[0].isConsumed)
+                                        .isFalse()
+                                }
+                            }
+                    )
+                }
+            }
+        }
+        rule.onNodeWithTag("tvSurface").performSemanticsAction(SemanticsActions.RequestFocus)
+        rule.onNodeWithTag("pressable", true)
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        Truth.assertThat(hitTested.value).isTrue()
+    }
+
+    @OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
+    @Test
+    fun tvSurface_reactsToStateChange() {
+        val interactionSource = MutableInteractionSource()
+        var isPressed by mutableStateOf(false)
+
+        rule.setContent {
+            isPressed = interactionSource.collectIsPressedAsState().value
+            Surface(
+                modifier = Modifier
+                    .testTag("tvSurface")
+                    .size(100.toDp()),
+                onClick = {},
+                interactionSource = interactionSource
+            ) {}
+        }
+
+        with(rule.onNodeWithTag("tvSurface")) {
+            performSemanticsAction(SemanticsActions.RequestFocus)
+            assertIsFocused()
+            performKeyInput { keyDown(Key.DirectionCenter) }
+        }
+
+        rule.waitUntil(condition = { isPressed })
+
+        Truth.assertThat(isPressed).isTrue()
+    }
+}
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt
new file mode 100644
index 0000000..fd811f2
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt
@@ -0,0 +1,325 @@
+/*
+ * 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.tv.material3
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.DpRect
+import androidx.compose.ui.unit.dp
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Rule
+import org.junit.Test
+
+class TabRowTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun tabRow_shouldNotCrashWithOnly1Tab() {
+        val tabs = constructTabs(count = 1)
+
+        setContent(tabs)
+    }
+
+    @Test
+    fun tabRow_shouldNotCrashWithNoTabs() {
+        val tabs = constructTabs(count = 0)
+
+        setContent(tabs)
+    }
+
+    @Test
+    fun tabRow_firstTabIsSelected() {
+        val tabs = constructTabs()
+        val firstTab = tabs[0]
+
+        setContent(tabs)
+
+        rule.onNodeWithTag(firstTab).assertIsFocused()
+    }
+
+    @Test
+    fun tabRow_dPadRightMovesFocusToSecondTab() {
+        val tabs = constructTabs()
+        val firstTab = tabs[0]
+        val secondTab = tabs[1]
+
+        setContent(tabs)
+
+        // First tab should be focused
+        rule.onNodeWithTag(firstTab).assertIsFocused()
+
+        rule.waitForIdle()
+
+        // Move to next tab
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        rule.waitForIdle()
+
+        // Second tab should be focused
+        rule.onNodeWithTag(secondTab).assertIsFocused()
+    }
+
+    @Test
+    fun tabRow_dPadLeftMovesFocusToPreviousTab() {
+        val tabs = constructTabs()
+        val firstTab = tabs[0]
+        val secondTab = tabs[1]
+        val thirdTab = tabs[2]
+
+        setContent(tabs)
+
+        // First tab should be focused
+        rule.onNodeWithTag(firstTab).assertIsFocused()
+
+        rule.waitForIdle()
+
+        // Move to next tab
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        rule.waitForIdle()
+
+        // Second tab should be focused
+        rule.onNodeWithTag(secondTab).assertIsFocused()
+
+        // Move to next tab
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        rule.waitForIdle()
+
+        // Third tab should be focused
+        rule.onNodeWithTag(thirdTab).assertIsFocused()
+
+        // Move to previous tab
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        rule.waitForIdle()
+
+        // Second tab should be focused
+        rule.onNodeWithTag(secondTab).assertIsFocused()
+
+        // Move to previous tab
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        rule.waitForIdle()
+
+        // First tab should be focused
+        rule.onNodeWithTag(firstTab).assertIsFocused()
+    }
+
+    @OptIn(ExperimentalTvMaterial3Api::class)
+    @Test
+    fun tabRow_changeActiveTabOnClick() {
+        val tabs = constructTabs(count = 2)
+
+        val firstPanel = "Panel 1"
+        val secondPanel = "Panel 2"
+
+        setContent(
+            tabs,
+            contentBuilder = @Composable {
+                var focusedTabIndex by remember { mutableStateOf(0) }
+                var activeTabIndex by remember { mutableStateOf(focusedTabIndex) }
+                TabRowSample(
+                    tabs = tabs,
+                    selectedTabIndex = activeTabIndex,
+                    onFocus = { focusedTabIndex = it },
+                    onClick = { activeTabIndex = it },
+                    buildTabPanel = @Composable { index, _ ->
+                        BasicText(text = "Panel ${index + 1}")
+                    },
+                    indicator = @Composable { tabPositions ->
+                        // FocusedTab's indicator
+                        TabRowDefaults.PillIndicator(
+                            currentTabPosition = tabPositions[focusedTabIndex],
+                            activeColor = Color.Blue.copy(alpha = 0.4f),
+                            inactiveColor = Color.Transparent,
+                        )
+
+                        // SelectedTab's indicator
+                        TabRowDefaults.PillIndicator(
+                            currentTabPosition = tabPositions[activeTabIndex]
+                        )
+                    }
+                )
+            }
+        )
+
+        rule.onNodeWithText(firstPanel).assertIsDisplayed()
+
+        // Move focus to next tab
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        rule.waitForIdle()
+
+        rule.onNodeWithText(firstPanel).assertIsDisplayed()
+        rule.onNodeWithText(secondPanel).assertDoesNotExist()
+
+        // Click on the new focused tab
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_CENTER)
+
+        rule.onNodeWithText(firstPanel).assertDoesNotExist()
+        rule.onNodeWithText(secondPanel).assertIsDisplayed()
+    }
+
+    private fun setContent(
+        tabs: List<String>,
+        contentBuilder: @Composable () -> Unit = {
+            var selectedTabIndex by remember { mutableStateOf(0) }
+            TabRowSample(
+                tabs = tabs,
+                selectedTabIndex = selectedTabIndex,
+                onFocus = { selectedTabIndex = it }
+            )
+        },
+    ) {
+        rule.setContent {
+            contentBuilder()
+        }
+
+        rule.waitForIdle()
+
+        // Move the focus TabRow
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN)
+
+        rule.waitForIdle()
+    }
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun TabRowSample(
+    tabs: List<String>,
+    selectedTabIndex: Int,
+    onFocus: (index: Int) -> Unit = {},
+    onClick: (index: Int) -> Unit = onFocus,
+    buildTab: @Composable ((index: Int, tab: String) -> Unit) = @Composable { index, tab ->
+        TabSample(
+            selected = selectedTabIndex == index,
+            onFocus = { onFocus(index) },
+            onClick = { onClick(index) },
+            modifier = Modifier.testTag(tab),
+        )
+    },
+    indicator: @Composable ((tabPositions: List<DpRect>) -> Unit)? = null,
+    buildTabPanel: @Composable ((index: Int, tab: String) -> Unit) = @Composable { _, tab ->
+        BasicText(text = tab)
+    },
+) {
+    val fr = remember { FocusRequester() }
+
+    Column(
+        modifier = Modifier
+            .fillMaxSize()
+            .background(Color.Black)
+    ) {
+        // Added so that this can get focus and pass it to the tab row
+        Box(
+            modifier = Modifier
+                .size(50.dp)
+                .focusRequester(fr)
+                .background(Color.White)
+                .focusable()
+        )
+
+        // Send focus to button
+        LaunchedEffect(Unit) {
+            fr.requestFocus()
+        }
+
+        if (indicator != null) {
+            TabRow(
+                selectedTabIndex = selectedTabIndex,
+                indicator = indicator,
+                separator = { Spacer(modifier = Modifier.width(12.dp)) },
+            ) {
+                tabs.forEachIndexed { index, tab -> buildTab(index, tab) }
+            }
+        } else {
+            TabRow(
+                selectedTabIndex = selectedTabIndex,
+                separator = { Spacer(modifier = Modifier.width(12.dp)) },
+            ) {
+                tabs.forEachIndexed { index, tab -> buildTab(index, tab) }
+            }
+        }
+
+        tabs.elementAtOrNull(selectedTabIndex)?.let { buildTabPanel(selectedTabIndex, it) }
+    }
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun TabSample(
+    selected: Boolean,
+    modifier: Modifier = Modifier,
+    onFocus: () -> Unit = {},
+    onClick: () -> Unit = {},
+    tag: String = "Tab",
+) {
+    Tab(
+        selected = selected,
+        onFocus = onFocus,
+        onClick = onClick,
+        modifier = modifier
+            .width(100.dp)
+            .height(50.dp)
+            .testTag(tag)
+            .border(2.dp, Color.White, RoundedCornerShape(50))
+    ) {}
+}
+
+private fun performKeyPress(keyCode: Int, count: Int = 1) {
+    for (i in 1..count) {
+        InstrumentationRegistry
+            .getInstrumentation()
+            .sendKeyDownUpSync(keyCode)
+    }
+}
+
+private fun constructTabs(
+    count: Int = 3,
+    buildTab: (index: Int) -> String = { "Season $it" }
+): List<String> = List(count, buildTab)
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/TextTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/TextTest.kt
new file mode 100644
index 0000000..5293277
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/TextTest.kt
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.test.assertTextEquals
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.unit.sp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class TextTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val ExpectedTextStyle = TextStyle(
+        color = Color.Blue,
+        textAlign = TextAlign.End,
+        fontSize = 32.sp,
+        fontStyle = FontStyle.Italic,
+        letterSpacing = 0.3.em
+    )
+
+    private val TestText = "TestText"
+
+    @Test
+    fun inheritsThemeTextStyle() {
+        var textColor: Color? = null
+        var textAlign: TextAlign? = null
+        var fontSize: TextUnit? = null
+        var fontStyle: FontStyle? = null
+        var letterSpacing: TextUnit? = null
+        rule.setContent {
+            ProvideTextStyle(ExpectedTextStyle) {
+                Box(Modifier.background(Color.White)) {
+                    Text(
+                        TestText,
+                        onTextLayout = {
+                            textColor = it.layoutInput.style.color
+                            textAlign = it.layoutInput.style.textAlign
+                            fontSize = it.layoutInput.style.fontSize
+                            fontStyle = it.layoutInput.style.fontStyle
+                            letterSpacing = it.layoutInput.style.letterSpacing
+                        }
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(textColor).isEqualTo(ExpectedTextStyle.color)
+            Truth.assertThat(textAlign).isEqualTo(ExpectedTextStyle.textAlign)
+            Truth.assertThat(fontSize).isEqualTo(ExpectedTextStyle.fontSize)
+            Truth.assertThat(fontStyle).isEqualTo(ExpectedTextStyle.fontStyle)
+            Truth.assertThat(letterSpacing).isEqualTo(ExpectedTextStyle.letterSpacing)
+        }
+    }
+
+    @Test
+    fun settingCustomTextStyle() {
+        var textColor: Color? = null
+        var textAlign: TextAlign? = null
+        var fontSize: TextUnit? = null
+        var fontStyle: FontStyle? = null
+        var letterSpacing: TextUnit? = null
+        val testStyle = TextStyle(
+            color = Color.Green,
+            textAlign = TextAlign.Center,
+            fontSize = 16.sp,
+            fontStyle = FontStyle.Normal,
+            letterSpacing = 0.6.em
+        )
+        rule.setContent {
+            ProvideTextStyle(ExpectedTextStyle) {
+                Box(Modifier.background(Color.White)) {
+                    Text(
+                        TestText,
+                        style = testStyle,
+                        onTextLayout = {
+                            textColor = it.layoutInput.style.color
+                            textAlign = it.layoutInput.style.textAlign
+                            fontSize = it.layoutInput.style.fontSize
+                            fontStyle = it.layoutInput.style.fontStyle
+                            letterSpacing = it.layoutInput.style.letterSpacing
+                        }
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(textColor).isEqualTo(testStyle.color)
+            Truth.assertThat(textAlign).isEqualTo(testStyle.textAlign)
+            Truth.assertThat(fontSize).isEqualTo(testStyle.fontSize)
+            Truth.assertThat(fontStyle).isEqualTo(testStyle.fontStyle)
+            Truth.assertThat(letterSpacing).isEqualTo(testStyle.letterSpacing)
+        }
+    }
+
+    @Test
+    fun settingParametersExplicitly() {
+        var textColor: Color? = null
+        var textAlign: TextAlign? = null
+        var fontSize: TextUnit? = null
+        var fontStyle: FontStyle? = null
+        var letterSpacing: TextUnit? = null
+        val expectedColor = Color.Green
+        val expectedTextAlign = TextAlign.Center
+        val expectedFontSize = 16.sp
+        val expectedFontStyle = FontStyle.Normal
+        val expectedLetterSpacing = 0.6.em
+
+        rule.setContent {
+            ProvideTextStyle(ExpectedTextStyle) {
+                Box(Modifier.background(Color.White)) {
+                    Text(
+                        TestText,
+                        color = expectedColor,
+                        textAlign = expectedTextAlign,
+                        fontSize = expectedFontSize,
+                        fontStyle = expectedFontStyle,
+                        letterSpacing = expectedLetterSpacing,
+                        onTextLayout = {
+                            textColor = it.layoutInput.style.color
+                            textAlign = it.layoutInput.style.textAlign
+                            fontSize = it.layoutInput.style.fontSize
+                            fontStyle = it.layoutInput.style.fontStyle
+                            letterSpacing = it.layoutInput.style.letterSpacing
+                        }
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            // explicit parameters should override values from the style.
+            Truth.assertThat(textColor).isEqualTo(expectedColor)
+            Truth.assertThat(textAlign).isEqualTo(expectedTextAlign)
+            Truth.assertThat(fontSize).isEqualTo(expectedFontSize)
+            Truth.assertThat(fontStyle).isEqualTo(expectedFontStyle)
+            Truth.assertThat(letterSpacing).isEqualTo(expectedLetterSpacing)
+        }
+    }
+
+    // Not really an expected use-case, but we should ensure the behavior here is consistent.
+    @Test
+    fun settingColorAndTextStyle() {
+        var textColor: Color? = null
+        var textAlign: TextAlign? = null
+        var fontSize: TextUnit? = null
+        var fontStyle: FontStyle? = null
+        var letterSpacing: TextUnit? = null
+        val expectedColor = Color.Green
+        val expectedTextAlign = TextAlign.Center
+        val expectedFontSize = 16.sp
+        val expectedFontStyle = FontStyle.Normal
+        val expectedLetterSpacing = 0.6.em
+        rule.setContent {
+            ProvideTextStyle(ExpectedTextStyle) {
+                Box(Modifier.background(Color.White)) {
+                    // Set both color and style
+                    Text(
+                        TestText,
+                        color = expectedColor,
+                        textAlign = expectedTextAlign,
+                        fontSize = expectedFontSize,
+                        fontStyle = expectedFontStyle,
+                        letterSpacing = expectedLetterSpacing,
+                        style = ExpectedTextStyle,
+                        onTextLayout = {
+                            textColor = it.layoutInput.style.color
+                            textAlign = it.layoutInput.style.textAlign
+                            fontSize = it.layoutInput.style.fontSize
+                            fontStyle = it.layoutInput.style.fontStyle
+                            letterSpacing = it.layoutInput.style.letterSpacing
+                        }
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            // explicit parameters should override values from the style.
+            Truth.assertThat(textColor).isEqualTo(expectedColor)
+            Truth.assertThat(textAlign).isEqualTo(expectedTextAlign)
+            Truth.assertThat(fontSize).isEqualTo(expectedFontSize)
+            Truth.assertThat(fontStyle).isEqualTo(expectedFontStyle)
+            Truth.assertThat(letterSpacing).isEqualTo(expectedLetterSpacing)
+        }
+    }
+
+    @Test
+    fun testSemantics() {
+        rule.setContent {
+            ProvideTextStyle(ExpectedTextStyle) {
+                Box(Modifier.background(Color.White)) {
+                    Text(
+                        TestText,
+                        modifier = Modifier.testTag("text")
+                    )
+                }
+            }
+        }
+
+        val textLayoutResults = mutableListOf<TextLayoutResult>()
+        rule.onNodeWithTag("text")
+            .assertTextEquals(TestText)
+            .performSemanticsAction(SemanticsActions.GetTextLayoutResult) { it(textLayoutResults) }
+        assert(textLayoutResults.size == 1) { "TextLayoutResult is null" }
+    }
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/BringIntoViewIfChildrenAreFocused.kt b/tv/tv-material/src/main/java/androidx/tv/material/BringIntoViewIfChildrenAreFocused.kt
deleted file mode 100644
index 0c4e5a8..0000000
--- a/tv/tv-material/src/main/java/androidx/tv/material/BringIntoViewIfChildrenAreFocused.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.material
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.relocation.BringIntoViewResponder
-import androidx.compose.foundation.relocation.bringIntoViewResponder
-import androidx.compose.runtime.remember
-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.layout.onSizeChanged
-import androidx.compose.ui.platform.debugInspectorInfo
-
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@OptIn(ExperimentalFoundationApi::class)
-internal fun Modifier.bringIntoViewIfChildrenAreFocused(): Modifier = composed(
-    inspectorInfo = debugInspectorInfo { name = "bringIntoViewIfChildrenAreFocused" },
-    factory = {
-        var myRect: Rect = Rect.Zero
-        this
-            .onSizeChanged {
-                myRect = Rect(Offset.Zero, Offset(it.width.toFloat(), it.height.toFloat()))
-            }
-            .bringIntoViewResponder(
-                remember {
-                    object : BringIntoViewResponder {
-                        // return the current rectangle and ignoring the child rectangle received.
-                        @ExperimentalFoundationApi
-                        override fun calculateRectForParent(localRect: Rect): Rect = myRect
-
-                        // The container is not expected to be scrollable. Hence the child is
-                        // already in view with respect to the container.
-                        @ExperimentalFoundationApi
-                        override suspend fun bringChildIntoView(localRect: () -> Rect?) {}
-                    }
-                }
-            )
-    }
-)
\ No newline at end of file
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/ContentColor.kt b/tv/tv-material/src/main/java/androidx/tv/material/ContentColor.kt
deleted file mode 100644
index f1b11f5..0000000
--- a/tv/tv-material/src/main/java/androidx/tv/material/ContentColor.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.material
-
-import androidx.compose.runtime.compositionLocalOf
-import androidx.compose.ui.graphics.Color
-
-/**
- * CompositionLocal containing the preferred content color for a given position in the hierarchy.
- * This typically represents the `on` color for a color in `ColorScheme`. For example, if the
- * background color is `surface`, this color is typically set to
- * `onSurface`.
- *
- * This color should be used for any typography / iconography, to ensure that the color of these
- * adjusts when the background color changes. For example, on a dark background, text should be
- * light, and on a light background, text should be dark.
- *
- * Defaults to [Color.Black] if no color has been explicitly set.
- */
-val LocalContentColor = compositionLocalOf { Color.Black }
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/ExperimentalTvMaterialApi.kt b/tv/tv-material/src/main/java/androidx/tv/material/ExperimentalTvMaterialApi.kt
deleted file mode 100644
index ebac64e..0000000
--- a/tv/tv-material/src/main/java/androidx/tv/material/ExperimentalTvMaterialApi.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.material
-
-@RequiresOptIn(
-    "This tv-material API is experimental and likely to change or be removed in the future."
-)
-@Retention(AnnotationRetention.BINARY)
-annotation class ExperimentalTvMaterialApi
\ No newline at end of file
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/Tab.kt b/tv/tv-material/src/main/java/androidx/tv/material/Tab.kt
deleted file mode 100644
index 48b306e..0000000
--- a/tv/tv-material/src/main/java/androidx/tv/material/Tab.kt
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.material
-
-import androidx.compose.animation.animateColorAsState
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.interaction.Interaction
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.RowScope
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.semantics.role
-import androidx.compose.ui.semantics.selected
-import androidx.compose.ui.semantics.semantics
-
-/**
- * Material Design tab.
- *
- * A default Tab, also known as a Primary Navigation Tab. Tabs organize content across different
- * screens, data sets, and other interactions.
- *
- * This should typically be used inside of a [TabRow], see the corresponding documentation for
- * example usage.
- *
- * @param selected whether this tab is selected or not
- * @param onFocus called when this tab is focused
- * @param modifier the [Modifier] to be applied to this tab
- * @param onClick called when this tab is clicked (with D-Pad Center)
- * @param enabled controls the enabled state of this tab. When `false`, this component will not
- * respond to user input, and it will appear visually disabled and disabled to accessibility
- * services.
- * @param colors these will be used by the tab when in different states (focused,
- * selected, etc.)
- * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
- * for this tab. You can create and pass in your own `remember`ed instance to observe [Interaction]s
- * and customize the appearance / behavior of this tab in different states.
- * @param content content of the [Tab]
- */
-@Composable
-fun Tab(
-  selected: Boolean,
-  onFocus: () -> Unit,
-  modifier: Modifier = Modifier,
-  onClick: () -> Unit = { },
-  enabled: Boolean = true,
-  colors: TabColors = TabDefaults.pillIndicatorTabColors(),
-  interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-  content: @Composable RowScope.() -> Unit
-) {
-  val contentColor by
-    animateColorAsState(
-      getTabContentColor(
-        colors = colors,
-        anyTabFocused = LocalTabRowHasFocus.current,
-        selected = selected,
-        enabled = enabled,
-      )
-    )
-  CompositionLocalProvider(LocalContentColor provides contentColor) {
-    Row(
-      modifier =
-        modifier
-          .semantics {
-            this.selected = selected
-            this.role = Role.Tab
-          }
-          .onFocusChanged {
-            if (it.isFocused) {
-              onFocus()
-            }
-          }
-          .focusable(enabled = enabled, interactionSource)
-          .clickable(
-            enabled = enabled,
-            role = Role.Tab,
-            onClick = onClick,
-          ),
-      horizontalArrangement = Arrangement.Center,
-      verticalAlignment = Alignment.CenterVertically,
-      content = content
-    )
-  }
-}
-
-/**
- * Represents the colors used in a tab in different states.
- *
- * - See [TabDefaults.pillIndicatorTabColors] for the default colors used in a [Tab] when using a
- * Pill indicator.
- * - See [TabDefaults.underlinedIndicatorTabColors] for the default colors used in a [Tab] when
- * using an Underlined indicator
- */
-class TabColors
-internal constructor(
-  private val activeContentColor: Color,
-  private val selectedContentColor: Color,
-  private val focusedContentColor: Color,
-  private val disabledActiveContentColor: Color,
-  private val disabledSelectedContentColor: Color,
-) {
-  /**
-   * Represents the content color for this tab, depending on whether it is inactive and [enabled]
-   *
-   * [Tab] is inactive when the [TabRow] is not focused
-   *
-   * @param enabled whether the button is enabled
-   */
-  internal fun inactiveContentColor(enabled: Boolean): Color {
-    return if (enabled) activeContentColor.copy(alpha = 0.4f)
-    else disabledActiveContentColor.copy(alpha = 0.4f)
-  }
-
-  /**
-   * Represents the content color for this tab, depending on whether it is active and [enabled]
-   *
-   * [Tab] is active when some other [Tab] is focused
-   *
-   * @param enabled whether the button is enabled
-   */
-  internal fun activeContentColor(enabled: Boolean): Color {
-    return if (enabled) activeContentColor else disabledActiveContentColor
-  }
-
-  /**
-   * Represents the content color for this tab, depending on whether it is selected and [enabled]
-   *
-   * [Tab] is selected when the current [Tab] is selected and not focused
-   *
-   * @param enabled whether the button is enabled
-   */
-  internal fun selectedContentColor(enabled: Boolean): Color {
-    return if (enabled) selectedContentColor else disabledSelectedContentColor
-  }
-
-  /**
-   * Represents the content color for this tab, depending on whether it is focused
-   *
-   * * [Tab] is focused when the current [Tab] is selected and focused
-   */
-  internal fun focusedContentColor(): Color {
-    return focusedContentColor
-  }
-
-  override fun equals(other: Any?): Boolean {
-    if (this === other) return true
-    if (other == null || other !is TabColors) return false
-
-    if (activeContentColor != other.activeContentColor(true)) return false
-    if (selectedContentColor != other.selectedContentColor(true)) return false
-    if (focusedContentColor != other.focusedContentColor()) return false
-
-    if (disabledActiveContentColor != other.activeContentColor(false)) return false
-    if (disabledSelectedContentColor != other.selectedContentColor(false)) return false
-
-    return true
-  }
-
-  override fun hashCode(): Int {
-    var result = activeContentColor.hashCode()
-    result = 31 * result + selectedContentColor.hashCode()
-    result = 31 * result + focusedContentColor.hashCode()
-    result = 31 * result + disabledActiveContentColor.hashCode()
-    result = 31 * result + disabledSelectedContentColor.hashCode()
-    return result
-  }
-}
-
-object TabDefaults {
-  /**
-   * [Tab]'s content colors to in conjunction with underlined indicator
-   */
-  // TODO: get selected & focused values from theme
-  @Composable
-  fun underlinedIndicatorTabColors(
-    activeContentColor: Color = LocalContentColor.current,
-    selectedContentColor: Color = Color(0xFFC9C2E8),
-    focusedContentColor: Color = Color(0xFFC9BFFF),
-    disabledActiveContentColor: Color = activeContentColor,
-    disabledSelectedContentColor: Color = selectedContentColor,
-  ): TabColors =
-    TabColors(
-      activeContentColor = activeContentColor,
-      selectedContentColor = selectedContentColor,
-      focusedContentColor = focusedContentColor,
-      disabledActiveContentColor = disabledActiveContentColor,
-      disabledSelectedContentColor = disabledSelectedContentColor,
-    )
-
-  /**
-   * [Tab]'s content colors to in conjunction with pill indicator
-   */
-  // TODO: get selected & focused values from theme
-  @Composable
-  fun pillIndicatorTabColors(
-    activeContentColor: Color = LocalContentColor.current,
-    selectedContentColor: Color = Color(0xFFE5DEFF),
-    focusedContentColor: Color = Color(0xFF313033),
-    disabledActiveContentColor: Color = activeContentColor,
-    disabledSelectedContentColor: Color = selectedContentColor,
-  ): TabColors =
-    TabColors(
-      activeContentColor = activeContentColor,
-      selectedContentColor = selectedContentColor,
-      focusedContentColor = focusedContentColor,
-      disabledActiveContentColor = disabledActiveContentColor,
-      disabledSelectedContentColor = disabledSelectedContentColor,
-    )
-}
-
-/** Returns the [Tab]'s content color based on focused/selected state */
-private fun getTabContentColor(
-  colors: TabColors,
-  anyTabFocused: Boolean,
-  selected: Boolean,
-  enabled: Boolean,
-): Color =
-  when {
-    anyTabFocused && selected -> colors.focusedContentColor()
-    selected -> colors.selectedContentColor(enabled)
-    anyTabFocused -> colors.activeContentColor(enabled)
-    else -> colors.inactiveContentColor(enabled)
-  }
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/TabRow.kt b/tv/tv-material/src/main/java/androidx/tv/material/TabRow.kt
deleted file mode 100644
index 64f0c77..0000000
--- a/tv/tv-material/src/main/java/androidx/tv/material/TabRow.kt
+++ /dev/null
@@ -1,283 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.material
-
-import androidx.compose.animation.animateColorAsState
-import androidx.compose.animation.core.animateDpAsState
-import androidx.compose.foundation.background
-import androidx.compose.foundation.horizontalScroll
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.selection.selectableGroup
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.compositionLocalOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clipToBounds
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.layout.SubcomposeLayout
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.DpRect
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.height
-import androidx.compose.ui.unit.width
-import androidx.compose.ui.zIndex
-
-/**
- * TV-Material Design Horizontal TabRow
- *
- * Display all tabs in a set simultaneously and if the tabs exceed the container size, it has
- * scrolling to navigate to next tab. They are best for switching between related content quickly,
- * such as between transportation methods in a map. To navigate between tabs, use d-pad left or
- * d-pad right when focused.
- *
- * A TvTabRow contains a row of []s, and displays an indicator underneath the currently selected
- * tab. A TvTabRow places its tabs offset from the starting edge, and allows scrolling to tabs that
- * are placed off screen.
- *
- * Examples:
- * @sample androidx.tv.samples.PillIndicatorTabRow
- * @sample androidx.tv.samples.UnderlinedIndicatorTabRow
- * @sample androidx.tv.samples.TabRowWithDebounce
- * @sample androidx.tv.samples.OnClickNavigation
- *
- * @param selectedTabIndex the index of the currently selected tab
- * @param modifier the [Modifier] to be applied to this tab row
- * @param containerColor the color used for the background of this tab row
- * @param contentColor the primary color used in the tabs
- * @param separator use this composable to add a separator between the tabs
- * @param indicator used to indicate which tab is currently selected and/or focused
- * @param tabs a composable which will render all the tabs
- */
-@Composable
-fun TabRow(
-  selectedTabIndex: Int,
-  modifier: Modifier = Modifier,
-  containerColor: Color = TabRowDefaults.ContainerColor,
-  contentColor: Color = TabRowDefaults.contentColor(),
-  separator: @Composable () -> Unit = { TabRowDefaults.TabSeparator() },
-  indicator: @Composable (tabPositions: List<DpRect>) -> Unit =
-    @Composable { tabPositions ->
-      tabPositions.getOrNull(selectedTabIndex)?.let {
-        TabRowDefaults.PillIndicator(currentTabPosition = it)
-      }
-    },
-  tabs: @Composable () -> Unit
-) {
-  val scrollState = rememberScrollState()
-  var isAnyTabFocused by remember { mutableStateOf(false) }
-
-  CompositionLocalProvider(
-    LocalTabRowHasFocus provides isAnyTabFocused,
-    LocalContentColor provides contentColor
-  ) {
-    SubcomposeLayout(
-      modifier =
-        modifier
-          .background(containerColor)
-          .clipToBounds()
-          .horizontalScroll(scrollState)
-          .onFocusChanged { isAnyTabFocused = it.hasFocus }
-          .selectableGroup()
-    ) { constraints ->
-      // Tab measurables
-      val tabMeasurables = subcompose(TabRowSlots.Tabs, tabs)
-
-      // Tab placeables
-      val tabPlaceables =
-        tabMeasurables.map { it.measure(constraints.copy(minWidth = 0, minHeight = 0)) }
-      val tabsCount = tabMeasurables.size
-      val separatorsCount = tabsCount - 1
-
-      // Separators
-      val separators = @Composable { repeat(separatorsCount) { separator() } }
-      val separatorMeasurables = subcompose(TabRowSlots.Separator, separators)
-      val separatorPlaceables =
-        separatorMeasurables.map { it.measure(constraints.copy(minWidth = 0, minHeight = 0)) }
-      val separatorWidth = separatorPlaceables.firstOrNull()?.width ?: 0
-
-      val layoutWidth = tabPlaceables.sumOf { it.width } + separatorsCount * separatorWidth
-      val layoutHeight =
-        (tabMeasurables.maxOfOrNull { it.maxIntrinsicHeight(Constraints.Infinity) } ?: 0)
-          .coerceAtLeast(0)
-
-      // Position the children
-      layout(layoutWidth, layoutHeight) {
-
-        // Place the tabs
-        val tabPositions = mutableListOf<DpRect>()
-        var left = 0
-        tabPlaceables.forEachIndexed { index, tabPlaceable ->
-          // place the tab
-          tabPlaceable.placeRelative(left, 0)
-
-          tabPositions.add(
-            this@SubcomposeLayout.buildTabPosition(placeable = tabPlaceable, initialLeft = left)
-          )
-          left += tabPlaceable.width
-
-          // place the separator
-          if (tabPlaceables.lastIndex != index) {
-            separatorPlaceables[index].placeRelative(left, 0)
-          }
-
-          left += separatorWidth
-        }
-
-        // Place the indicator
-        subcompose(TabRowSlots.Indicator) { indicator(tabPositions) }
-          .forEach { it.measure(Constraints.fixed(layoutWidth, layoutHeight)).placeRelative(0, 0) }
-      }
-    }
-  }
-}
-
-object TabRowDefaults {
-  /** Color of the background of a tab */
-  val ContainerColor = Color.Transparent
-
-  /** Space between tabs in the tab row */
-  @Composable
-  fun TabSeparator() {
-    Spacer(modifier = Modifier.width(20.dp))
-  }
-
-  /** Default accent color for the TabRow */
-  // TODO: Use value from a theme
-  @Composable fun contentColor(): Color = Color(0xFFC9C5D0)
-
-  /**
-   * Adds a pill indicator behind the tab
-   *
-   * @param currentTabPosition position of the current selected tab
-   * @param modifier modifier to be applied to the indicator
-   * @param activeColor color of indicator when [TabRow] is active
-   * @param inactiveColor color of indicator when [TabRow] is inactive
-   */
-  @Composable
-  fun PillIndicator(
-    currentTabPosition: DpRect,
-    modifier: Modifier = Modifier,
-    activeColor: Color = Color(0xFFE5E1E6),
-    inactiveColor: Color = Color(0xFF484362).copy(alpha = 0.4f)
-  ) {
-    val anyTabFocused = LocalTabRowHasFocus.current
-    val width by animateDpAsState(targetValue = currentTabPosition.width)
-    val height = currentTabPosition.height
-    val leftOffset by animateDpAsState(targetValue = currentTabPosition.left)
-    val topOffset = currentTabPosition.top
-
-    val pillColor by
-      animateColorAsState(targetValue = if (anyTabFocused) activeColor else inactiveColor)
-
-    Box(
-      modifier
-        .fillMaxWidth()
-        .wrapContentSize(Alignment.BottomStart)
-        .offset(x = leftOffset, y = topOffset)
-        .width(width)
-        .height(height)
-        .background(color = pillColor, shape = RoundedCornerShape(50))
-        .zIndex(-1f)
-    )
-  }
-
-  /**
-   * Adds an underlined indicator below the tab
-   *
-   * @param currentTabPosition position of the current selected tab
-   * @param modifier modifier to be applied to the indicator
-   * @param activeColor color of indicator when [TabRow] is active
-   * @param inactiveColor color of indicator when [TabRow] is inactive
-   */
-  @Composable
-  fun UnderlinedIndicator(
-    currentTabPosition: DpRect,
-    modifier: Modifier = Modifier,
-    activeColor: Color = Color(0xFFC9BFFF),
-    inactiveColor: Color = Color(0xFFC9C2E8)
-  ) {
-    val anyTabFocused = LocalTabRowHasFocus.current
-    val unfocusedUnderlineWidth = 10.dp
-    val indicatorHeight = 2.dp
-    val width by
-      animateDpAsState(
-        targetValue = if (anyTabFocused) currentTabPosition.width else unfocusedUnderlineWidth
-      )
-    val leftOffset by
-      animateDpAsState(
-        targetValue =
-          if (anyTabFocused) {
-            currentTabPosition.left
-          } else {
-            val tabCenter = currentTabPosition.left + currentTabPosition.width / 2
-            tabCenter - unfocusedUnderlineWidth / 2
-          }
-      )
-
-    val underlineColor by
-      animateColorAsState(targetValue = if (anyTabFocused) activeColor else inactiveColor)
-
-    Box(
-      modifier
-        .fillMaxWidth()
-        .wrapContentSize(Alignment.BottomStart)
-        .offset(x = leftOffset)
-        .width(width)
-        .height(indicatorHeight)
-        .background(color = underlineColor)
-    )
-  }
-}
-
-/** A provider to store whether any [Tab] is focused inside the [TabRow] */
-internal val LocalTabRowHasFocus = compositionLocalOf { false }
-
-/** Slots for [TabRow]'s content */
-private enum class TabRowSlots {
-  Tabs,
-  Indicator,
-  Separator
-}
-
-/** Builds TabPosition based on placeable */
-private fun Density.buildTabPosition(
-  placeable: Placeable,
-  initialLeft: Int = 0,
-  initialTop: Int = 0,
-): DpRect =
-  DpRect(
-    left = initialLeft.toDp(),
-    right = (initialLeft + placeable.width).toDp(),
-    top = initialTop.toDp(),
-    bottom = (initialTop + placeable.height).toDp(),
-  )
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
deleted file mode 100644
index de7a1b3..0000000
--- a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
+++ /dev/null
@@ -1,392 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.material.carousel
-
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.AnimatedVisibilityScope
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.with
-import androidx.compose.foundation.background
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.foundation.layout.Row
-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.LaunchedEffect
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusDirection
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.FocusState
-import androidx.compose.ui.focus.focusProperties
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.tv.material.ExperimentalTvMaterialApi
-import androidx.tv.material.bringIntoViewIfChildrenAreFocused
-import java.lang.Math.floorMod
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.yield
-
-/**
- * Composes a hero card rotator to highlight a piece of content.
- *
- * Examples:
- * @sample androidx.tv.samples.SimpleCarousel
- * @sample androidx.tv.samples.CarouselIndicatorWithRectangleShape
- *
- * @param slideCount total number of slides present in the carousel.
- * @param carouselState state associated with this carousel.
- * @param timeToDisplaySlideMillis duration for which slide should be visible before moving to
- * the next slide.
- * @param enterTransition transition used to bring a slide into view.
- * @param exitTransition transition used to remove a slide from view.
- * @param carouselIndicator indicator showing the position of the current slide among all slides.
- * @param content defines the slides for a given index.
- */
-
-@Suppress("IllegalExperimentalApiUsage")
-@OptIn(ExperimentalComposeUiApi::class, ExperimentalAnimationApi::class)
-@ExperimentalTvMaterialApi
-@Composable
-fun Carousel(
-    slideCount: Int,
-    modifier: Modifier = Modifier,
-    carouselState: CarouselState = remember { CarouselState() },
-    timeToDisplaySlideMillis: Long = CarouselDefaults.TimeToDisplaySlideMillis,
-    enterTransition: EnterTransition = CarouselDefaults.EnterTransition,
-    exitTransition: ExitTransition = CarouselDefaults.ExitTransition,
-    carouselIndicator:
-    @Composable BoxScope.() -> Unit = {
-        CarouselDefaults.IndicatorRow(
-            slideCount = slideCount,
-            activeSlideIndex = carouselState.activeSlideIndex,
-            modifier = Modifier
-                .align(Alignment.BottomEnd)
-                .padding(16.dp),
-        )
-    },
-    content: @Composable (index: Int) -> Unit
-) {
-    CarouselStateUpdater(carouselState, slideCount)
-    var focusState: FocusState? by remember { mutableStateOf(null) }
-    val focusManager = LocalFocusManager.current
-    val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
-    val carouselOuterBoxFocusRequester = remember { FocusRequester() }
-    var isAutoScrollActive by remember { mutableStateOf(false) }
-
-    AutoScrollSideEffect(
-        timeToDisplaySlideMillis,
-        slideCount,
-        carouselState,
-        focusState,
-        onAutoScrollChange = { isAutoScrollActive = it })
-
-    Box(modifier = modifier
-        .bringIntoViewIfChildrenAreFocused()
-        .focusRequester(carouselOuterBoxFocusRequester)
-        .onFocusChanged {
-            focusState = it
-            if (it.isFocused && isAutoScrollActive) {
-                focusManager.moveFocus(FocusDirection.Enter)
-            }
-        }
-        .manualScrolling(carouselState, slideCount, isLtr)
-        .focusable()) {
-        AnimatedContent(
-            targetState = carouselState.activeSlideIndex,
-            transitionSpec = { enterTransition.with(exitTransition) }
-        ) {
-            LaunchedEffect(Unit) {
-                this@AnimatedContent.onAnimationCompletion {
-                    if (isAutoScrollActive.not()) {
-                        carouselOuterBoxFocusRequester.requestFocus()
-                        focusManager.moveFocus(FocusDirection.Enter)
-                    }
-                }
-            }
-            content.invoke(it)
-        }
-        this.carouselIndicator()
-    }
-}
-
-@Suppress("IllegalExperimentalApiUsage")
-@OptIn(ExperimentalAnimationApi::class)
-private suspend fun AnimatedVisibilityScope.onAnimationCompletion(action: suspend () -> Unit) {
-    snapshotFlow { transition.currentState == transition.targetState }.first { it }
-    action.invoke()
-}
-
-@OptIn(ExperimentalTvMaterialApi::class)
-@Composable
-private fun AutoScrollSideEffect(
-    timeToDisplaySlideMillis: Long,
-    slideCount: Int,
-    carouselState: CarouselState,
-    focusState: FocusState?,
-    onAutoScrollChange: (isAutoScrollActive: Boolean) -> Unit = {},
-) {
-    val currentTimeToDisplaySlideMillis by rememberUpdatedState(timeToDisplaySlideMillis)
-    val currentSlideCount by rememberUpdatedState(slideCount)
-    val carouselIsFocused = focusState?.isFocused ?: false
-    val carouselHasFocus = focusState?.hasFocus ?: false
-    val doAutoScroll = (carouselIsFocused || carouselHasFocus).not()
-
-    if (doAutoScroll) {
-        LaunchedEffect(carouselState) {
-            while (true) {
-                yield()
-                delay(currentTimeToDisplaySlideMillis)
-                if (carouselState.activePauseHandlesCount > 0) {
-                    snapshotFlow { carouselState.activePauseHandlesCount }
-                        .first { pauseHandleCount -> pauseHandleCount == 0 }
-                }
-                carouselState.moveToNextSlide(currentSlideCount)
-            }
-        }
-    }
-    onAutoScrollChange(doAutoScroll)
-}
-
-@Suppress("IllegalExperimentalApiUsage")
-@OptIn(ExperimentalTvMaterialApi::class, ExperimentalComposeUiApi::class)
-private fun Modifier.manualScrolling(
-    carouselState: CarouselState,
-    slideCount: Int,
-    isLtr: Boolean
-): Modifier =
-    this.focusProperties {
-        exit = {
-            val showPreviousSlideAndGetFocusRequester = {
-                if (carouselState.isFirstSlide().not()) {
-                    carouselState.moveToPreviousSlide(slideCount)
-                    FocusRequester.Cancel
-                } else {
-                    FocusRequester.Default
-                }
-            }
-            val showNextSlideAndGetFocusRequester = {
-                if (carouselState.isLastSlide(slideCount).not()) {
-                    carouselState.moveToNextSlide(slideCount)
-                    FocusRequester.Cancel
-                } else {
-                    FocusRequester.Default
-                }
-            }
-            when (it) {
-                FocusDirection.Left -> {
-                    if (isLtr) {
-                        showPreviousSlideAndGetFocusRequester()
-                    } else {
-                        showNextSlideAndGetFocusRequester()
-                    }
-                }
-
-                FocusDirection.Right -> {
-                    if (isLtr) {
-                        showNextSlideAndGetFocusRequester()
-                    } else {
-                        showPreviousSlideAndGetFocusRequester()
-                    }
-                }
-
-                else -> FocusRequester.Default
-            }
-        }
-    }
-
-@OptIn(ExperimentalTvMaterialApi::class)
-@Composable
-private fun CarouselStateUpdater(carouselState: CarouselState, slideCount: Int) {
-    LaunchedEffect(carouselState, slideCount) {
-        if (slideCount != 0) {
-            carouselState.activeSlideIndex = floorMod(carouselState.activeSlideIndex, slideCount)
-        }
-    }
-}
-
-/**
- * State of the Carousel which allows the user to specify the first slide that is shown when the
- * Carousel is instantiated in the constructor.
- *
- * It also provides the user with support to pause and resume the auto-scroll behaviour of the
- * Carousel.
- * @param initialActiveSlideIndex the index of the first active slide
- */
-@Stable
-@ExperimentalTvMaterialApi
-class CarouselState(initialActiveSlideIndex: Int = 0) {
-    internal var activePauseHandlesCount by mutableStateOf(0)
-
-    /**
-     * The index of the slide that is currently displayed by the carousel
-     */
-    var activeSlideIndex by mutableStateOf(initialActiveSlideIndex)
-        internal set
-
-    /**
-     * Pauses the auto-scrolling behaviour of Carousel.
-     * The pause request is ignored if [slideIndex] is not the current slide that is visible.
-     * Returns a [ScrollPauseHandle] that can be used to resume
-     */
-    fun pauseAutoScroll(slideIndex: Int): ScrollPauseHandle {
-        if (this.activeSlideIndex != slideIndex) {
-            return NoOpScrollPauseHandle
-        }
-        return ScrollPauseHandleImpl(this)
-    }
-
-    internal fun isFirstSlide() = activeSlideIndex == 0
-
-    internal fun isLastSlide(slideCount: Int) = activeSlideIndex == slideCount - 1
-
-    internal fun moveToPreviousSlide(slideCount: Int) {
-        // No slides available for carousel
-        if (slideCount == 0) return
-
-        // Go to previous slide
-        activeSlideIndex = floorMod(activeSlideIndex - 1, slideCount)
-    }
-
-    internal fun moveToNextSlide(slideCount: Int) {
-        // No slides available for carousel
-        if (slideCount == 0) return
-
-        // Go to next slide
-        activeSlideIndex = floorMod(activeSlideIndex + 1, slideCount)
-    }
-}
-
-@ExperimentalTvMaterialApi
-/**
- * Handle returned by [CarouselState.pauseAutoScroll] that can be used to resume auto-scroll.
- */
-sealed interface ScrollPauseHandle {
-    /**
-     * Resumes the auto-scroll behaviour if there are no other active [ScrollPauseHandle]s.
-     */
-    fun resumeAutoScroll()
-}
-
-@OptIn(ExperimentalTvMaterialApi::class)
-internal object NoOpScrollPauseHandle : ScrollPauseHandle {
-    /**
-     * Resumes the auto-scroll behaviour if there are no other active [ScrollPauseHandle]s.
-     */
-    override fun resumeAutoScroll() {}
-}
-
-@OptIn(ExperimentalTvMaterialApi::class)
-internal class ScrollPauseHandleImpl(private val carouselState: CarouselState) : ScrollPauseHandle {
-    private var active by mutableStateOf(true)
-    init {
-        carouselState.activePauseHandlesCount += 1
-    }
-    /**
-     * Resumes the auto-scroll behaviour if there are no other active [ScrollPauseHandle]s.
-     */
-    override fun resumeAutoScroll() {
-        if (active) {
-            active = false
-            carouselState.activePauseHandlesCount -= 1
-        }
-    }
-}
-
-@ExperimentalTvMaterialApi
-object CarouselDefaults {
-    /**
-     * Default time for which the slide is visible to the user.
-     */
-    const val TimeToDisplaySlideMillis: Long = 5000
-
-    /**
-     * Default transition used to bring the slide into view
-     */
-    val EnterTransition: EnterTransition = fadeIn(animationSpec = tween(100))
-
-    /**
-     * Default transition used to remove the slide from view
-     */
-    val ExitTransition: ExitTransition = fadeOut(animationSpec = tween(100))
-
-    /**
-     * An indicator showing the position of the current active slide among the slides of the
-     * carousel.
-     *
-     * @param slideCount total number of slides in the carousel
-     * @param activeSlideIndex the current active slide index
-     * @param modifier Modifier applied to the indicators' container
-     * @param spacing spacing between the indicator dots
-     * @param indicator indicator dot representing each slide in the carousel
-     */
-    @ExperimentalTvMaterialApi
-    @Composable
-    fun IndicatorRow(
-        slideCount: Int,
-        activeSlideIndex: Int,
-        modifier: Modifier = Modifier,
-        spacing: Dp = 8.dp,
-        indicator: @Composable (isActive: Boolean) -> Unit = { isActive ->
-            val activeColor = Color.White
-            val inactiveColor = activeColor.copy(alpha = 0.5f)
-            Box(
-                modifier = Modifier
-                    .size(8.dp)
-                    .background(
-                        color = if (isActive) activeColor else inactiveColor,
-                        shape = CircleShape,
-                    ),
-            )
-        }
-    ) {
-        Row(
-            horizontalArrangement = Arrangement.spacedBy(spacing),
-            verticalAlignment = Alignment.CenterVertically,
-            modifier = modifier,
-        ) {
-            repeat(slideCount) {
-                val isActive = it == activeSlideIndex
-                indicator(isActive = isActive)
-            }
-        }
-    }
-}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt b/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
deleted file mode 100644
index c39c68b..0000000
--- a/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.material.carousel
-
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.core.MutableTransitionState
-import androidx.compose.animation.slideInHorizontally
-import androidx.compose.animation.slideOutHorizontally
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusDirection
-import androidx.compose.ui.focus.FocusState
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.tv.material.ExperimentalTvMaterialApi
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.first
-
-/**
- * This composable is intended for use in Carousel.
- * A composable that has
- * - a [background] layer that is rendered as soon as the composable is visible.
- * - an [overlay] layer that is rendered after a delay of
- *   [overlayEnterTransitionStartDelayMillis].
- *
- * @param overlayEnterTransitionStartDelayMillis time between the rendering of the
- * background and the overlay.
- * @param overlayEnterTransition animation used to bring the overlay into view.
- * @param overlayExitTransition animation used to remove the overlay from view.
- * @param background composable defining the background of the slide.
- * @param overlay composable defining the content overlaid on the background.
- */
-@Suppress("IllegalExperimentalApiUsage")
-@OptIn(ExperimentalComposeUiApi::class)
-@ExperimentalTvMaterialApi
-@Composable
-fun CarouselItem(
-    background: @Composable () -> Unit,
-    modifier: Modifier = Modifier,
-    overlayEnterTransitionStartDelayMillis: Long =
-        CarouselItemDefaults.OverlayEnterTransitionStartDelayMillis,
-    overlayEnterTransition: EnterTransition = CarouselItemDefaults.OverlayEnterTransition,
-    overlayExitTransition: ExitTransition = CarouselItemDefaults.OverlayExitTransition,
-    overlay: @Composable () -> Unit
-) {
-    val overlayVisible = remember { MutableTransitionState(initialState = false) }
-    var focusState: FocusState? by remember { mutableStateOf(null) }
-    val focusManager = LocalFocusManager.current
-
-    LaunchedEffect(overlayVisible) {
-        overlayVisible.onAnimationCompletion {
-            // slide has loaded completely.
-            if (focusState?.isFocused == true) { focusManager.moveFocus(FocusDirection.Enter) }
-        }
-    }
-
-    Box(modifier = modifier
-        .onFocusChanged {
-            focusState = it
-            if (it.isFocused && overlayVisible.isIdle && overlayVisible.currentState) {
-                focusManager.moveFocus(FocusDirection.Enter)
-            }
-        }
-        .focusable()) {
-        background()
-
-        LaunchedEffect(overlayVisible) {
-            // After the delay, set overlay-visibility to true and trigger the animation to show the
-            // overlay.
-            delay(overlayEnterTransitionStartDelayMillis)
-            overlayVisible.targetState = true
-        }
-
-        AnimatedVisibility(
-            modifier = Modifier
-                .align(Alignment.BottomStart)
-                .onFocusChanged {
-                    if (it.isFocused) {
-                        focusManager.moveFocus(FocusDirection.Enter)
-                    }
-                }
-                .focusable(),
-            visibleState = overlayVisible,
-            enter = overlayEnterTransition,
-            exit = overlayExitTransition
-        ) {
-            overlay.invoke()
-        }
-    }
-}
-
-private suspend fun MutableTransitionState<Boolean>.onAnimationCompletion(
-    action: suspend () -> Unit
-) {
-    snapshotFlow { isIdle && currentState }.first { it }
-    action.invoke()
-}
-
-@ExperimentalTvMaterialApi
-object CarouselItemDefaults {
-    /**
-     * Default delay between the background being rendered and the overlay being rendered.
-     */
-    const val OverlayEnterTransitionStartDelayMillis: Long = 200
-
-    /**
-     * Default transition to bring the overlay into view.
-     */
-    val OverlayEnterTransition: EnterTransition = slideInHorizontally(initialOffsetX = { it * 4 })
-
-    /**
-     * Default transition to remove overlay from view.
-     */
-    val OverlayExitTransition: ExitTransition = slideOutHorizontally()
-}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt b/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt
deleted file mode 100644
index e750f38..0000000
--- a/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.material.immersivelist
-
-import androidx.compose.animation.AnimatedContentScope
-import androidx.compose.animation.AnimatedVisibilityScope
-import androidx.compose.animation.ContentTransform
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.with
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusDirection
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.tv.material.ExperimentalTvMaterialApi
-import androidx.tv.material.bringIntoViewIfChildrenAreFocused
-
-/**
- * Immersive List consists of a list with multiple items and a background that displays content
- * based on the item in focus.
- * To animate the background's entry and exit, use [ImmersiveListBackgroundScope.AnimatedContent].
- * To display the background only when the list is in focus, use
- * [ImmersiveListBackgroundScope.AnimatedVisibility].
- *
- * @param background Composable defining the background to be displayed for a given item's
- * index. `listHasFocus` argument can be used to hide the background when the list is not in focus
- * @param modifier applied to Immersive List.
- * @param listAlignment Alignment of the List with respect to the Immersive List.
- * @param list composable defining the list of items that has to be rendered.
- */
-@Suppress("IllegalExperimentalApiUsage")
-@OptIn(ExperimentalComposeUiApi::class)
-@ExperimentalTvMaterialApi
-@Composable
-fun ImmersiveList(
-    background:
-    @Composable ImmersiveListBackgroundScope.(index: Int, listHasFocus: Boolean) -> Unit,
-    modifier: Modifier = Modifier,
-    listAlignment: Alignment = Alignment.BottomEnd,
-    list: @Composable ImmersiveListScope.() -> Unit,
-) {
-    var currentItemIndex by remember { mutableStateOf(0) }
-    var listHasFocus by remember { mutableStateOf(false) }
-
-    Box(modifier.bringIntoViewIfChildrenAreFocused()) {
-        ImmersiveListBackgroundScope(this).background(currentItemIndex, listHasFocus)
-
-        val focusManager = LocalFocusManager.current
-
-        Box(Modifier.align(listAlignment).onFocusChanged { listHasFocus = it.hasFocus }) {
-            ImmersiveListScope {
-                currentItemIndex = it
-                focusManager.moveFocus(FocusDirection.Enter)
-            }.list()
-        }
-    }
-}
-
-@ExperimentalTvMaterialApi
-object ImmersiveListDefaults {
-    /**
-     * Default transition used to bring the background content into view
-     */
-    val EnterTransition: EnterTransition = fadeIn(animationSpec = tween(300))
-
-    /**
-     * Default transition used to remove the background content from view
-     */
-    val ExitTransition: ExitTransition = fadeOut(animationSpec = tween(300))
-}
-
-@Immutable
-@ExperimentalTvMaterialApi
-public class ImmersiveListBackgroundScope internal constructor(boxScope: BoxScope) : BoxScope
-by boxScope {
-
-    /**
-     * [ImmersiveListBackgroundScope.AnimatedVisibility] composable animates the appearance and
-     * disappearance of its content, as [visible] value changes. Different [EnterTransition]s and
-     * [ExitTransition]s can be defined in [enter] and [exit] for the appearance and disappearance
-     * animation.
-     *
-     * @param visible defines whether the content should be visible
-     * @param modifier modifier for the Layout created to contain the [content]
-     * @param enter EnterTransition(s) used for the appearing animation, fading in by default
-     * @param exit ExitTransition(s) used for the disappearing animation, fading out by default
-     * @param content Content to appear or disappear based on the value of [visible]
-     *
-     * @link androidx.compose.animation.AnimatedVisibility
-     * @see androidx.compose.animation.AnimatedVisibility
-     * @see EnterTransition
-     * @see ExitTransition
-     * @see AnimatedVisibilityScope
-     */
-    @Composable
-    fun AnimatedVisibility(
-        visible: Boolean,
-        modifier: Modifier = Modifier,
-        enter: EnterTransition = ImmersiveListDefaults.EnterTransition,
-        exit: ExitTransition = ImmersiveListDefaults.ExitTransition,
-        label: String = "AnimatedVisibility",
-        content: @Composable AnimatedVisibilityScope.() -> Unit
-    ) {
-        androidx.compose.animation.AnimatedVisibility(
-            visible,
-            modifier,
-            enter,
-            exit,
-            label,
-            content
-        )
-    }
-
-    /**
-     * [ImmersiveListBackgroundScope.AnimatedContent] is a container that automatically animates its
-     * content when [targetState] changes. Its [content] for different target states is defined in a
-     * mapping between a target state and a composable function.
-     *
-     * @param targetState defines the key to choose the content to be displayed
-     * @param modifier modifier for the Layout created to contain the [content]
-     * @param transitionSpec defines the EnterTransition(s) and ExitTransition(s) used to display
-     * and remove the content, fading in and fading out by default
-     * @param content Content to appear or disappear based on the value of [targetState]
-     *
-     * @link androidx.compose.animation.AnimatedContent
-     * @see androidx.compose.animation.AnimatedContent
-     * @see ContentTransform
-     * @see AnimatedContentScope
-     */
-    @Suppress("IllegalExperimentalApiUsage")
-    @ExperimentalAnimationApi
-    @Composable
-    fun AnimatedContent(
-        targetState: Int,
-        modifier: Modifier = Modifier,
-        transitionSpec: AnimatedContentScope<Int>.() -> ContentTransform = {
-            ImmersiveListDefaults.EnterTransition.with(ImmersiveListDefaults.ExitTransition)
-        },
-        contentAlignment: Alignment = Alignment.TopStart,
-        content: @Composable AnimatedVisibilityScope.(targetState: Int) -> Unit
-    ) {
-        androidx.compose.animation.AnimatedContent(
-            targetState,
-            modifier,
-            transitionSpec,
-            contentAlignment,
-            content = content
-        )
-    }
-}
-
-@Immutable
-@ExperimentalTvMaterialApi
-public class ImmersiveListScope internal constructor(private val onFocused: (Int) -> Unit) {
-    /**
-     * Modifier to be added to each of the items of the list within ImmersiveList to inform the
-     * ImmersiveList of the index of the item in focus.
-     *
-     * @param index index of the item within the list.
-     */
-    fun Modifier.focusableItem(index: Int): Modifier {
-        return onFocusChanged { if (it.hasFocus || it.isFocused) { onFocused(index) } }
-            .focusable()
-    }
-}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/BringIntoViewIfChildrenAreFocused.kt b/tv/tv-material/src/main/java/androidx/tv/material3/BringIntoViewIfChildrenAreFocused.kt
new file mode 100644
index 0000000..b973bb7
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/BringIntoViewIfChildrenAreFocused.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.relocation.BringIntoViewResponder
+import androidx.compose.foundation.relocation.bringIntoViewResponder
+import androidx.compose.runtime.remember
+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.layout.onSizeChanged
+import androidx.compose.ui.platform.debugInspectorInfo
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal fun Modifier.bringIntoViewIfChildrenAreFocused(): Modifier = composed(
+    inspectorInfo = debugInspectorInfo { name = "bringIntoViewIfChildrenAreFocused" },
+    factory = {
+        var myRect: Rect = Rect.Zero
+        this
+            .onSizeChanged {
+                myRect = Rect(Offset.Zero, Offset(it.width.toFloat(), it.height.toFloat()))
+            }
+            .bringIntoViewResponder(
+                remember {
+                    object : BringIntoViewResponder {
+                        // return the current rectangle and ignoring the child rectangle received.
+                        @ExperimentalFoundationApi
+                        override fun calculateRectForParent(localRect: Rect): Rect = myRect
+
+                        // The container is not expected to be scrollable. Hence the child is
+                        // already in view with respect to the container.
+                        @ExperimentalFoundationApi
+                        override suspend fun bringChildIntoView(localRect: () -> Rect?) {}
+                    }
+                }
+            )
+    }
+)
\ No newline at end of file
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
new file mode 100644
index 0000000..2d04b43
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
@@ -0,0 +1,441 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import android.view.KeyEvent.KEYCODE_BACK
+import android.view.KeyEvent.KEYCODE_DPAD_LEFT
+import android.view.KeyEvent.KEYCODE_DPAD_RIGHT
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.with
+import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Row
+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.LaunchedEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.FocusState
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.key.KeyEventType.Companion.KeyUp
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.nativeKeyCode
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.input.key.type
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import java.lang.Math.floorMod
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.yield
+
+/**
+ * Composes a hero card rotator to highlight a piece of content.
+ *
+ * Examples:
+ * @sample androidx.tv.samples.SimpleCarousel
+ * @sample androidx.tv.samples.CarouselIndicatorWithRectangleShape
+ *
+ * @param modifier Modifier applied to the Carousel.
+ * @param slideCount total number of slides present in the carousel.
+ * @param carouselState state associated with this carousel.
+ * @param timeToDisplaySlideMillis duration for which slide should be visible before moving to
+ * the next slide.
+ * @param enterTransition transition used to bring a slide into view.
+ * @param exitTransition transition used to remove a slide from view.
+ * @param carouselIndicator indicator showing the position of the current slide among all slides.
+ * @param content defines the slides for a given index.
+ */
+@Suppress("IllegalExperimentalApiUsage")
+@OptIn(ExperimentalComposeUiApi::class, ExperimentalAnimationApi::class)
+@ExperimentalTvMaterial3Api
+@Composable
+fun Carousel(
+    slideCount: Int,
+    modifier: Modifier = Modifier,
+    carouselState: CarouselState = remember { CarouselState() },
+    timeToDisplaySlideMillis: Long = CarouselDefaults.TimeToDisplaySlideMillis,
+    enterTransition: EnterTransition = CarouselDefaults.EnterTransition,
+    exitTransition: ExitTransition = CarouselDefaults.ExitTransition,
+    carouselIndicator:
+    @Composable BoxScope.() -> Unit = {
+        CarouselDefaults.IndicatorRow(
+            slideCount = slideCount,
+            activeSlideIndex = carouselState.activeSlideIndex,
+            modifier = Modifier
+                .align(Alignment.BottomEnd)
+                .padding(16.dp),
+        )
+    },
+    content: @Composable (index: Int) -> Unit
+) {
+    CarouselStateUpdater(carouselState, slideCount)
+    var focusState: FocusState? by remember { mutableStateOf(null) }
+    val focusManager = LocalFocusManager.current
+    val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
+    val carouselOuterBoxFocusRequester = remember { FocusRequester() }
+    var isAutoScrollActive by remember { mutableStateOf(false) }
+
+    AutoScrollSideEffect(
+        timeToDisplaySlideMillis = timeToDisplaySlideMillis,
+        slideCount = slideCount,
+        carouselState = carouselState,
+        doAutoScroll = shouldPerformAutoScroll(focusState),
+        onAutoScrollChange = { isAutoScrollActive = it })
+
+    Box(modifier = modifier
+        .bringIntoViewIfChildrenAreFocused()
+        .focusRequester(carouselOuterBoxFocusRequester)
+        .onFocusChanged {
+            focusState = it
+
+            // When the carousel gains focus for the first time
+            if (it.isFocused && isAutoScrollActive) {
+                focusManager.moveFocus(FocusDirection.Enter)
+            }
+        }
+        .handleKeyEvents(
+            carouselState = carouselState,
+            outerBoxFocusRequester = carouselOuterBoxFocusRequester,
+            focusManager = focusManager,
+            slideCount = slideCount,
+            isLtr = isLtr,
+        )
+        .focusable()
+    ) {
+        AnimatedContent(
+            targetState = carouselState.activeSlideIndex,
+            transitionSpec = { enterTransition.with(exitTransition) }
+        ) { slideIndex ->
+            LaunchedEffect(Unit) {
+                this@AnimatedContent.onAnimationCompletion {
+                    // Outer box is focused
+                    if (!isAutoScrollActive && focusState?.isFocused == true) {
+                        carouselOuterBoxFocusRequester.requestFocus()
+                        focusManager.moveFocus(FocusDirection.Enter)
+                    }
+                }
+            }
+            // it is possible for the slideCount to have changed during the transition.
+            // This can cause the slideIndex to be greater than or equal to slideCount and cause
+            // IndexOutOfBoundsException. Guarding against this by checking against slideCount
+            // before invoking.
+            if (slideCount > 0) {
+                content.invoke(if (slideIndex < slideCount) slideIndex else 0)
+            }
+        }
+        this.carouselIndicator()
+    }
+}
+
+@Composable
+private fun shouldPerformAutoScroll(focusState: FocusState?): Boolean {
+    val carouselIsFocused = focusState?.isFocused ?: false
+    val carouselHasFocus = focusState?.hasFocus ?: false
+    return !(carouselIsFocused || carouselHasFocus)
+}
+
+@Suppress("IllegalExperimentalApiUsage")
+@OptIn(ExperimentalAnimationApi::class)
+private suspend fun AnimatedVisibilityScope.onAnimationCompletion(action: suspend () -> Unit) {
+    snapshotFlow { transition.currentState == transition.targetState }.first { it }
+    action.invoke()
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun AutoScrollSideEffect(
+    timeToDisplaySlideMillis: Long,
+    slideCount: Int,
+    carouselState: CarouselState,
+    doAutoScroll: Boolean,
+    onAutoScrollChange: (isAutoScrollActive: Boolean) -> Unit = {},
+) {
+    // Needed to ensure that the code within LaunchedEffect receives updates to the slideCount.
+    val updatedSlideCount by rememberUpdatedState(newValue = slideCount)
+    if (doAutoScroll) {
+        LaunchedEffect(carouselState) {
+            while (true) {
+                yield()
+                delay(timeToDisplaySlideMillis)
+                if (carouselState.activePauseHandlesCount > 0) {
+                    snapshotFlow { carouselState.activePauseHandlesCount }
+                        .first { pauseHandleCount -> pauseHandleCount == 0 }
+                }
+                carouselState.moveToNextSlide(updatedSlideCount)
+            }
+        }
+    }
+    onAutoScrollChange(doAutoScroll)
+}
+
+@Suppress("IllegalExperimentalApiUsage")
+@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalComposeUiApi::class)
+private fun Modifier.handleKeyEvents(
+    carouselState: CarouselState,
+    outerBoxFocusRequester: FocusRequester,
+    focusManager: FocusManager,
+    slideCount: Int,
+    isLtr: Boolean
+): Modifier = onKeyEvent {
+    // Ignore KeyUp action type
+    if (it.type == KeyUp) {
+        return@onKeyEvent KeyEventPropagation.ContinuePropagation
+    }
+
+    val showPreviousSlideAndGetKeyEventPropagation = {
+        if (carouselState.isFirstSlide()) {
+            KeyEventPropagation.ContinuePropagation
+        } else {
+            carouselState.moveToPreviousSlide(slideCount)
+            outerBoxFocusRequester.requestFocus()
+            KeyEventPropagation.StopPropagation
+        }
+    }
+    val showNextSlideAndGetKeyEventPropagation = {
+        if (carouselState.isLastSlide(slideCount)) {
+            KeyEventPropagation.ContinuePropagation
+        } else {
+            carouselState.moveToNextSlide(slideCount)
+            outerBoxFocusRequester.requestFocus()
+            KeyEventPropagation.StopPropagation
+        }
+    }
+
+    when (it.key.nativeKeyCode) {
+        KEYCODE_BACK -> {
+            focusManager.moveFocus(FocusDirection.Exit)
+            KeyEventPropagation.ContinuePropagation
+        }
+
+        KEYCODE_DPAD_LEFT -> {
+            // Ignore long press key event for manual scrolling
+            if (it.nativeKeyEvent.repeatCount > 0) {
+                return@onKeyEvent KeyEventPropagation.StopPropagation
+            }
+
+            if (isLtr) {
+                showPreviousSlideAndGetKeyEventPropagation()
+            } else {
+                showNextSlideAndGetKeyEventPropagation()
+            }
+        }
+
+        KEYCODE_DPAD_RIGHT -> {
+            // Ignore long press key event for manual scrolling
+            if (it.nativeKeyEvent.repeatCount > 0) {
+                return@onKeyEvent KeyEventPropagation.StopPropagation
+            }
+
+            if (isLtr) {
+                showNextSlideAndGetKeyEventPropagation()
+            } else {
+                showPreviousSlideAndGetKeyEventPropagation()
+            }
+        }
+
+        else -> KeyEventPropagation.ContinuePropagation
+    }
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun CarouselStateUpdater(carouselState: CarouselState, slideCount: Int) {
+    LaunchedEffect(carouselState, slideCount) {
+        if (slideCount != 0) {
+            carouselState.activeSlideIndex = floorMod(carouselState.activeSlideIndex, slideCount)
+        }
+    }
+}
+
+/**
+ * State of the Carousel which allows the user to specify the first slide that is shown when the
+ * Carousel is instantiated in the constructor.
+ *
+ * It also provides the user with support to pause and resume the auto-scroll behaviour of the
+ * Carousel.
+ * @param initialActiveSlideIndex the index of the first active slide
+ */
+@Stable
+@ExperimentalTvMaterial3Api
+class CarouselState(initialActiveSlideIndex: Int = 0) {
+    internal var activePauseHandlesCount by mutableStateOf(0)
+
+    /**
+     * The index of the slide that is currently displayed by the carousel
+     */
+    var activeSlideIndex by mutableStateOf(initialActiveSlideIndex)
+        internal set
+
+    /**
+     * Pauses the auto-scrolling behaviour of Carousel.
+     * The pause request is ignored if [slideIndex] is not the current slide that is visible.
+     * Returns a [ScrollPauseHandle] that can be used to resume
+     */
+    fun pauseAutoScroll(slideIndex: Int): ScrollPauseHandle {
+        if (this.activeSlideIndex != slideIndex) {
+            return NoOpScrollPauseHandle
+        }
+        return ScrollPauseHandleImpl(this)
+    }
+
+    internal fun isFirstSlide() = activeSlideIndex == 0
+
+    internal fun isLastSlide(slideCount: Int) = activeSlideIndex == slideCount - 1
+
+    internal fun moveToPreviousSlide(slideCount: Int) {
+        // No slides available for carousel
+        if (slideCount == 0) return
+
+        // Go to previous slide
+        activeSlideIndex = floorMod(activeSlideIndex - 1, slideCount)
+    }
+
+    internal fun moveToNextSlide(slideCount: Int) {
+        // No slides available for carousel
+        if (slideCount == 0) return
+
+        // Go to next slide
+        activeSlideIndex = floorMod(activeSlideIndex + 1, slideCount)
+    }
+}
+
+@ExperimentalTvMaterial3Api
+/**
+ * Handle returned by [CarouselState.pauseAutoScroll] that can be used to resume auto-scroll.
+ */
+sealed interface ScrollPauseHandle {
+    /**
+     * Resumes the auto-scroll behaviour if there are no other active [ScrollPauseHandle]s.
+     */
+    fun resumeAutoScroll()
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+internal object NoOpScrollPauseHandle : ScrollPauseHandle {
+    /**
+     * Resumes the auto-scroll behaviour if there are no other active [ScrollPauseHandle]s.
+     */
+    override fun resumeAutoScroll() {}
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+internal class ScrollPauseHandleImpl(private val carouselState: CarouselState) : ScrollPauseHandle {
+    private var active by mutableStateOf(true)
+
+    init {
+        carouselState.activePauseHandlesCount += 1
+    }
+
+    /**
+     * Resumes the auto-scroll behaviour if there are no other active [ScrollPauseHandle]s.
+     */
+    override fun resumeAutoScroll() {
+        if (active) {
+            active = false
+            carouselState.activePauseHandlesCount -= 1
+        }
+    }
+}
+
+@ExperimentalTvMaterial3Api
+object CarouselDefaults {
+    /**
+     * Default time for which the slide is visible to the user.
+     */
+    const val TimeToDisplaySlideMillis: Long = 5000
+
+    /**
+     * Default transition used to bring the slide into view
+     */
+    val EnterTransition: EnterTransition = fadeIn(animationSpec = tween(100))
+
+    /**
+     * Default transition used to remove the slide from view
+     */
+    val ExitTransition: ExitTransition = fadeOut(animationSpec = tween(100))
+
+    /**
+     * An indicator showing the position of the current active slide among the slides of the
+     * carousel.
+     *
+     * @param slideCount total number of slides in the carousel
+     * @param activeSlideIndex the current active slide index
+     * @param modifier Modifier applied to the indicators' container
+     * @param spacing spacing between the indicator dots
+     * @param indicator indicator dot representing each slide in the carousel
+     */
+    @ExperimentalTvMaterial3Api
+    @Composable
+    fun IndicatorRow(
+        slideCount: Int,
+        activeSlideIndex: Int,
+        modifier: Modifier = Modifier,
+        spacing: Dp = 8.dp,
+        indicator: @Composable (isActive: Boolean) -> Unit = { isActive ->
+            val activeColor = Color.White
+            val inactiveColor = activeColor.copy(alpha = 0.5f)
+            Box(
+                modifier = Modifier
+                    .size(8.dp)
+                    .background(
+                        color = if (isActive) activeColor else inactiveColor,
+                        shape = CircleShape,
+                    ),
+            )
+        }
+    ) {
+        Row(
+            horizontalArrangement = Arrangement.spacedBy(spacing),
+            verticalAlignment = Alignment.CenterVertically,
+            modifier = modifier,
+        ) {
+            repeat(slideCount) {
+                val isActive = it == activeSlideIndex
+                indicator(isActive = isActive)
+            }
+        }
+    }
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/CarouselItem.kt b/tv/tv-material/src/main/java/androidx/tv/material3/CarouselItem.kt
new file mode 100644
index 0000000..96114ca
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/CarouselItem.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import android.view.KeyEvent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.focus.FocusState
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.nativeKeyCode
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.input.key.type
+import androidx.compose.ui.platform.LocalFocusManager
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.first
+
+/**
+ * This composable is intended for use in Carousel.
+ * A composable that has
+ * - a [background] layer that is rendered as soon as the composable is visible.
+ * - an [overlay] layer that is rendered after a delay of
+ *   [overlayEnterTransitionStartDelayMillis].
+ *
+ * @param modifier modifier applied to the CarouselItem.
+ * @param overlayEnterTransitionStartDelayMillis time between the rendering of the
+ * background and the overlay.
+ * @param overlayEnterTransition animation used to bring the overlay into view.
+ * @param overlayExitTransition animation used to remove the overlay from view.
+ * @param background composable defining the background of the slide.
+ * @param overlay composable defining the content overlaid on the background.
+ */
+@Suppress("IllegalExperimentalApiUsage")
+@OptIn(ExperimentalComposeUiApi::class)
+@ExperimentalTvMaterial3Api
+@Composable
+fun CarouselItem(
+    background: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    overlayEnterTransitionStartDelayMillis: Long =
+        CarouselItemDefaults.OverlayEnterTransitionStartDelayMillis,
+    overlayEnterTransition: EnterTransition = CarouselItemDefaults.OverlayEnterTransition,
+    overlayExitTransition: ExitTransition = CarouselItemDefaults.OverlayExitTransition,
+    overlay: @Composable () -> Unit
+) {
+    val overlayVisible = remember { MutableTransitionState(initialState = false) }
+    var containerBoxFocusState: FocusState? by remember { mutableStateOf(null) }
+    val focusManager = LocalFocusManager.current
+    var exitFocus by remember { mutableStateOf(false) }
+
+    LaunchedEffect(overlayVisible) {
+        overlayVisible.onAnimationCompletion {
+            // slide has loaded completely.
+            if (containerBoxFocusState?.isFocused == true) {
+                focusManager.moveFocus(FocusDirection.Enter)
+            }
+        }
+    }
+
+    // This box holds the focus until the overlay animation completes
+    Box(modifier = modifier
+        .onKeyEvent {
+            exitFocus = it.key.nativeKeyCode == KeyEvent.KEYCODE_BACK && it.type == KeyDown
+            false
+        }
+        .onFocusChanged {
+            containerBoxFocusState = it
+            if (it.isFocused && exitFocus) {
+                focusManager.moveFocus(FocusDirection.Exit)
+                exitFocus = false
+            } else if (it.isFocused && overlayVisible.isIdle && overlayVisible.currentState) {
+                focusManager.moveFocus(FocusDirection.Enter)
+            }
+        }
+        .focusable()
+    ) {
+        background()
+
+        LaunchedEffect(overlayVisible) {
+            // After the delay, set overlay-visibility to true and trigger the animation to show the
+            // overlay.
+            delay(overlayEnterTransitionStartDelayMillis)
+            overlayVisible.targetState = true
+        }
+
+        AnimatedVisibility(
+            modifier = Modifier.align(Alignment.BottomStart),
+            visibleState = overlayVisible,
+            enter = overlayEnterTransition,
+            exit = overlayExitTransition
+        ) {
+            overlay.invoke()
+        }
+    }
+}
+
+private suspend fun MutableTransitionState<Boolean>.onAnimationCompletion(
+    action: suspend () -> Unit
+) {
+    snapshotFlow { isIdle && currentState }.first { it }
+    action.invoke()
+}
+
+@ExperimentalTvMaterial3Api
+object CarouselItemDefaults {
+    /**
+     * Default delay between the background being rendered and the overlay being rendered.
+     */
+    const val OverlayEnterTransitionStartDelayMillis: Long = 200
+
+    /**
+     * Default transition to bring the overlay into view.
+     */
+    val OverlayEnterTransition: EnterTransition = slideInHorizontally(initialOffsetX = { it * 4 })
+
+    /**
+     * Default transition to remove overlay from view.
+     */
+    val OverlayExitTransition: ExitTransition = slideOutHorizontally()
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/ColorScheme.kt b/tv/tv-material/src/main/java/androidx/tv/material3/ColorScheme.kt
new file mode 100644
index 0000000..257754a
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/ColorScheme.kt
@@ -0,0 +1,625 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.runtime.structuralEqualityPolicy
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.graphics.takeOrElse
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.tv.material3.tokens.ColorDarkTokens
+import androidx.tv.material3.tokens.ColorLightTokens
+import androidx.tv.material3.tokens.ColorSchemeKeyTokens
+import kotlin.math.ln
+
+/**
+ * Returns a light Material color scheme.
+ */
+@ExperimentalTvMaterial3Api
+fun lightColorScheme(
+    primary: Color = ColorLightTokens.Primary,
+    onPrimary: Color = ColorLightTokens.OnPrimary,
+    primaryContainer: Color = ColorLightTokens.PrimaryContainer,
+    onPrimaryContainer: Color = ColorLightTokens.OnPrimaryContainer,
+    inversePrimary: Color = ColorLightTokens.InversePrimary,
+    secondary: Color = ColorLightTokens.Secondary,
+    onSecondary: Color = ColorLightTokens.OnSecondary,
+    secondaryContainer: Color = ColorLightTokens.SecondaryContainer,
+    onSecondaryContainer: Color = ColorLightTokens.OnSecondaryContainer,
+    tertiary: Color = ColorLightTokens.Tertiary,
+    onTertiary: Color = ColorLightTokens.OnTertiary,
+    tertiaryContainer: Color = ColorLightTokens.TertiaryContainer,
+    onTertiaryContainer: Color = ColorLightTokens.OnTertiaryContainer,
+    background: Color = ColorLightTokens.Background,
+    onBackground: Color = ColorLightTokens.OnBackground,
+    surface: Color = ColorLightTokens.Surface,
+    onSurface: Color = ColorLightTokens.OnSurface,
+    surfaceVariant: Color = ColorLightTokens.SurfaceVariant,
+    onSurfaceVariant: Color = ColorLightTokens.OnSurfaceVariant,
+    surfaceTint: Color = primary,
+    inverseSurface: Color = ColorLightTokens.InverseSurface,
+    inverseOnSurface: Color = ColorLightTokens.InverseOnSurface,
+    error: Color = ColorLightTokens.Error,
+    onError: Color = ColorLightTokens.OnError,
+    errorContainer: Color = ColorLightTokens.ErrorContainer,
+    onErrorContainer: Color = ColorLightTokens.OnErrorContainer,
+    outline: Color = ColorLightTokens.Outline,
+    outlineVariant: Color = ColorLightTokens.OutlineVariant,
+    scrim: Color = ColorLightTokens.Scrim
+): ColorScheme =
+    ColorScheme(
+        primary = primary,
+        onPrimary = onPrimary,
+        primaryContainer = primaryContainer,
+        onPrimaryContainer = onPrimaryContainer,
+        inversePrimary = inversePrimary,
+        secondary = secondary,
+        onSecondary = onSecondary,
+        secondaryContainer = secondaryContainer,
+        onSecondaryContainer = onSecondaryContainer,
+        tertiary = tertiary,
+        onTertiary = onTertiary,
+        tertiaryContainer = tertiaryContainer,
+        onTertiaryContainer = onTertiaryContainer,
+        background = background,
+        onBackground = onBackground,
+        surface = surface,
+        onSurface = onSurface,
+        surfaceVariant = surfaceVariant,
+        onSurfaceVariant = onSurfaceVariant,
+        surfaceTint = surfaceTint,
+        inverseSurface = inverseSurface,
+        inverseOnSurface = inverseOnSurface,
+        error = error,
+        onError = onError,
+        errorContainer = errorContainer,
+        onErrorContainer = onErrorContainer,
+        outline = outline,
+        outlineVariant = outlineVariant,
+        scrim = scrim
+    )
+
+/**
+ * A color scheme holds all the named color parameters for a [MaterialTheme].
+ *
+ * Color schemes are designed to be harmonious, ensure accessible text, and distinguish UI
+ * elements and surfaces from one another. There are two built-in baseline schemes,
+ * [lightColorScheme] and a [darkColorScheme], that can be used as-is or customized.
+ *
+ * The Material color system and custom schemes provide default values for color as a starting point
+ * for customization.
+ *
+ * To learn more about colors, see [Material Design colors](https://m3.material.io/styles/color/overview).
+ *
+ * @property primary The primary color is the color displayed most frequently across your app’s
+ * screens and components.
+ * @property onPrimary Color used for text and icons displayed on top of the primary color.
+ * @property primaryContainer The preferred tonal color of containers.
+ * @property onPrimaryContainer The color (and state variants) that should be used for content on
+ * top of [primaryContainer].
+ * @property inversePrimary Color to be used as a "primary" color in places where the inverse color
+ * scheme is needed, such as the button on a SnackBar.
+ * @property secondary The secondary color provides more ways to accent and distinguish your
+ * product. Secondary colors are best for:
+ * - Floating action buttons
+ * - Selection controls, like checkboxes and radio buttons
+ * - Highlighting selected text
+ * - Links and headlines
+ * @property onSecondary Color used for text and icons displayed on top of the secondary color.
+ * @property secondaryContainer A tonal color to be used in containers.
+ * @property onSecondaryContainer The color (and state variants) that should be used for content on
+ * top of [secondaryContainer].
+ * @property tertiary The tertiary color that can be used to balance primary and secondary
+ * colors, or bring heightened attention to an element such as an input field.
+ * @property onTertiary Color used for text and icons displayed on top of the tertiary color.
+ * @property tertiaryContainer A tonal color to be used in containers.
+ * @property onTertiaryContainer The color (and state variants) that should be used for content on
+ * top of [tertiaryContainer].
+ * @property background The background color that appears behind scrollable content.
+ * @property onBackground Color used for text and icons displayed on top of the background color.
+ * @property surface The surface color that affect surfaces of components, such as cards, sheets,
+ * and menus.
+ * @property onSurface Color used for text and icons displayed on top of the surface color.
+ * @property surfaceVariant Another option for a color with similar uses of [surface].
+ * @property onSurfaceVariant The color (and state variants) that can be used for content on top of
+ * [surface].
+ * @property surfaceTint This color will be used by components that apply tonal elevation and is
+ * applied on top of [surface]. The higher the elevation the more this color is used.
+ * @property inverseSurface A color that contrasts sharply with [surface]. Useful for surfaces that
+ * sit on top of other surfaces with [surface] color.
+ * @property inverseOnSurface A color that contrasts well with [inverseSurface]. Useful for content
+ * that sits on top of containers that are [inverseSurface].
+ * @property error The error color is used to indicate errors in components, such as invalid text in
+ * a text field.
+ * @property onError Color used for text and icons displayed on top of the error color.
+ * @property errorContainer The preferred tonal color of error containers.
+ * @property onErrorContainer The color (and state variants) that should be used for content on
+ * top of [errorContainer].
+ * @property outline Subtle color used for boundaries. Outline color role adds contrast for
+ * accessibility purposes.
+ * @property outlineVariant Utility color used for boundaries for decorative elements when strong
+ * contrast is not required.
+ * @property scrim Color of a scrim that obscures content.
+ */
+@ExperimentalTvMaterial3Api
+@Stable
+class ColorScheme(
+    primary: Color,
+    onPrimary: Color,
+    primaryContainer: Color,
+    onPrimaryContainer: Color,
+    inversePrimary: Color,
+    secondary: Color,
+    onSecondary: Color,
+    secondaryContainer: Color,
+    onSecondaryContainer: Color,
+    tertiary: Color,
+    onTertiary: Color,
+    tertiaryContainer: Color,
+    onTertiaryContainer: Color,
+    background: Color,
+    onBackground: Color,
+    surface: Color,
+    onSurface: Color,
+    surfaceVariant: Color,
+    onSurfaceVariant: Color,
+    surfaceTint: Color,
+    inverseSurface: Color,
+    inverseOnSurface: Color,
+    error: Color,
+    onError: Color,
+    errorContainer: Color,
+    onErrorContainer: Color,
+    outline: Color,
+    outlineVariant: Color,
+    scrim: Color
+) {
+    var primary by mutableStateOf(primary, structuralEqualityPolicy())
+        internal set
+    var onPrimary by mutableStateOf(onPrimary, structuralEqualityPolicy())
+        internal set
+    var primaryContainer by mutableStateOf(primaryContainer, structuralEqualityPolicy())
+        internal set
+    var onPrimaryContainer by mutableStateOf(onPrimaryContainer, structuralEqualityPolicy())
+        internal set
+    var inversePrimary by mutableStateOf(inversePrimary, structuralEqualityPolicy())
+        internal set
+    var secondary by mutableStateOf(secondary, structuralEqualityPolicy())
+        internal set
+    var onSecondary by mutableStateOf(onSecondary, structuralEqualityPolicy())
+        internal set
+    var secondaryContainer by mutableStateOf(secondaryContainer, structuralEqualityPolicy())
+        internal set
+    var onSecondaryContainer by mutableStateOf(onSecondaryContainer, structuralEqualityPolicy())
+        internal set
+    var tertiary by mutableStateOf(tertiary, structuralEqualityPolicy())
+        internal set
+    var onTertiary by mutableStateOf(onTertiary, structuralEqualityPolicy())
+        internal set
+    var tertiaryContainer by mutableStateOf(tertiaryContainer, structuralEqualityPolicy())
+        internal set
+    var onTertiaryContainer by mutableStateOf(onTertiaryContainer, structuralEqualityPolicy())
+        internal set
+    var background by mutableStateOf(background, structuralEqualityPolicy())
+        internal set
+    var onBackground by mutableStateOf(onBackground, structuralEqualityPolicy())
+        internal set
+    var surface by mutableStateOf(surface, structuralEqualityPolicy())
+        internal set
+    var onSurface by mutableStateOf(onSurface, structuralEqualityPolicy())
+        internal set
+    var surfaceVariant by mutableStateOf(surfaceVariant, structuralEqualityPolicy())
+        internal set
+    var onSurfaceVariant by mutableStateOf(onSurfaceVariant, structuralEqualityPolicy())
+        internal set
+    var surfaceTint by mutableStateOf(surfaceTint, structuralEqualityPolicy())
+        internal set
+    var inverseSurface by mutableStateOf(inverseSurface, structuralEqualityPolicy())
+        internal set
+    var inverseOnSurface by mutableStateOf(inverseOnSurface, structuralEqualityPolicy())
+        internal set
+    var error by mutableStateOf(error, structuralEqualityPolicy())
+        internal set
+    var onError by mutableStateOf(onError, structuralEqualityPolicy())
+        internal set
+    var errorContainer by mutableStateOf(errorContainer, structuralEqualityPolicy())
+        internal set
+    var onErrorContainer by mutableStateOf(onErrorContainer, structuralEqualityPolicy())
+        internal set
+    var outline by mutableStateOf(outline, structuralEqualityPolicy())
+        internal set
+    var outlineVariant by mutableStateOf(outlineVariant, structuralEqualityPolicy())
+        internal set
+    var scrim by mutableStateOf(scrim, structuralEqualityPolicy())
+        internal set
+
+    /** Returns a copy of this ColorScheme, optionally overriding some of the values. */
+    fun copy(
+        primary: Color = this.primary,
+        onPrimary: Color = this.onPrimary,
+        primaryContainer: Color = this.primaryContainer,
+        onPrimaryContainer: Color = this.onPrimaryContainer,
+        inversePrimary: Color = this.inversePrimary,
+        secondary: Color = this.secondary,
+        onSecondary: Color = this.onSecondary,
+        secondaryContainer: Color = this.secondaryContainer,
+        onSecondaryContainer: Color = this.onSecondaryContainer,
+        tertiary: Color = this.tertiary,
+        onTertiary: Color = this.onTertiary,
+        tertiaryContainer: Color = this.tertiaryContainer,
+        onTertiaryContainer: Color = this.onTertiaryContainer,
+        background: Color = this.background,
+        onBackground: Color = this.onBackground,
+        surface: Color = this.surface,
+        onSurface: Color = this.onSurface,
+        surfaceVariant: Color = this.surfaceVariant,
+        onSurfaceVariant: Color = this.onSurfaceVariant,
+        surfaceTint: Color = this.surfaceTint,
+        inverseSurface: Color = this.inverseSurface,
+        inverseOnSurface: Color = this.inverseOnSurface,
+        error: Color = this.error,
+        onError: Color = this.onError,
+        errorContainer: Color = this.errorContainer,
+        onErrorContainer: Color = this.onErrorContainer,
+        outline: Color = this.outline,
+        outlineVariant: Color = this.outlineVariant,
+        scrim: Color = this.scrim
+    ): ColorScheme =
+        ColorScheme(
+            primary = primary,
+            onPrimary = onPrimary,
+            primaryContainer = primaryContainer,
+            onPrimaryContainer = onPrimaryContainer,
+            inversePrimary = inversePrimary,
+            secondary = secondary,
+            onSecondary = onSecondary,
+            secondaryContainer = secondaryContainer,
+            onSecondaryContainer = onSecondaryContainer,
+            tertiary = tertiary,
+            onTertiary = onTertiary,
+            tertiaryContainer = tertiaryContainer,
+            onTertiaryContainer = onTertiaryContainer,
+            background = background,
+            onBackground = onBackground,
+            surface = surface,
+            onSurface = onSurface,
+            surfaceVariant = surfaceVariant,
+            onSurfaceVariant = onSurfaceVariant,
+            surfaceTint = surfaceTint,
+            inverseSurface = inverseSurface,
+            inverseOnSurface = inverseOnSurface,
+            error = error,
+            onError = onError,
+            errorContainer = errorContainer,
+            onErrorContainer = onErrorContainer,
+            outline = outline,
+            outlineVariant = outlineVariant,
+            scrim = scrim
+        )
+
+    override fun toString(): String {
+        return "ColorScheme(" +
+            "primary=$primary" +
+            "onPrimary=$onPrimary" +
+            "primaryContainer=$primaryContainer" +
+            "onPrimaryContainer=$onPrimaryContainer" +
+            "inversePrimary=$inversePrimary" +
+            "secondary=$secondary" +
+            "onSecondary=$onSecondary" +
+            "secondaryContainer=$secondaryContainer" +
+            "onSecondaryContainer=$onSecondaryContainer" +
+            "tertiary=$tertiary" +
+            "onTertiary=$onTertiary" +
+            "tertiaryContainer=$tertiaryContainer" +
+            "onTertiaryContainer=$onTertiaryContainer" +
+            "background=$background" +
+            "onBackground=$onBackground" +
+            "surface=$surface" +
+            "onSurface=$onSurface" +
+            "surfaceVariant=$surfaceVariant" +
+            "onSurfaceVariant=$onSurfaceVariant" +
+            "surfaceTint=$surfaceTint" +
+            "inverseSurface=$inverseSurface" +
+            "inverseOnSurface=$inverseOnSurface" +
+            "error=$error" +
+            "onError=$onError" +
+            "errorContainer=$errorContainer" +
+            "onErrorContainer=$onErrorContainer" +
+            "outline=$outline" +
+            "outlineVariant=$outlineVariant" +
+            "scrim=$scrim" +
+            ")"
+    }
+}
+
+/**
+ * Returns a dark Material color scheme.
+ */
+@ExperimentalTvMaterial3Api
+fun darkColorScheme(
+    primary: Color = ColorDarkTokens.Primary,
+    onPrimary: Color = ColorDarkTokens.OnPrimary,
+    primaryContainer: Color = ColorDarkTokens.PrimaryContainer,
+    onPrimaryContainer: Color = ColorDarkTokens.OnPrimaryContainer,
+    inversePrimary: Color = ColorDarkTokens.InversePrimary,
+    secondary: Color = ColorDarkTokens.Secondary,
+    onSecondary: Color = ColorDarkTokens.OnSecondary,
+    secondaryContainer: Color = ColorDarkTokens.SecondaryContainer,
+    onSecondaryContainer: Color = ColorDarkTokens.OnSecondaryContainer,
+    tertiary: Color = ColorDarkTokens.Tertiary,
+    onTertiary: Color = ColorDarkTokens.OnTertiary,
+    tertiaryContainer: Color = ColorDarkTokens.TertiaryContainer,
+    onTertiaryContainer: Color = ColorDarkTokens.OnTertiaryContainer,
+    background: Color = ColorDarkTokens.Background,
+    onBackground: Color = ColorDarkTokens.OnBackground,
+    surface: Color = ColorDarkTokens.Surface,
+    onSurface: Color = ColorDarkTokens.OnSurface,
+    surfaceVariant: Color = ColorDarkTokens.SurfaceVariant,
+    onSurfaceVariant: Color = ColorDarkTokens.OnSurfaceVariant,
+    surfaceTint: Color = primary,
+    inverseSurface: Color = ColorDarkTokens.InverseSurface,
+    inverseOnSurface: Color = ColorDarkTokens.InverseOnSurface,
+    error: Color = ColorDarkTokens.Error,
+    onError: Color = ColorDarkTokens.OnError,
+    errorContainer: Color = ColorDarkTokens.ErrorContainer,
+    onErrorContainer: Color = ColorDarkTokens.OnErrorContainer,
+    outline: Color = ColorDarkTokens.Outline,
+    outlineVariant: Color = ColorDarkTokens.OutlineVariant,
+    scrim: Color = ColorDarkTokens.Scrim
+): ColorScheme =
+    ColorScheme(
+        primary = primary,
+        onPrimary = onPrimary,
+        primaryContainer = primaryContainer,
+        onPrimaryContainer = onPrimaryContainer,
+        inversePrimary = inversePrimary,
+        secondary = secondary,
+        onSecondary = onSecondary,
+        secondaryContainer = secondaryContainer,
+        onSecondaryContainer = onSecondaryContainer,
+        tertiary = tertiary,
+        onTertiary = onTertiary,
+        tertiaryContainer = tertiaryContainer,
+        onTertiaryContainer = onTertiaryContainer,
+        background = background,
+        onBackground = onBackground,
+        surface = surface,
+        onSurface = onSurface,
+        surfaceVariant = surfaceVariant,
+        onSurfaceVariant = onSurfaceVariant,
+        surfaceTint = surfaceTint,
+        inverseSurface = inverseSurface,
+        inverseOnSurface = inverseOnSurface,
+        error = error,
+        onError = onError,
+        errorContainer = errorContainer,
+        onErrorContainer = onErrorContainer,
+        outline = outline,
+        outlineVariant = outlineVariant,
+        scrim = scrim
+    )
+
+/**
+ * The Material color system contains pairs of colors that are typically used for the background and
+ * content color inside a component. For example, a Button typically uses `primary` for its
+ * background, and `onPrimary` for the color of its content (usually text or iconography).
+ *
+ * This function tries to match the provided [backgroundColor] to a 'background' color in this
+ * [ColorScheme], and then will return the corresponding color used for content. For example, when
+ * [backgroundColor] is [ColorScheme.primary], this will return [ColorScheme.onPrimary].
+ *
+ * If [backgroundColor] does not match a background color in the theme, this will return
+ * [Color.Unspecified].
+ *
+ * @return the matching content color for [backgroundColor]. If [backgroundColor] is not present in
+ * the theme's [ColorScheme], then returns [Color.Unspecified].
+ *
+ * @see contentColorFor
+ */
+@ExperimentalTvMaterial3Api
+fun ColorScheme.contentColorFor(backgroundColor: Color): Color =
+    when (backgroundColor) {
+        primary -> onPrimary
+        secondary -> onSecondary
+        tertiary -> onTertiary
+        background -> onBackground
+        error -> onError
+        surface -> onSurface
+        surfaceVariant -> onSurfaceVariant
+        primaryContainer -> onPrimaryContainer
+        secondaryContainer -> onSecondaryContainer
+        tertiaryContainer -> onTertiaryContainer
+        errorContainer -> onErrorContainer
+        inverseSurface -> inverseOnSurface
+        else -> Color.Unspecified
+    }
+
+/**
+ * The Material color system contains pairs of colors that are typically used for the background and
+ * content color inside a component. For example, a Button typically uses `primary` for its
+ * background, and `onPrimary` for the color of its content (usually text or iconography).
+ *
+ * This function tries to match the provided [backgroundColor] to a 'background' color in this
+ * [ColorScheme], and then will return the corresponding color used for content. For example, when
+ * [backgroundColor] is [ColorScheme.primary], this will return [ColorScheme.onPrimary].
+ *
+ * If [backgroundColor] does not match a background color in the theme, this will return the current
+ * value of [LocalContentColor] as a best-effort color.
+ *
+ * @return the matching content color for [backgroundColor]. If [backgroundColor] is not present in
+ * the theme's [ColorScheme], then returns the current value of [LocalContentColor].
+ *
+ * @see ColorScheme.contentColorFor
+ */
+@Composable
+@ReadOnlyComposable
+@ExperimentalTvMaterial3Api
+fun contentColorFor(backgroundColor: Color) =
+    MaterialTheme.colorScheme.contentColorFor(backgroundColor).takeOrElse {
+        LocalContentColor.current
+    }
+
+/**
+ * Returns the new background [Color] to use, representing the original background [color] with an
+ * overlay corresponding to [elevation] applied. The overlay will only be applied to
+ * [ColorScheme.surface].
+ */
+@OptIn(ExperimentalTvMaterial3Api::class)
+internal fun ColorScheme.applyTonalElevation(backgroundColor: Color, elevation: Dp): Color {
+    return if (backgroundColor == surface) {
+        surfaceColorAtElevation(elevation)
+    } else {
+        backgroundColor
+    }
+}
+
+/**
+ * Computes the surface tonal color at different elevation levels e.g. surface1 through surface5.
+ *
+ * @param elevation Elevation value used to compute alpha of the color overlay layer.
+ *
+ * @return the [ColorScheme.surface] color with an alpha of the [ColorScheme.surfaceTint] color
+ * overlaid on top of it.
+
+ */
+@ExperimentalTvMaterial3Api
+fun ColorScheme.surfaceColorAtElevation(
+    elevation: Dp
+): Color {
+    if (elevation == 0.dp) return surface
+    val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f
+    return surfaceTint.copy(alpha = alpha).compositeOver(surface)
+}
+
+/**
+ * Updates the internal values of a given [ColorScheme] with values from the [other]
+ * [ColorScheme].
+ * This allows efficiently updating a subset of [ColorScheme], without recomposing every
+ * composable that consumes values from [LocalColorScheme].
+ *
+ * Because [ColorScheme] is very wide-reaching, and used by many expensive composables in the
+ * hierarchy, providing a new value to [LocalColorScheme] causes every composable consuming
+ * [LocalColorScheme] to recompose, which is prohibitively expensive in cases such as animating one
+ * color in the theme. Instead, [ColorScheme] is internally backed by [mutableStateOf], and this
+ * function mutates the internal state of [this] to match values in [other]. This means that any
+ * changes will mutate the internal state of [this], and only cause composables that are reading the
+ * specific changed value to recompose.
+ */
+@OptIn(ExperimentalTvMaterial3Api::class)
+internal fun ColorScheme.updateColorSchemeFrom(other: ColorScheme) {
+    primary = other.primary
+    onPrimary = other.onPrimary
+    primaryContainer = other.primaryContainer
+    onPrimaryContainer = other.onPrimaryContainer
+    inversePrimary = other.inversePrimary
+    secondary = other.secondary
+    onSecondary = other.onSecondary
+    secondaryContainer = other.secondaryContainer
+    onSecondaryContainer = other.onSecondaryContainer
+    tertiary = other.tertiary
+    onTertiary = other.onTertiary
+    tertiaryContainer = other.tertiaryContainer
+    onTertiaryContainer = other.onTertiaryContainer
+    background = other.background
+    onBackground = other.onBackground
+    surface = other.surface
+    onSurface = other.onSurface
+    surfaceVariant = other.surfaceVariant
+    onSurfaceVariant = other.onSurfaceVariant
+    surfaceTint = other.surfaceTint
+    inverseSurface = other.inverseSurface
+    inverseOnSurface = other.inverseOnSurface
+    error = other.error
+    onError = other.onError
+    errorContainer = other.errorContainer
+    onErrorContainer = other.onErrorContainer
+    outline = other.outline
+    outlineVariant = other.outlineVariant
+    scrim = other.scrim
+}
+
+/**
+ * Helper function for component color tokens. Here is an example on how to use component color
+ * tokens:
+ * ``MaterialTheme.colorScheme.fromToken(ExtendedFabBranded.BrandedContainerColor)``
+ */
+@OptIn(ExperimentalTvMaterial3Api::class)
+internal fun ColorScheme.fromToken(value: ColorSchemeKeyTokens): Color {
+    return when (value) {
+        ColorSchemeKeyTokens.Background -> background
+        ColorSchemeKeyTokens.Error -> error
+        ColorSchemeKeyTokens.ErrorContainer -> errorContainer
+        ColorSchemeKeyTokens.InverseOnSurface -> inverseOnSurface
+        ColorSchemeKeyTokens.InversePrimary -> inversePrimary
+        ColorSchemeKeyTokens.InverseSurface -> inverseSurface
+        ColorSchemeKeyTokens.OnBackground -> onBackground
+        ColorSchemeKeyTokens.OnError -> onError
+        ColorSchemeKeyTokens.OnErrorContainer -> onErrorContainer
+        ColorSchemeKeyTokens.OnPrimary -> onPrimary
+        ColorSchemeKeyTokens.OnPrimaryContainer -> onPrimaryContainer
+        ColorSchemeKeyTokens.OnSecondary -> onSecondary
+        ColorSchemeKeyTokens.OnSecondaryContainer -> onSecondaryContainer
+        ColorSchemeKeyTokens.OnSurface -> onSurface
+        ColorSchemeKeyTokens.OnSurfaceVariant -> onSurfaceVariant
+        ColorSchemeKeyTokens.SurfaceTint -> surfaceTint
+        ColorSchemeKeyTokens.OnTertiary -> onTertiary
+        ColorSchemeKeyTokens.OnTertiaryContainer -> onTertiaryContainer
+        ColorSchemeKeyTokens.Outline -> outline
+        ColorSchemeKeyTokens.OutlineVariant -> outlineVariant
+        ColorSchemeKeyTokens.Primary -> primary
+        ColorSchemeKeyTokens.PrimaryContainer -> primaryContainer
+        ColorSchemeKeyTokens.Scrim -> scrim
+        ColorSchemeKeyTokens.Secondary -> secondary
+        ColorSchemeKeyTokens.SecondaryContainer -> secondaryContainer
+        ColorSchemeKeyTokens.Surface -> surface
+        ColorSchemeKeyTokens.SurfaceVariant -> surfaceVariant
+        ColorSchemeKeyTokens.Tertiary -> tertiary
+        ColorSchemeKeyTokens.TertiaryContainer -> tertiaryContainer
+    }
+}
+
+/**
+ * CompositionLocal used to pass [ColorScheme] down the tree.
+ *
+ * Setting the value here is typically done as part of [MaterialTheme], which will automatically
+ * handle efficiently updating any changed colors without causing unnecessary recompositions, using
+ * [ColorScheme.updateColorSchemeFrom]. To retrieve the current value of this CompositionLocal, use
+ * [MaterialTheme.colorScheme].
+ */
+@OptIn(ExperimentalTvMaterial3Api::class)
+internal val LocalColorScheme = staticCompositionLocalOf { lightColorScheme() }
+
+/**
+ * A low level of alpha used to represent disabled components, such as text in a disabled Button.
+ */
+internal const val DisabledAlpha = 0.38f
+
+/** Converts a color token key to the local color scheme provided by the theme */
+@ReadOnlyComposable
+@Composable
+@OptIn(ExperimentalTvMaterial3Api::class)
+internal fun ColorSchemeKeyTokens.toColor(): Color {
+    return MaterialTheme.colorScheme.fromToken(this)
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/ContentColor.kt b/tv/tv-material/src/main/java/androidx/tv/material3/ContentColor.kt
new file mode 100644
index 0000000..261dc24
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/ContentColor.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.ui.graphics.Color
+
+/**
+ * CompositionLocal containing the preferred content color for a given position in the hierarchy.
+ * This typically represents the `on` color for a color in `ColorScheme`. For example, if the
+ * background color is `surface`, this color is typically set to
+ * `onSurface`.
+ *
+ * This color should be used for any typography / iconography, to ensure that the color of these
+ * adjusts when the background color changes. For example, on a dark background, text should be
+ * light, and on a light background, text should be dark.
+ *
+ * Defaults to [Color.Black] if no color has been explicitly set.
+ */
+val LocalContentColor = compositionLocalOf { Color.Black }
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/ExperimentalTvMaterial3Api.kt b/tv/tv-material/src/main/java/androidx/tv/material3/ExperimentalTvMaterial3Api.kt
new file mode 100644
index 0000000..006ab10
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/ExperimentalTvMaterial3Api.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+@RequiresOptIn(
+    "This tv-material API is experimental and likely to change or be removed in the future."
+)
+@Retention(AnnotationRetention.BINARY)
+annotation class ExperimentalTvMaterial3Api
\ No newline at end of file
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/ImmersiveList.kt b/tv/tv-material/src/main/java/androidx/tv/material3/ImmersiveList.kt
new file mode 100644
index 0000000..7a5631b
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/ImmersiveList.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.animation.AnimatedContentScope
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.with
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.platform.LocalFocusManager
+
+/**
+ * Immersive List consists of a list with multiple items and a background that displays content
+ * based on the item in focus.
+ * To animate the background's entry and exit, use [ImmersiveListBackgroundScope.AnimatedContent].
+ * To display the background only when the list is in focus, use
+ * [ImmersiveListBackgroundScope.AnimatedVisibility].
+ *
+ * @sample androidx.tv.samples.SampleImmersiveList
+ *
+ * @param background Composable defining the background to be displayed for a given item's
+ * index. `listHasFocus` argument can be used to hide the background when the list is not in focus
+ * @param modifier applied to Immersive List.
+ * @param listAlignment Alignment of the List with respect to the Immersive List.
+ * @param list composable defining the list of items that has to be rendered.
+ */
+@Suppress("IllegalExperimentalApiUsage")
+@OptIn(ExperimentalComposeUiApi::class)
+@ExperimentalTvMaterial3Api
+@Composable
+fun ImmersiveList(
+    background:
+    @Composable ImmersiveListBackgroundScope.(index: Int, listHasFocus: Boolean) -> Unit,
+    modifier: Modifier = Modifier,
+    listAlignment: Alignment = Alignment.BottomEnd,
+    list: @Composable ImmersiveListScope.() -> Unit,
+) {
+    var currentItemIndex by remember { mutableStateOf(0) }
+    var listHasFocus by remember { mutableStateOf(false) }
+
+    Box(modifier.bringIntoViewIfChildrenAreFocused()) {
+        ImmersiveListBackgroundScope(this).background(currentItemIndex, listHasFocus)
+
+        val focusManager = LocalFocusManager.current
+
+        Box(Modifier.align(listAlignment).onFocusChanged { listHasFocus = it.hasFocus }) {
+            ImmersiveListScope {
+                currentItemIndex = it
+                focusManager.moveFocus(FocusDirection.Enter)
+            }.list()
+        }
+    }
+}
+
+@ExperimentalTvMaterial3Api
+object ImmersiveListDefaults {
+    /**
+     * Default transition used to bring the background content into view
+     */
+    val EnterTransition: EnterTransition = fadeIn(animationSpec = tween(300))
+
+    /**
+     * Default transition used to remove the background content from view
+     */
+    val ExitTransition: ExitTransition = fadeOut(animationSpec = tween(300))
+}
+
+@Immutable
+@ExperimentalTvMaterial3Api
+public class ImmersiveListBackgroundScope internal constructor(boxScope: BoxScope) : BoxScope
+by boxScope {
+
+    /**
+     * [ImmersiveListBackgroundScope.AnimatedVisibility] composable animates the appearance and
+     * disappearance of its content, as [visible] value changes. Different [EnterTransition]s and
+     * [ExitTransition]s can be defined in [enter] and [exit] for the appearance and disappearance
+     * animation.
+     *
+     * @param visible defines whether the content should be visible
+     * @param modifier modifier for the Layout created to contain the [content]
+     * @param enter EnterTransition(s) used for the appearing animation, fading in by default
+     * @param exit ExitTransition(s) used for the disappearing animation, fading out by default
+     * @param label helps differentiate from other animations in Android Studio
+     * @param content Content to appear or disappear based on the value of [visible]
+     *
+     * @link androidx.compose.animation.AnimatedVisibility
+     * @see androidx.compose.animation.AnimatedVisibility
+     * @see EnterTransition
+     * @see ExitTransition
+     * @see AnimatedVisibilityScope
+     */
+    @Composable
+    fun AnimatedVisibility(
+        visible: Boolean,
+        modifier: Modifier = Modifier,
+        enter: EnterTransition = ImmersiveListDefaults.EnterTransition,
+        exit: ExitTransition = ImmersiveListDefaults.ExitTransition,
+        label: String = "AnimatedVisibility",
+        content: @Composable AnimatedVisibilityScope.() -> Unit
+    ) {
+        androidx.compose.animation.AnimatedVisibility(
+            visible,
+            modifier,
+            enter,
+            exit,
+            label,
+            content
+        )
+    }
+
+    /**
+     * [ImmersiveListBackgroundScope.AnimatedContent] is a container that automatically animates its
+     * content when [targetState] changes. Its [content] for different target states is defined in a
+     * mapping between a target state and a composable function.
+     *
+     * @param targetState defines the key to choose the content to be displayed
+     * @param modifier modifier for the Layout created to contain the [content]
+     * @param transitionSpec defines the EnterTransition(s) and ExitTransition(s) used to display
+     * and remove the content, fading in and fading out by default
+     * @param contentAlignment specifies how the background content should be aligned in the
+     * container
+     * @param content Content to appear or disappear based on the value of [targetState]
+     *
+     * @link androidx.compose.animation.AnimatedContent
+     * @see androidx.compose.animation.AnimatedContent
+     * @see ContentTransform
+     * @see AnimatedContentScope
+     */
+    @Suppress("IllegalExperimentalApiUsage")
+    @ExperimentalAnimationApi
+    @Composable
+    fun AnimatedContent(
+        targetState: Int,
+        modifier: Modifier = Modifier,
+        transitionSpec: AnimatedContentScope<Int>.() -> ContentTransform = {
+            ImmersiveListDefaults.EnterTransition.with(ImmersiveListDefaults.ExitTransition)
+        },
+        contentAlignment: Alignment = Alignment.TopStart,
+        content: @Composable AnimatedVisibilityScope.(targetState: Int) -> Unit
+    ) {
+        androidx.compose.animation.AnimatedContent(
+            targetState,
+            modifier,
+            transitionSpec,
+            contentAlignment,
+            content = content
+        )
+    }
+}
+
+@Immutable
+@ExperimentalTvMaterial3Api
+public class ImmersiveListScope internal constructor(private val onFocused: (Int) -> Unit) {
+    /**
+     * Modifier to be added to each of the items of the list within ImmersiveList to inform the
+     * ImmersiveList of the index of the item in focus
+     *
+     * > **NOTE**: This modifier needs to be paired with either the "focusable" or the "clickable"
+     * modifier for it to work
+     *
+     * @param index index of the item within the list
+     */
+    fun Modifier.immersiveListItem(index: Int): Modifier {
+        return onFocusChanged {
+            if (it.isFocused) {
+                onFocused(index)
+            }
+        }
+    }
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/KeyEventPropagation.kt b/tv/tv-material/src/main/java/androidx/tv/material3/KeyEventPropagation.kt
new file mode 100644
index 0000000..a5d8ce8
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/KeyEventPropagation.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+internal object KeyEventPropagation {
+    const val StopPropagation = true
+    const val ContinuePropagation = false
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/MaterialTheme.kt b/tv/tv-material/src/main/java/androidx/tv/material3/MaterialTheme.kt
new file mode 100644
index 0000000..71ec11d
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/MaterialTheme.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.foundation.text.selection.LocalTextSelectionColors
+import androidx.compose.foundation.text.selection.TextSelectionColors
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.runtime.remember
+
+/**
+ * Material Theming refers to the customization of your Material Design app to better reflect your
+ * product’s brand.
+ *
+ * Material components such as Button and Checkbox use values provided here when retrieving
+ * default values.
+ *
+ * All values may be set by providing this component with the [colorScheme][ColorScheme],
+ * [typography][Typography] attributes. Use this to configure the overall
+ * theme of elements within this MaterialTheme.
+ *
+ * Any values that are not set will inherit the current value from the theme, falling back to the
+ * defaults if there is no parent MaterialTheme. This allows using a MaterialTheme at the top
+ * of your application, and then separate MaterialTheme(s) for different screens / parts of your
+ * UI, overriding only the parts of the theme definition that need to change.
+ *
+ * @param colorScheme A complete definition of the Material Color theme for this hierarchy
+ * @param shapes A set of corner shapes to be used as this hierarchy's shape system
+ * @param typography A set of text styles to be used as this hierarchy's typography system
+ * @param content The composable content that will be displayed with this theme
+ */
+@ExperimentalTvMaterial3Api
+@Composable
+fun MaterialTheme(
+    colorScheme: ColorScheme = MaterialTheme.colorScheme,
+    shapes: Shapes = MaterialTheme.shapes,
+    typography: Typography = MaterialTheme.typography,
+    content: @Composable () -> Unit
+) {
+    val rememberedColorScheme = remember {
+        // Explicitly creating a new object here so we don't mutate the initial [colorScheme]
+        // provided, and overwrite the values set in it.
+        colorScheme.copy()
+    }.apply {
+        updateColorSchemeFrom(colorScheme)
+    }
+    val selectionColors = rememberTextSelectionColors(rememberedColorScheme)
+    CompositionLocalProvider(
+        LocalColorScheme provides rememberedColorScheme,
+        LocalShapes provides shapes,
+        LocalTextSelectionColors provides selectionColors,
+        LocalTypography provides typography
+    ) {
+        ProvideTextStyle(value = typography.bodyLarge, content = content)
+    }
+}
+
+/**
+ * Contains functions to access the current theme values provided at the call site's position in
+ * the hierarchy.
+ */
+object MaterialTheme {
+    /**
+     * Retrieves the current [ColorScheme] at the call site's position in the hierarchy.
+     */
+    @ExperimentalTvMaterial3Api
+    val colorScheme: ColorScheme
+        @Composable
+        @ReadOnlyComposable
+        @SuppressWarnings("HiddenTypeParameter", "UnavailableSymbol")
+        get() = LocalColorScheme.current
+
+    /**
+     * Retrieves the current [Typography] at the call site's position in the hierarchy.
+     */
+    val typography: Typography
+        @Composable
+        @ReadOnlyComposable
+        get() = LocalTypography.current
+
+    /**
+     * Retrieves the current [Shapes] at the call site's position in the hierarchy.
+     */
+    val shapes: Shapes
+        @Composable
+        @ReadOnlyComposable
+        get() = LocalShapes.current
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+/*@VisibleForTesting*/
+internal fun rememberTextSelectionColors(colorScheme: ColorScheme): TextSelectionColors {
+    val primaryColor = colorScheme.primary
+    return remember(primaryColor) {
+        TextSelectionColors(
+            handleColor = primaryColor,
+            backgroundColor = primaryColor.copy(alpha = TextSelectionBackgroundOpacity)
+        )
+    }
+}
+
+/*@VisibleForTesting*/
+internal const val TextSelectionBackgroundOpacity = 0.4f
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Shapes.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Shapes.kt
new file mode 100644
index 0000000..9398128
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Shapes.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.CornerBasedShape
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.dp
+import androidx.tv.material3.tokens.ShapeKeyTokens
+import androidx.tv.material3.tokens.ShapeTokens
+
+/**
+ * Material surfaces can be displayed in different shapes. Shapes direct attention, identify
+ * components, communicate state, and express brand.
+ *
+ * The shape scale defines the style of container corners, offering a range of roundedness from
+ * square to fully circular.
+ *
+ * There are different sizes of shapes:
+ * - Extra Small
+ * - Small
+ * - Medium
+ * - Large
+ * - Extra Large
+ *
+ * You can customize the shape system for all components in the [MaterialTheme] or you can do it
+ * on a per component basis.
+ *
+ * You can change the shape that a component has by overriding the shape parameter for that
+ * component. For example, by default, buttons use the shape style “full.” If your product requires
+ * a smaller amount of roundedness, you can override the shape parameter with a different shape
+ * value like MaterialTheme.shapes.small.
+ *
+ * To learn more about shapes, see [Material Design shapes](https://m3.material.io/styles/shape/overview).
+ *
+ * @param extraSmall A shape style with 4 same-sized corners whose size are bigger than
+ * [RectangleShape] and smaller than [Shapes.small]. By default autocomplete menu, select menu,
+ * snackbars, standard menu, and text fields use this shape.
+ * @param small A shape style with 4 same-sized corners whose size are bigger than
+ * [Shapes.extraSmall] and smaller than [Shapes.medium]. By default chips use this shape.
+ * @param medium A shape style with 4 same-sized corners whose size are bigger than [Shapes.small]
+ * and smaller than [Shapes.large]. By default cards and small FABs use this shape.
+ * @param large A shape style with 4 same-sized corners whose size are bigger than [Shapes.medium]
+ * and smaller than [Shapes.extraLarge]. By default extended FABs, FABs, and navigation drawers use
+ * this shape.
+ * @param extraLarge A shape style with 4 same-sized corners whose size are bigger than
+ * [Shapes.large] and smaller than [CircleShape]. By default large FABs use this shape.
+ */
+@Immutable
+class Shapes(
+    // Shapes None and Full are omitted as None is a RectangleShape and Full is a CircleShape.
+    val extraSmall: CornerBasedShape = ShapeDefaults.ExtraSmall,
+    val small: CornerBasedShape = ShapeDefaults.Small,
+    val medium: CornerBasedShape = ShapeDefaults.Medium,
+    val large: CornerBasedShape = ShapeDefaults.Large,
+    val extraLarge: CornerBasedShape = ShapeDefaults.ExtraLarge
+) {
+    /** Returns a copy of this Shapes, optionally overriding some of the values. */
+    fun copy(
+        extraSmall: CornerBasedShape = this.extraSmall,
+        small: CornerBasedShape = this.small,
+        medium: CornerBasedShape = this.medium,
+        large: CornerBasedShape = this.large,
+        extraLarge: CornerBasedShape = this.extraLarge
+    ): Shapes = Shapes(
+        extraSmall = extraSmall,
+        small = small,
+        medium = medium,
+        large = large,
+        extraLarge = extraLarge
+    )
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Shapes) return false
+        if (extraSmall != other.extraSmall) return false
+        if (small != other.small) return false
+        if (medium != other.medium) return false
+        if (large != other.large) return false
+        if (extraLarge != other.extraLarge) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = extraSmall.hashCode()
+        result = 31 * result + small.hashCode()
+        result = 31 * result + medium.hashCode()
+        result = 31 * result + large.hashCode()
+        result = 31 * result + extraLarge.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "Shapes(" +
+            "extraSmall=$extraSmall, " +
+            "small=$small, " +
+            "medium=$medium, " +
+            "large=$large, " +
+            "extraLarge=$extraLarge)"
+    }
+}
+
+/**
+ * Contains the default values used by [Shapes]
+ */
+object ShapeDefaults {
+    /** Extra small sized corner shape */
+    val ExtraSmall: CornerBasedShape = ShapeTokens.CornerExtraSmall
+
+    /** Small sized corner shape */
+    val Small: CornerBasedShape = ShapeTokens.CornerSmall
+
+    /** Medium sized corner shape */
+    val Medium: CornerBasedShape = ShapeTokens.CornerMedium
+
+    /** Large sized corner shape */
+    val Large: CornerBasedShape = ShapeTokens.CornerLarge
+
+    /** Extra large sized corner shape */
+    val ExtraLarge: CornerBasedShape = ShapeTokens.CornerExtraLarge
+}
+
+/** Helper function for component shape tokens. Used to grab the top values of a shape parameter. */
+internal fun CornerBasedShape.top(): CornerBasedShape {
+    return copy(bottomStart = CornerSize(0.0.dp), bottomEnd = CornerSize(0.0.dp))
+}
+
+/** Helper function for component shape tokens. Used to grab the end values of a shape parameter. */
+internal fun CornerBasedShape.end(): CornerBasedShape {
+    return copy(topStart = CornerSize(0.0.dp), bottomStart = CornerSize(0.0.dp))
+}
+
+/**
+ * Helper function for component shape tokens. Here is an example on how to use component color
+ * tokens:
+ * ``MaterialTheme.shapes.fromToken(FabPrimarySmallTokens.ContainerShape)``
+ */
+internal fun Shapes.fromToken(value: ShapeKeyTokens): Shape {
+    return when (value) {
+        ShapeKeyTokens.CornerExtraLarge -> extraLarge
+        ShapeKeyTokens.CornerExtraLargeTop -> extraLarge.top()
+        ShapeKeyTokens.CornerExtraSmall -> extraSmall
+        ShapeKeyTokens.CornerExtraSmallTop -> extraSmall.top()
+        ShapeKeyTokens.CornerFull -> CircleShape
+        ShapeKeyTokens.CornerLarge -> large
+        ShapeKeyTokens.CornerLargeEnd -> large.end()
+        ShapeKeyTokens.CornerLargeTop -> large.top()
+        ShapeKeyTokens.CornerMedium -> medium
+        ShapeKeyTokens.CornerNone -> RectangleShape
+        ShapeKeyTokens.CornerSmall -> small
+    }
+}
+
+/** Converts a shape token key to the local shape provided by the theme */
+@Composable
+internal fun ShapeKeyTokens.toShape(): Shape {
+    return MaterialTheme.shapes.fromToken(this)
+}
+
+/** CompositionLocal used to specify the default shapes for the surfaces. */
+internal val LocalShapes = staticCompositionLocalOf { Shapes() }
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..36e4df2
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
@@ -0,0 +1,313 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.focusable
+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.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.NonRestartableComposable
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.disabled
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
+
+/**
+ * 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 click handling as well as everything else that a
+ * regular Surface does:
+ *
+ * This clickable 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,
+ * updating the [interactionSource] when [PressInteraction] occurs, and showing ripple indication in
+ * response to press events. If you don't need click handling, consider using the Surface function
+ * that doesn't require [onClick] param. If you need to set a custom label for the [onClick], apply
+ * a `Modifier.semantics { onClick(label = "YOUR_LABEL", action = null) }` to the Surface.
+ *
+ * 6) Semantics for clicks. Just like with [Modifier.clickable], clickable version of Surface will
+ * produce semantics to indicate that it is clicked. Also, by default, accessibility services will
+ * describe the element as [Role.Button]. 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 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 role The type of user interface element. Accessibility services might use this
+ * to describe the element or do customizations
+ * @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 content inside this Surface
+ */
+@ExperimentalTvMaterial3Api
+@NonRestartableComposable
+@Composable
+fun Surface(
+    onClick: () -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    shape: Shape = RectangleShape,
+    color: Color = MaterialTheme.colorScheme.surface,
+    contentColor: Color = contentColorFor(color),
+    border: BorderStroke? = null,
+    tonalElevation: Dp = 0.dp,
+    role: Role? = null,
+    shadowElevation: Dp = 0.dp,
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    content: @Composable () -> Unit
+) {
+    SurfaceImpl(
+        modifier = modifier.tvClickable(
+            enabled = enabled,
+            onClick = onClick,
+            interactionSource = interactionSource,
+            role = role
+        ),
+        shape = shape,
+        color = color,
+        contentColor = contentColor,
+        tonalElevation = tonalElevation,
+        shadowElevation = shadowElevation,
+        border = border,
+        content = content
+    )
+}
+
+@ExperimentalTvMaterial3Api
+@Composable
+private fun SurfaceImpl(
+    modifier: Modifier = Modifier,
+    shape: Shape = RectangleShape,
+    color: Color = MaterialTheme.colorScheme.surface,
+    contentColor: Color = contentColorFor(color),
+    tonalElevation: Dp = 0.dp,
+    shadowElevation: Dp = 0.dp,
+    border: BorderStroke? = null,
+    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
+                ),
+            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)
+
+/**
+ * This modifier handles click, press, and focus events for a TV composable.
+ * @param enabled decides whether [onClick] or [onValueChanged] is executed
+ * @param onClick executes the provided lambda
+ * @param value differentiates whether the current item is selected or unselected
+ * @param onValueChanged executes the provided lambda while returning the inverse state of [value]
+ * @param interactionSource used to emit [PressInteraction] events
+ * @param role used to define this composable's semantic role (for Accessibility purposes)
+ */
+private fun Modifier.tvClickable(
+    enabled: Boolean,
+    onClick: (() -> Unit)? = null,
+    value: Boolean = false,
+    onValueChanged: ((Boolean) -> Unit)? = null,
+    interactionSource: MutableInteractionSource,
+    role: Role?
+) = this
+    .handleDPadEnter(
+        enabled = enabled,
+        interactionSource = interactionSource,
+        onClick = onClick,
+        value = value,
+        onValueChanged = onValueChanged
+    )
+    .focusable(interactionSource = interactionSource)
+    .semantics(mergeDescendants = true) {
+        onClick {
+            onClick?.let { nnOnClick ->
+                nnOnClick()
+                return@onClick true
+            } ?: onValueChanged?.let { nnOnValueChanged ->
+                nnOnValueChanged(!value)
+                return@onClick true
+            }
+            false
+        }
+        role?.let { nnRole -> this.role = nnRole }
+        if (!enabled) {
+            disabled()
+        }
+    }
+
+private fun Modifier.handleDPadEnter(
+    enabled: Boolean,
+    interactionSource: MutableInteractionSource,
+    onClick: (() -> Unit)?,
+    value: Boolean,
+    onValueChanged: ((Boolean) -> Unit)?
+) = composed(
+    inspectorInfo = debugInspectorInfo {
+        name = "handleDPadEnter"
+        properties["enabled"] = enabled
+        properties["interactionSource"] = interactionSource
+        properties["onClick"] = onClick
+        properties["onValueChanged"] = onValueChanged
+        properties["value"] = value
+    }
+) {
+    val coroutineScope = rememberCoroutineScope()
+    val pressInteraction = remember { PressInteraction.Press(Offset.Zero) }
+    var isPressed by remember { mutableStateOf(false) }
+    this.then(
+        onKeyEvent { keyEvent ->
+            if (AcceptableKeys.any { keyEvent.nativeKeyEvent.keyCode == it } && enabled) {
+                when (keyEvent.nativeKeyEvent.action) {
+                    NativeKeyEvent.ACTION_DOWN -> {
+                        if (!isPressed) {
+                            isPressed = true
+                            coroutineScope.launch {
+                                interactionSource.emit(pressInteraction)
+                            }
+                        }
+                    }
+
+                    NativeKeyEvent.ACTION_UP -> {
+                        if (isPressed) {
+                            isPressed = false
+                            coroutineScope.launch {
+                                interactionSource.emit(PressInteraction.Release(pressInteraction))
+                            }
+                            onClick?.invoke()
+                            onValueChanged?.invoke(!value)
+                        }
+                    }
+                }
+                return@onKeyEvent KeyEventPropagation.StopPropagation
+            }
+            KeyEventPropagation.ContinuePropagation
+        }
+    )
+}
+
+@Composable
+@ExperimentalTvMaterial3Api
+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 [SurfaceImpl].
+ */
+val LocalAbsoluteTonalElevation = compositionLocalOf { 0.dp }
+private val AcceptableKeys = listOf(
+    NativeKeyEvent.KEYCODE_DPAD_CENTER,
+    NativeKeyEvent.KEYCODE_ENTER,
+    NativeKeyEvent.KEYCODE_NUMPAD_ENTER
+)
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt
new file mode 100644
index 0000000..bed889f
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt
@@ -0,0 +1,301 @@
+/*
+ * 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.tv.material3
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.selected
+import androidx.compose.ui.semantics.semantics
+
+/**
+ * Material Design tab.
+ *
+ * A default Tab, also known as a Primary Navigation Tab. Tabs organize content across different
+ * screens, data sets, and other interactions.
+ *
+ * This should typically be used inside of a [TabRow], see the corresponding documentation for
+ * example usage.
+ *
+ * @param selected whether this tab is selected or not
+ * @param onFocus called when this tab is focused
+ * @param modifier the [Modifier] to be applied to this tab
+ * @param onClick called when this tab is clicked (with D-Pad Center)
+ * @param enabled controls the enabled state of this tab. When `false`, this component will not
+ * respond to user input, and it will appear visually disabled and disabled to accessibility
+ * services.
+ * @param colors these will be used by the tab when in different states (focused,
+ * selected, etc.)
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this tab. You can create and pass in your own `remember`ed instance to observe [Interaction]s
+ * and customize the appearance / behavior of this tab in different states.
+ * @param content content of the [Tab]
+ */
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
+@Composable
+fun Tab(
+  selected: Boolean,
+  onFocus: () -> Unit,
+  modifier: Modifier = Modifier,
+  onClick: () -> Unit = { },
+  enabled: Boolean = true,
+  colors: TabColors = TabDefaults.pillIndicatorTabColors(),
+  interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+  content: @Composable RowScope.() -> Unit
+) {
+  var isFocused by remember { mutableStateOf(false) }
+  val contentColor by
+    animateColorAsState(
+      getTabContentColor(
+        colors = colors,
+        isTabRowActive = LocalTabRowHasFocus.current,
+        focused = isFocused,
+        selected = selected,
+        enabled = enabled,
+      )
+    )
+  CompositionLocalProvider(LocalContentColor provides contentColor) {
+    Row(
+      modifier =
+        modifier
+          .semantics {
+            this.selected = selected
+            this.role = Role.Tab
+          }
+          .onFocusChanged {
+            isFocused = it.isFocused
+            if (it.isFocused) {
+              onFocus()
+            }
+          }
+          .focusable(enabled = enabled, interactionSource)
+          .clickable(
+            enabled = enabled,
+            role = Role.Tab,
+            onClick = onClick,
+          ),
+      horizontalArrangement = Arrangement.Center,
+      verticalAlignment = Alignment.CenterVertically,
+      content = content
+    )
+  }
+}
+
+/**
+ * Represents the colors used in a tab in different states.
+ *
+ * - See [TabDefaults.pillIndicatorTabColors] for the default colors used in a [Tab] when using a
+ * Pill indicator.
+ * - See [TabDefaults.underlinedIndicatorTabColors] for the default colors used in a [Tab] when
+ * using an Underlined indicator
+ */
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
+class TabColors
+internal constructor(
+  private val activeContentColor: Color,
+  private val contentColor: Color = activeContentColor.copy(alpha = 0.4f),
+  private val selectedContentColor: Color,
+  private val focusedContentColor: Color,
+  private val focusedSelectedContentColor: Color,
+  private val disabledActiveContentColor: Color,
+  private val disabledContentColor: Color = disabledActiveContentColor.copy(alpha = 0.4f),
+  private val disabledSelectedContentColor: Color,
+) {
+  /**
+   * Represents the content color for this tab, depending on whether it is inactive and [enabled]
+   *
+   * [Tab] is inactive when the [TabRow] is not focused
+   *
+   * @param enabled whether the button is enabled
+   */
+  internal fun inactiveContentColor(enabled: Boolean): Color {
+    return if (enabled) contentColor else disabledContentColor
+  }
+
+  /**
+   * Represents the content color for this tab, depending on whether it is active and [enabled]
+   *
+   * [Tab] is active when some other [Tab] is focused
+   *
+   * @param enabled whether the button is enabled
+   */
+  internal fun activeContentColor(enabled: Boolean): Color {
+    return if (enabled) activeContentColor else disabledActiveContentColor
+  }
+
+  /**
+   * Represents the content color for this tab, depending on whether it is selected and [enabled]
+   *
+   * [Tab] is selected when the current [Tab] is selected and not focused
+   *
+   * @param enabled whether the button is enabled
+   */
+  internal fun selectedContentColor(enabled: Boolean): Color {
+    return if (enabled) selectedContentColor else disabledSelectedContentColor
+  }
+
+  /**
+   * Represents the content color for this tab, depending on whether it is focused
+   *
+   * * [Tab] is focused when the current [Tab] is selected and focused
+   *
+   * @param selected whether the tab is selected
+   */
+  internal fun focusedContentColor(selected: Boolean): Color {
+    return if (selected) focusedSelectedContentColor else focusedContentColor
+  }
+
+  override fun equals(other: Any?): Boolean {
+    if (this === other) return true
+    if (other == null || other !is TabColors) return false
+
+    if (activeContentColor != other.activeContentColor) return false
+    if (contentColor != other.contentColor) return false
+    if (selectedContentColor != other.selectedContentColor) return false
+    if (focusedContentColor != other.focusedContentColor) return false
+    if (focusedSelectedContentColor != other.focusedSelectedContentColor) return false
+    if (disabledActiveContentColor != other.disabledActiveContentColor) return false
+    if (disabledContentColor != other.disabledContentColor) return false
+    if (disabledSelectedContentColor != other.disabledSelectedContentColor) return false
+
+    return true
+  }
+
+  override fun hashCode(): Int {
+    var result = activeContentColor.hashCode()
+    result = 31 * result + contentColor.hashCode()
+    result = 31 * result + selectedContentColor.hashCode()
+    result = 31 * result + focusedContentColor.hashCode()
+    result = 31 * result + focusedSelectedContentColor.hashCode()
+    result = 31 * result + disabledActiveContentColor.hashCode()
+    result = 31 * result + disabledContentColor.hashCode()
+    result = 31 * result + disabledSelectedContentColor.hashCode()
+    return result
+  }
+}
+
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
+object TabDefaults {
+  /**
+   * [Tab]'s content colors to in conjunction with underlined indicator
+   *
+   * @param activeContentColor applied when the any of the other tabs is focused
+   * @param contentColor the default color of the tab's content when none of the tabs are focused
+   * @param selectedContentColor applied when the current tab is selected
+   * @param focusedContentColor applied when the current tab is focused
+   * @param focusedSelectedContentColor applied when the current tab is both focused and selected
+   * @param disabledActiveContentColor applied when any of the other tabs is focused and the
+   * current tab is disabled
+   * @param disabledContentColor applied when the current tab is disabled and none of the tabs are
+   * focused
+   * @param disabledSelectedContentColor applied when the current tab is disabled and selected
+   */
+  // TODO: get selectedContentColor & focusedContentColor values from theme
+  @OptIn(ExperimentalTvMaterial3Api::class)
+  @Composable
+  fun underlinedIndicatorTabColors(
+    activeContentColor: Color = LocalContentColor.current,
+    contentColor: Color = activeContentColor.copy(alpha = 0.4f),
+    selectedContentColor: Color = Color(0xFFC9C2E8),
+    focusedContentColor: Color = Color(0xFFC9BFFF),
+    focusedSelectedContentColor: Color = focusedContentColor,
+    disabledActiveContentColor: Color = activeContentColor,
+    disabledContentColor: Color = disabledActiveContentColor.copy(alpha = 0.4f),
+    disabledSelectedContentColor: Color = selectedContentColor,
+  ): TabColors =
+    TabColors(
+      activeContentColor = activeContentColor,
+      contentColor = contentColor,
+      selectedContentColor = selectedContentColor,
+      focusedContentColor = focusedContentColor,
+      focusedSelectedContentColor = focusedSelectedContentColor,
+      disabledActiveContentColor = disabledActiveContentColor,
+      disabledContentColor = disabledContentColor,
+      disabledSelectedContentColor = disabledSelectedContentColor,
+    )
+
+  /**
+   * [Tab]'s content colors to in conjunction with pill indicator
+   *
+   * @param activeContentColor applied when the any of the other tabs is focused
+   * @param contentColor the default color of the tab's content when none of the tabs are focused
+   * @param selectedContentColor applied when the current tab is selected
+   * @param focusedContentColor applied when the current tab is focused
+   * @param focusedSelectedContentColor applied when the current tab is both focused and selected
+   * @param disabledActiveContentColor applied when any of the other tabs is focused and the
+   * current tab is disabled
+   * @param disabledContentColor applied when the current tab is disabled and none of the tabs are
+   * focused
+   * @param disabledSelectedContentColor applied when the current tab is disabled and selected
+   */
+  // TODO: get selected & focused values from theme
+  @OptIn(ExperimentalTvMaterial3Api::class)
+  @Composable
+  fun pillIndicatorTabColors(
+    activeContentColor: Color = LocalContentColor.current,
+    contentColor: Color = activeContentColor.copy(alpha = 0.4f),
+    selectedContentColor: Color = Color(0xFFE5DEFF),
+    focusedContentColor: Color = Color(0xFF313033),
+    focusedSelectedContentColor: Color = focusedContentColor,
+    disabledActiveContentColor: Color = activeContentColor,
+    disabledContentColor: Color = disabledActiveContentColor.copy(alpha = 0.4f),
+    disabledSelectedContentColor: Color = selectedContentColor,
+  ): TabColors =
+    TabColors(
+      activeContentColor = activeContentColor,
+      contentColor = contentColor,
+      selectedContentColor = selectedContentColor,
+      focusedContentColor = focusedContentColor,
+      focusedSelectedContentColor = focusedSelectedContentColor,
+      disabledActiveContentColor = disabledActiveContentColor,
+      disabledContentColor = disabledContentColor,
+      disabledSelectedContentColor = disabledSelectedContentColor,
+    )
+}
+
+/** Returns the [Tab]'s content color based on focused/selected state */
+@OptIn(ExperimentalTvMaterial3Api::class)
+private fun getTabContentColor(
+  colors: TabColors,
+  isTabRowActive: Boolean,
+  focused: Boolean,
+  selected: Boolean,
+  enabled: Boolean,
+): Color =
+  when {
+    focused -> colors.focusedContentColor(selected)
+    selected -> colors.selectedContentColor(enabled)
+    isTabRowActive -> colors.activeContentColor(enabled)
+    else -> colors.inactiveContentColor(enabled)
+  }
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt b/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt
new file mode 100644
index 0000000..a1af7e7
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt
@@ -0,0 +1,285 @@
+/*
+ * 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.tv.material3
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.SubcomposeLayout
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.DpRect
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.width
+import androidx.compose.ui.zIndex
+
+/**
+ * TV-Material Design Horizontal TabRow
+ *
+ * Display all tabs in a set simultaneously and if the tabs exceed the container size, it has
+ * scrolling to navigate to next tab. They are best for switching between related content quickly,
+ * such as between transportation methods in a map. To navigate between tabs, use d-pad left or
+ * d-pad right when focused.
+ *
+ * A TvTabRow contains a row of []s, and displays an indicator underneath the currently selected
+ * tab. A TvTabRow places its tabs offset from the starting edge, and allows scrolling to tabs that
+ * are placed off screen.
+ *
+ * Examples:
+ * @sample androidx.tv.samples.PillIndicatorTabRow
+ * @sample androidx.tv.samples.UnderlinedIndicatorTabRow
+ * @sample androidx.tv.samples.TabRowWithDebounce
+ * @sample androidx.tv.samples.OnClickNavigation
+ *
+ * @param selectedTabIndex the index of the currently selected tab
+ * @param modifier the [Modifier] to be applied to this tab row
+ * @param containerColor the color used for the background of this tab row
+ * @param contentColor the primary color used in the tabs
+ * @param separator use this composable to add a separator between the tabs
+ * @param indicator used to indicate which tab is currently selected and/or focused
+ * @param tabs a composable which will render all the tabs
+ */
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
+@Composable
+fun TabRow(
+  selectedTabIndex: Int,
+  modifier: Modifier = Modifier,
+  containerColor: Color = TabRowDefaults.ContainerColor,
+  contentColor: Color = TabRowDefaults.contentColor(),
+  separator: @Composable () -> Unit = { TabRowDefaults.TabSeparator() },
+  indicator: @Composable (tabPositions: List<DpRect>) -> Unit =
+    @Composable { tabPositions ->
+      tabPositions.getOrNull(selectedTabIndex)?.let {
+        TabRowDefaults.PillIndicator(currentTabPosition = it)
+      }
+    },
+  tabs: @Composable () -> Unit
+) {
+  val scrollState = rememberScrollState()
+  var isAnyTabFocused by remember { mutableStateOf(false) }
+
+  CompositionLocalProvider(
+    LocalTabRowHasFocus provides isAnyTabFocused,
+    LocalContentColor provides contentColor
+  ) {
+    SubcomposeLayout(
+      modifier =
+        modifier
+          .background(containerColor)
+          .clipToBounds()
+          .horizontalScroll(scrollState)
+          .onFocusChanged { isAnyTabFocused = it.hasFocus }
+          .selectableGroup()
+    ) { constraints ->
+      // Tab measurables
+      val tabMeasurables = subcompose(TabRowSlots.Tabs, tabs)
+
+      // Tab placeables
+      val tabPlaceables =
+        tabMeasurables.map { it.measure(constraints.copy(minWidth = 0, minHeight = 0)) }
+      val tabsCount = tabMeasurables.size
+      val separatorsCount = tabsCount - 1
+
+      // Separators
+      val separators = @Composable { repeat(separatorsCount) { separator() } }
+      val separatorMeasurables = subcompose(TabRowSlots.Separator, separators)
+      val separatorPlaceables =
+        separatorMeasurables.map { it.measure(constraints.copy(minWidth = 0, minHeight = 0)) }
+      val separatorWidth = separatorPlaceables.firstOrNull()?.width ?: 0
+
+      val layoutWidth = tabPlaceables.sumOf { it.width } + separatorsCount * separatorWidth
+      val layoutHeight =
+        (tabMeasurables.maxOfOrNull { it.maxIntrinsicHeight(Constraints.Infinity) } ?: 0)
+          .coerceAtLeast(0)
+
+      // Position the children
+      layout(layoutWidth, layoutHeight) {
+
+        // Place the tabs
+        val tabPositions = mutableListOf<DpRect>()
+        var left = 0
+        tabPlaceables.forEachIndexed { index, tabPlaceable ->
+          // place the tab
+          tabPlaceable.placeRelative(left, 0)
+
+          tabPositions.add(
+            this@SubcomposeLayout.buildTabPosition(placeable = tabPlaceable, initialLeft = left)
+          )
+          left += tabPlaceable.width
+
+          // place the separator
+          if (tabPlaceables.lastIndex != index) {
+            separatorPlaceables[index].placeRelative(left, 0)
+          }
+
+          left += separatorWidth
+        }
+
+        // Place the indicator
+        subcompose(TabRowSlots.Indicator) { indicator(tabPositions) }
+          .forEach { it.measure(Constraints.fixed(layoutWidth, layoutHeight)).placeRelative(0, 0) }
+      }
+    }
+  }
+}
+
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
+object TabRowDefaults {
+  /** Color of the background of a tab */
+  val ContainerColor = Color.Transparent
+
+  /** Space between tabs in the tab row */
+  @Composable
+  fun TabSeparator() {
+    Spacer(modifier = Modifier.width(20.dp))
+  }
+
+  /** Default accent color for the TabRow */
+  // TODO: Use value from a theme
+  @Composable fun contentColor(): Color = Color(0xFFC9C5D0)
+
+  /**
+   * Adds a pill indicator behind the tab
+   *
+   * @param currentTabPosition position of the current selected tab
+   * @param modifier modifier to be applied to the indicator
+   * @param activeColor color of indicator when [TabRow] is active
+   * @param inactiveColor color of indicator when [TabRow] is inactive
+   */
+  @Composable
+  fun PillIndicator(
+    currentTabPosition: DpRect,
+    modifier: Modifier = Modifier,
+    activeColor: Color = Color(0xFFE5E1E6),
+    inactiveColor: Color = Color(0xFF484362).copy(alpha = 0.4f)
+  ) {
+    val anyTabFocused = LocalTabRowHasFocus.current
+    val width by animateDpAsState(targetValue = currentTabPosition.width)
+    val height = currentTabPosition.height
+    val leftOffset by animateDpAsState(targetValue = currentTabPosition.left)
+    val topOffset = currentTabPosition.top
+
+    val pillColor by
+      animateColorAsState(targetValue = if (anyTabFocused) activeColor else inactiveColor)
+
+    Box(
+      modifier
+        .fillMaxWidth()
+        .wrapContentSize(Alignment.BottomStart)
+        .offset(x = leftOffset, y = topOffset)
+        .width(width)
+        .height(height)
+        .background(color = pillColor, shape = RoundedCornerShape(50))
+        .zIndex(-1f)
+    )
+  }
+
+  /**
+   * Adds an underlined indicator below the tab
+   *
+   * @param currentTabPosition position of the current selected tab
+   * @param modifier modifier to be applied to the indicator
+   * @param activeColor color of indicator when [TabRow] is active
+   * @param inactiveColor color of indicator when [TabRow] is inactive
+   */
+  @Composable
+  fun UnderlinedIndicator(
+    currentTabPosition: DpRect,
+    modifier: Modifier = Modifier,
+    activeColor: Color = Color(0xFFC9BFFF),
+    inactiveColor: Color = Color(0xFFC9C2E8)
+  ) {
+    val anyTabFocused = LocalTabRowHasFocus.current
+    val unfocusedUnderlineWidth = 10.dp
+    val indicatorHeight = 2.dp
+    val width by
+      animateDpAsState(
+        targetValue = if (anyTabFocused) currentTabPosition.width else unfocusedUnderlineWidth
+      )
+    val leftOffset by
+      animateDpAsState(
+        targetValue =
+          if (anyTabFocused) {
+            currentTabPosition.left
+          } else {
+            val tabCenter = currentTabPosition.left + currentTabPosition.width / 2
+            tabCenter - unfocusedUnderlineWidth / 2
+          }
+      )
+
+    val underlineColor by
+      animateColorAsState(targetValue = if (anyTabFocused) activeColor else inactiveColor)
+
+    Box(
+      modifier
+        .fillMaxWidth()
+        .wrapContentSize(Alignment.BottomStart)
+        .offset(x = leftOffset)
+        .width(width)
+        .height(indicatorHeight)
+        .background(color = underlineColor)
+    )
+  }
+}
+
+/** A provider to store whether any [Tab] is focused inside the [TabRow] */
+internal val LocalTabRowHasFocus = compositionLocalOf { false }
+
+/** Slots for [TabRow]'s content */
+private enum class TabRowSlots {
+  Tabs,
+  Indicator,
+  Separator
+}
+
+/** Builds TabPosition based on placeable */
+private fun Density.buildTabPosition(
+  placeable: Placeable,
+  initialLeft: Int = 0,
+  initialTop: Int = 0,
+): DpRect =
+  DpRect(
+    left = initialLeft.toDp(),
+    right = (initialLeft + placeable.width).toDp(),
+    top = initialTop.toDp(),
+    bottom = (initialTop + placeable.height).toDp(),
+  )
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Text.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Text.kt
new file mode 100644
index 0000000..cd7aaec
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Text.kt
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.InlineTextContent
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.structuralEqualityPolicy
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.takeOrElse
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.Paragraph
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.TextUnit
+
+/**
+ * High level element that displays text and provides semantics / accessibility information.
+ *
+ * The default [style] uses the [LocalTextStyle] provided by the [MaterialTheme] / components. If
+ * you are setting your own style, you may want to consider first retrieving [LocalTextStyle],
+ * and using [TextStyle.copy] to keep any theme defined attributes, only modifying the specific
+ * attributes you want to override.
+ *
+ * For ease of use, commonly used parameters from [TextStyle] are also present here. The order of
+ * precedence is as follows:
+ * - If a parameter is explicitly set here (i.e, it is _not_ `null` or [TextUnit.Unspecified]),
+ * then this parameter will always be used.
+ * - If a parameter is _not_ set, (`null` or [TextUnit.Unspecified]), then the corresponding value
+ * from [style] will be used instead.
+ *
+ * Additionally, for [color], if [color] is not set, and [style] does not have a color, then
+ * [LocalContentColor] will be used.
+ *
+ * @param text the text to be displayed
+ * @param modifier the [Modifier] to be applied to this layout node
+ * @param color [Color] to apply to the text. If [Color.Unspecified], and [style] has no color set,
+ * this will be [LocalContentColor].
+ * @param fontSize the size of glyphs to use when painting the text. See [TextStyle.fontSize].
+ * @param fontStyle the typeface variant to use when drawing the letters (e.g., italic).
+ * See [TextStyle.fontStyle].
+ * @param fontWeight the typeface thickness to use when painting the text (e.g., [FontWeight.Bold]).
+ * @param fontFamily the font family to be used when rendering the text. See [TextStyle.fontFamily].
+ * @param letterSpacing the amount of space to add between each letter.
+ * See [TextStyle.letterSpacing].
+ * @param textDecoration the decorations to paint on the text (e.g., an underline).
+ * See [TextStyle.textDecoration].
+ * @param textAlign the alignment of the text within the lines of the paragraph.
+ * See [TextStyle.textAlign].
+ * @param lineHeight line height for the [Paragraph] in [TextUnit] unit, e.g. SP or EM.
+ * See [TextStyle.lineHeight].
+ * @param overflow how visual overflow should be handled.
+ * @param softWrap whether the text should break at soft line breaks. If false, the glyphs in the
+ * text will be positioned as if there was unlimited horizontal space. If [softWrap] is false,
+ * [overflow] and TextAlign may have unexpected effects.
+ * @param maxLines an optional maximum number of lines for the text to span, wrapping if
+ * necessary. If the text exceeds the given number of lines, it will be truncated according to
+ * [overflow] and [softWrap]. If it is not null, then it must be greater than zero.
+ * @param onTextLayout callback that is executed when a new text layout is calculated. A
+ * [TextLayoutResult] object that callback provides contains paragraph information, size of the
+ * text, baselines and other details. The callback can be used to add additional decoration or
+ * functionality to the text. For example, to draw selection around the text.
+ * @param style style configuration for the text such as color, font, line height etc.
+ */
+@Composable
+fun Text(
+    text: String,
+    modifier: Modifier = Modifier,
+    color: Color = Color.Unspecified,
+    fontSize: TextUnit = TextUnit.Unspecified,
+    fontStyle: FontStyle? = null,
+    fontWeight: FontWeight? = null,
+    fontFamily: FontFamily? = null,
+    letterSpacing: TextUnit = TextUnit.Unspecified,
+    textDecoration: TextDecoration? = null,
+    textAlign: TextAlign? = null,
+    lineHeight: TextUnit = TextUnit.Unspecified,
+    overflow: TextOverflow = TextOverflow.Clip,
+    softWrap: Boolean = true,
+    maxLines: Int = Int.MAX_VALUE,
+    onTextLayout: (TextLayoutResult) -> Unit = {},
+    style: TextStyle = LocalTextStyle.current
+) {
+    val textColor = color.takeOrElse {
+        style.color.takeOrElse {
+            LocalContentColor.current
+        }
+    }
+    // NOTE(text-perf-review): It might be worthwhile writing a bespoke merge implementation that
+    // will avoid reallocating if all of the options here are the defaults
+    val mergedStyle = style.merge(
+        TextStyle(
+            color = textColor,
+            fontSize = fontSize,
+            fontWeight = fontWeight,
+            textAlign = textAlign,
+            lineHeight = lineHeight,
+            fontFamily = fontFamily,
+            textDecoration = textDecoration,
+            fontStyle = fontStyle,
+            letterSpacing = letterSpacing
+        )
+    )
+    BasicText(
+        text,
+        modifier,
+        mergedStyle,
+        onTextLayout,
+        overflow,
+        softWrap,
+        maxLines
+    )
+}
+
+/**
+ * High level element that displays text and provides semantics / accessibility information.
+ *
+ * The default [style] uses the [LocalTextStyle] provided by the [MaterialTheme] / components. If
+ * you are setting your own style, you may want to consider first retrieving [LocalTextStyle],
+ * and using [TextStyle.copy] to keep any theme defined attributes, only modifying the specific
+ * attributes you want to override.
+ *
+ * For ease of use, commonly used parameters from [TextStyle] are also present here. The order of
+ * precedence is as follows:
+ * - If a parameter is explicitly set here (i.e, it is _not_ `null` or [TextUnit.Unspecified]),
+ * then this parameter will always be used.
+ * - If a parameter is _not_ set, (`null` or [TextUnit.Unspecified]), then the corresponding value
+ * from [style] will be used instead.
+ *
+ * Additionally, for [color], if [color] is not set, and [style] does not have a color, then
+ * [LocalContentColor] will be used.
+ *
+ * @param text the text to be displayed
+ * @param modifier the [Modifier] to be applied to this layout node
+ * @param color [Color] to apply to the text. If [Color.Unspecified], and [style] has no color set,
+ * this will be [LocalContentColor].
+ * @param fontSize the size of glyphs to use when painting the text. See [TextStyle.fontSize].
+ * @param fontStyle the typeface variant to use when drawing the letters (e.g., italic).
+ * See [TextStyle.fontStyle].
+ * @param fontWeight the typeface thickness to use when painting the text (e.g., [FontWeight.Bold]).
+ * @param fontFamily the font family to be used when rendering the text. See [TextStyle.fontFamily].
+ * @param letterSpacing the amount of space to add between each letter.
+ * See [TextStyle.letterSpacing].
+ * @param textDecoration the decorations to paint on the text (e.g., an underline).
+ * See [TextStyle.textDecoration].
+ * @param textAlign the alignment of the text within the lines of the paragraph.
+ * See [TextStyle.textAlign].
+ * @param lineHeight line height for the [Paragraph] in [TextUnit] unit, e.g. SP or EM.
+ * See [TextStyle.lineHeight].
+ * @param overflow how visual overflow should be handled.
+ * @param softWrap whether the text should break at soft line breaks. If false, the glyphs in the
+ * text will be positioned as if there was unlimited horizontal space. If [softWrap] is false,
+ * [overflow] and TextAlign may have unexpected effects.
+ * @param maxLines an optional maximum number of lines for the text to span, wrapping if
+ * necessary. If the text exceeds the given number of lines, it will be truncated according to
+ * [overflow] and [softWrap]. If it is not null, then it must be greater than zero.
+ * @param inlineContent a map storing composables that replaces certain ranges of the text, used to
+ * insert composables into text layout. See [InlineTextContent].
+ * @param onTextLayout callback that is executed when a new text layout is calculated. A
+ * [TextLayoutResult] object that callback provides contains paragraph information, size of the
+ * text, baselines and other details. The callback can be used to add additional decoration or
+ * functionality to the text. For example, to draw selection around the text.
+ * @param style style configuration for the text such as color, font, line height etc.
+ */
+@Composable
+fun Text(
+    text: AnnotatedString,
+    modifier: Modifier = Modifier,
+    color: Color = Color.Unspecified,
+    fontSize: TextUnit = TextUnit.Unspecified,
+    fontStyle: FontStyle? = null,
+    fontWeight: FontWeight? = null,
+    fontFamily: FontFamily? = null,
+    letterSpacing: TextUnit = TextUnit.Unspecified,
+    textDecoration: TextDecoration? = null,
+    textAlign: TextAlign? = null,
+    lineHeight: TextUnit = TextUnit.Unspecified,
+    overflow: TextOverflow = TextOverflow.Clip,
+    softWrap: Boolean = true,
+    maxLines: Int = Int.MAX_VALUE,
+    inlineContent: Map<String, InlineTextContent> = mapOf(),
+    onTextLayout: (TextLayoutResult) -> Unit = {},
+    style: TextStyle = LocalTextStyle.current
+) {
+    val textColor = color.takeOrElse {
+        style.color.takeOrElse {
+            LocalContentColor.current
+        }
+    }
+    // NOTE(text-perf-review): It might be worthwhile writing a bespoke merge implementation that
+    // will avoid reallocating if all of the options here are the defaults
+    val mergedStyle = style.merge(
+        TextStyle(
+            color = textColor,
+            fontSize = fontSize,
+            fontWeight = fontWeight,
+            textAlign = textAlign,
+            lineHeight = lineHeight,
+            fontFamily = fontFamily,
+            textDecoration = textDecoration,
+            fontStyle = fontStyle,
+            letterSpacing = letterSpacing
+        )
+    )
+    BasicText(
+        text = text,
+        modifier = modifier,
+        style = mergedStyle,
+        onTextLayout = onTextLayout,
+        overflow = overflow,
+        softWrap = softWrap,
+        maxLines = maxLines,
+        inlineContent = inlineContent
+    )
+}
+
+/**
+ * CompositionLocal containing the preferred [TextStyle] that will be used by [Text] components by
+ * default. To set the value for this CompositionLocal, see [ProvideTextStyle] which will merge any
+ * missing [TextStyle] properties with the existing [TextStyle] set in this CompositionLocal.
+ *
+ * @see ProvideTextStyle
+ */
+val LocalTextStyle = compositionLocalOf(structuralEqualityPolicy()) { TextStyle.Default }
+
+// TODO(b/156598010): remove this and replace with fold definition on the backing CompositionLocal
+/**
+ * This function is used to set the current value of [LocalTextStyle], merging the given style
+ * with the current style values for any missing attributes. Any [Text] components included in
+ * this component's [content] will be styled with this style unless styled explicitly.
+ *
+ * @see LocalTextStyle
+ */
+@Composable
+fun ProvideTextStyle(value: TextStyle, content: @Composable () -> Unit) {
+    val mergedStyle = LocalTextStyle.current.merge(value)
+    CompositionLocalProvider(LocalTextStyle provides mergedStyle, content = content)
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Typography.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Typography.kt
new file mode 100644
index 0000000..014694b
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Typography.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.text.TextStyle
+import androidx.tv.material3.tokens.TypographyKeyTokens
+import androidx.tv.material3.tokens.TypographyTokens
+
+/**
+ * The Material Design type scale includes a range of contrasting styles that support the needs of
+ * your product and its content.
+ *
+ * Use typography to make writing legible and beautiful. Material's default type scale includes
+ * contrasting and flexible styles to support a wide range of use cases.
+ *
+ * The type scale is a combination of thirteen styles that are supported by the type system. It
+ * contains reusable categories of text, each with an intended application and meaning.
+ *
+ * To learn more about typography, see [Material Design typography](https://m3.material.io/styles/typography/overview).
+ *
+ * @property displayLarge displayLarge is the largest display text.
+ * @property displayMedium displayMedium is the second largest display text.
+ * @property displaySmall displaySmall is the smallest display text.
+ * @property headlineLarge headlineLarge is the largest headline, reserved for short, important text
+ * or numerals. For headlines, you can choose an expressive font, such as a display, handwritten, or
+ * script style. These unconventional font designs have details and intricacy that help attract the
+ * eye.
+ * @property headlineMedium headlineMedium is the second largest headline, reserved for short,
+ * important text or numerals. For headlines, you can choose an expressive font, such as a display,
+ * handwritten, or script style. These unconventional font designs have details and intricacy that
+ * help attract the eye.
+ * @property headlineSmall headlineSmall is the smallest headline, reserved for short, important
+ * text or numerals. For headlines, you can choose an expressive font, such as a display,
+ * handwritten, or script style. These unconventional font designs have details and intricacy that
+ * help attract the eye.
+ * @property titleLarge titleLarge is the largest title, and is typically reserved for
+ * medium-emphasis text that is shorter in length. Serif or sans serif typefaces work well for
+ * subtitles.
+ * @property titleMedium titleMedium is the second largest title, and is typically reserved for
+ * medium-emphasis text that is shorter in length. Serif or sans serif typefaces work well for
+ * subtitles.
+ * @property titleSmall titleSmall is the smallest title, and is typically reserved for
+ * medium-emphasis text that is shorter in length. Serif or sans serif typefaces work well for
+ * subtitles.
+ * @property bodyLarge bodyLarge is the largest body, and is typically used for long-form writing as
+ * it works well for small text sizes. For longer sections of text, a serif or sans serif typeface
+ * is recommended.
+ * @property bodyMedium bodyMedium is the second largest body, and is typically used for long-form
+ * writing as it works well for small text sizes. For longer sections of text, a serif or sans serif
+ * typeface is recommended.
+ * @property bodySmall bodySmall is the smallest body, and is typically used for long-form writing
+ * as it works well for small text sizes. For longer sections of text, a serif or sans serif
+ * typeface is recommended.
+ * @property labelLarge labelLarge text is a call to action used in different types of buttons (such
+ * as text, outlined and contained buttons) and in tabs, dialogs, and cards. Button text is
+ * typically sans serif, using all caps text.
+ * @property labelMedium labelMedium is one of the smallest font sizes. It is used sparingly to
+ * annotate imagery or to introduce a headline.
+ * @property labelSmall labelSmall is one of the smallest font sizes. It is used sparingly to
+ * annotate imagery or to introduce a headline.
+ */
+@Immutable
+class Typography(
+    val displayLarge: TextStyle = TypographyTokens.DisplayLarge,
+    val displayMedium: TextStyle = TypographyTokens.DisplayMedium,
+    val displaySmall: TextStyle = TypographyTokens.DisplaySmall,
+    val headlineLarge: TextStyle = TypographyTokens.HeadlineLarge,
+    val headlineMedium: TextStyle = TypographyTokens.HeadlineMedium,
+    val headlineSmall: TextStyle = TypographyTokens.HeadlineSmall,
+    val titleLarge: TextStyle = TypographyTokens.TitleLarge,
+    val titleMedium: TextStyle = TypographyTokens.TitleMedium,
+    val titleSmall: TextStyle = TypographyTokens.TitleSmall,
+    val bodyLarge: TextStyle = TypographyTokens.BodyLarge,
+    val bodyMedium: TextStyle = TypographyTokens.BodyMedium,
+    val bodySmall: TextStyle = TypographyTokens.BodySmall,
+    val labelLarge: TextStyle = TypographyTokens.LabelLarge,
+    val labelMedium: TextStyle = TypographyTokens.LabelMedium,
+    val labelSmall: TextStyle = TypographyTokens.LabelSmall
+) {
+
+    /** Returns a copy of this Typography, optionally overriding some of the values. */
+    fun copy(
+        displayLarge: TextStyle = this.displayLarge,
+        displayMedium: TextStyle = this.displayMedium,
+        displaySmall: TextStyle = this.displaySmall,
+        headlineLarge: TextStyle = this.headlineLarge,
+        headlineMedium: TextStyle = this.headlineMedium,
+        headlineSmall: TextStyle = this.headlineSmall,
+        titleLarge: TextStyle = this.titleLarge,
+        titleMedium: TextStyle = this.titleMedium,
+        titleSmall: TextStyle = this.titleSmall,
+        bodyLarge: TextStyle = this.bodyLarge,
+        bodyMedium: TextStyle = this.bodyMedium,
+        bodySmall: TextStyle = this.bodySmall,
+        labelLarge: TextStyle = this.labelLarge,
+        labelMedium: TextStyle = this.labelMedium,
+        labelSmall: TextStyle = this.labelSmall
+    ): Typography =
+        Typography(
+            displayLarge = displayLarge,
+            displayMedium = displayMedium,
+            displaySmall = displaySmall,
+            headlineLarge = headlineLarge,
+            headlineMedium = headlineMedium,
+            headlineSmall = headlineSmall,
+            titleLarge = titleLarge,
+            titleMedium = titleMedium,
+            titleSmall = titleSmall,
+            bodyLarge = bodyLarge,
+            bodyMedium = bodyMedium,
+            bodySmall = bodySmall,
+            labelLarge = labelLarge,
+            labelMedium = labelMedium,
+            labelSmall = labelSmall
+        )
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Typography) return false
+
+        if (displayLarge != other.displayLarge) return false
+        if (displayMedium != other.displayMedium) return false
+        if (displaySmall != other.displaySmall) return false
+        if (headlineLarge != other.headlineLarge) return false
+        if (headlineMedium != other.headlineMedium) return false
+        if (headlineSmall != other.headlineSmall) return false
+        if (titleLarge != other.titleLarge) return false
+        if (titleMedium != other.titleMedium) return false
+        if (titleSmall != other.titleSmall) return false
+        if (bodyLarge != other.bodyLarge) return false
+        if (bodyMedium != other.bodyMedium) return false
+        if (bodySmall != other.bodySmall) return false
+        if (labelLarge != other.labelLarge) return false
+        if (labelMedium != other.labelMedium) return false
+        if (labelSmall != other.labelSmall) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = displayLarge.hashCode()
+        result = 31 * result + displayMedium.hashCode()
+        result = 31 * result + displaySmall.hashCode()
+        result = 31 * result + headlineLarge.hashCode()
+        result = 31 * result + headlineMedium.hashCode()
+        result = 31 * result + headlineSmall.hashCode()
+        result = 31 * result + titleLarge.hashCode()
+        result = 31 * result + titleMedium.hashCode()
+        result = 31 * result + titleSmall.hashCode()
+        result = 31 * result + bodyLarge.hashCode()
+        result = 31 * result + bodyMedium.hashCode()
+        result = 31 * result + bodySmall.hashCode()
+        result = 31 * result + labelLarge.hashCode()
+        result = 31 * result + labelMedium.hashCode()
+        result = 31 * result + labelSmall.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "Typography(displayLarge=$displayLarge, displayMedium=$displayMedium," +
+            "displaySmall=$displaySmall, " +
+            "headlineLarge=$headlineLarge, headlineMedium=$headlineMedium," +
+            " headlineSmall=$headlineSmall, " +
+            "titleLarge=$titleLarge, titleMedium=$titleMedium, titleSmall=$titleSmall, " +
+            "bodyLarge=$bodyLarge, bodyMedium=$bodyMedium, bodySmall=$bodySmall, " +
+            "labelLarge=$labelLarge, labelMedium=$labelMedium, labelSmall=$labelSmall)"
+    }
+}
+
+/**
+ * Helper function for component typography tokens.
+ */
+internal fun Typography.fromToken(value: TypographyKeyTokens): TextStyle {
+    return when (value) {
+        TypographyKeyTokens.DisplayLarge -> displayLarge
+        TypographyKeyTokens.DisplayMedium -> displayMedium
+        TypographyKeyTokens.DisplaySmall -> displaySmall
+        TypographyKeyTokens.HeadlineLarge -> headlineLarge
+        TypographyKeyTokens.HeadlineMedium -> headlineMedium
+        TypographyKeyTokens.HeadlineSmall -> headlineSmall
+        TypographyKeyTokens.TitleLarge -> titleLarge
+        TypographyKeyTokens.TitleMedium -> titleMedium
+        TypographyKeyTokens.TitleSmall -> titleSmall
+        TypographyKeyTokens.BodyLarge -> bodyLarge
+        TypographyKeyTokens.BodyMedium -> bodyMedium
+        TypographyKeyTokens.BodySmall -> bodySmall
+        TypographyKeyTokens.LabelLarge -> labelLarge
+        TypographyKeyTokens.LabelMedium -> labelMedium
+        TypographyKeyTokens.LabelSmall -> labelSmall
+    }
+}
+
+internal val LocalTypography = staticCompositionLocalOf { Typography() }
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorDarkTokens.kt b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorDarkTokens.kt
new file mode 100644
index 0000000..460f41e
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorDarkTokens.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_001 (Inspired by androidx.compose.material3.tokens v0_126)
+
+package androidx.tv.material3.tokens
+
+internal object ColorDarkTokens {
+    val Background = PaletteTokens.Neutral10
+    val Error = PaletteTokens.Error80
+    val ErrorContainer = PaletteTokens.Error30
+    val InverseOnSurface = PaletteTokens.Neutral20
+    val InversePrimary = PaletteTokens.Primary40
+    val InverseSurface = PaletteTokens.Neutral90
+    val OnBackground = PaletteTokens.Neutral90
+    val OnError = PaletteTokens.Error20
+    val OnErrorContainer = PaletteTokens.Error90
+    val OnPrimary = PaletteTokens.Primary20
+    val OnPrimaryContainer = PaletteTokens.Primary90
+    val OnSecondary = PaletteTokens.Secondary20
+    val OnSecondaryContainer = PaletteTokens.Secondary90
+    val OnSurface = PaletteTokens.Neutral90
+    val OnSurfaceVariant = PaletteTokens.NeutralVariant80
+    val OnTertiary = PaletteTokens.Tertiary20
+    val OnTertiaryContainer = PaletteTokens.Tertiary90
+    val Outline = PaletteTokens.NeutralVariant60
+    val OutlineVariant = PaletteTokens.NeutralVariant30
+    val Primary = PaletteTokens.Primary80
+    val PrimaryContainer = PaletteTokens.Primary30
+    val Scrim = PaletteTokens.Neutral0
+    val Secondary = PaletteTokens.Secondary80
+    val SecondaryContainer = PaletteTokens.Secondary30
+    val Surface = PaletteTokens.Neutral10
+    val SurfaceTint = Primary
+    val SurfaceVariant = PaletteTokens.NeutralVariant30
+    val Tertiary = PaletteTokens.Tertiary80
+    val TertiaryContainer = PaletteTokens.Tertiary30
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorLightTokens.kt b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorLightTokens.kt
new file mode 100644
index 0000000..261c8d8
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorLightTokens.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_001 (Inspired by androidx.compose.material3.tokens v0_103)
+
+package androidx.tv.material3.tokens
+
+internal object ColorLightTokens {
+    val Background = PaletteTokens.Neutral99
+    val Error = PaletteTokens.Error40
+    val ErrorContainer = PaletteTokens.Error90
+    val InverseOnSurface = PaletteTokens.Neutral95
+    val InversePrimary = PaletteTokens.Primary80
+    val InverseSurface = PaletteTokens.Neutral20
+    val OnBackground = PaletteTokens.Neutral10
+    val OnError = PaletteTokens.Error100
+    val OnErrorContainer = PaletteTokens.Error10
+    val OnPrimary = PaletteTokens.Primary100
+    val OnPrimaryContainer = PaletteTokens.Primary10
+    val OnSecondary = PaletteTokens.Secondary100
+    val OnSecondaryContainer = PaletteTokens.Secondary10
+    val OnSurface = PaletteTokens.Neutral10
+    val OnSurfaceVariant = PaletteTokens.NeutralVariant30
+    val OnTertiary = PaletteTokens.Tertiary100
+    val OnTertiaryContainer = PaletteTokens.Tertiary10
+    val Outline = PaletteTokens.NeutralVariant50
+    val OutlineVariant = PaletteTokens.NeutralVariant80
+    val Primary = PaletteTokens.Primary40
+    val PrimaryContainer = PaletteTokens.Primary90
+    val Scrim = PaletteTokens.Neutral0
+    val Secondary = PaletteTokens.Secondary40
+    val SecondaryContainer = PaletteTokens.Secondary90
+    val Surface = PaletteTokens.Neutral99
+    val SurfaceTint = Primary
+    val SurfaceVariant = PaletteTokens.NeutralVariant90
+    val Tertiary = PaletteTokens.Tertiary40
+    val TertiaryContainer = PaletteTokens.Tertiary90
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorSchemeKeyTokens.kt b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorSchemeKeyTokens.kt
new file mode 100644
index 0000000..ccc622a
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorSchemeKeyTokens.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_001 (Inspired by androidx.compose.material3.tokens v0_103)
+
+package androidx.tv.material3.tokens
+
+internal enum class ColorSchemeKeyTokens {
+    Background,
+    Error,
+    ErrorContainer,
+    InverseOnSurface,
+    InversePrimary,
+    InverseSurface,
+    OnBackground,
+    OnError,
+    OnErrorContainer,
+    OnPrimary,
+    OnPrimaryContainer,
+    OnSecondary,
+    OnSecondaryContainer,
+    OnSurface,
+    OnSurfaceVariant,
+    OnTertiary,
+    OnTertiaryContainer,
+    Outline,
+    OutlineVariant,
+    Primary,
+    PrimaryContainer,
+    Scrim,
+    Secondary,
+    SecondaryContainer,
+    Surface,
+    SurfaceTint,
+    SurfaceVariant,
+    Tertiary,
+    TertiaryContainer,
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/Elevation.kt b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/Elevation.kt
new file mode 100644
index 0000000..fef575f
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/Elevation.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_001 (Inspired by androidx.compose.material3.tokens v0_103)
+
+package androidx.tv.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object Elevation {
+    val Level0 = 0.0.dp
+    val Level1 = 1.0.dp
+    val Level2 = 3.0.dp
+    val Level3 = 6.0.dp
+    val Level4 = 8.0.dp
+    val Level5 = 12.0.dp
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/PaletteTokens.kt b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/PaletteTokens.kt
new file mode 100644
index 0000000..419b2bb
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/PaletteTokens.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_001 (Inspired by androidx.compose.material3.tokens v0_103)
+
+package androidx.tv.material3.tokens
+
+import androidx.compose.ui.graphics.Color
+
+internal object PaletteTokens {
+    val Black = Color(red = 0, green = 0, blue = 0)
+    val Error0 = Color(red = 0, green = 0, blue = 0)
+    val Error10 = Color(red = 65, green = 14, blue = 11)
+    val Error100 = Color(red = 255, green = 255, blue = 255)
+    val Error20 = Color(red = 96, green = 20, blue = 16)
+    val Error30 = Color(red = 140, green = 29, blue = 24)
+    val Error40 = Color(red = 179, green = 38, blue = 30)
+    val Error50 = Color(red = 220, green = 54, blue = 46)
+    val Error60 = Color(red = 228, green = 105, blue = 98)
+    val Error70 = Color(red = 236, green = 146, blue = 142)
+    val Error80 = Color(red = 242, green = 184, blue = 181)
+    val Error90 = Color(red = 249, green = 222, blue = 220)
+    val Error95 = Color(red = 252, green = 238, blue = 238)
+    val Error99 = Color(red = 255, green = 251, blue = 249)
+    val Neutral0 = Color(red = 0, green = 0, blue = 0)
+    val Neutral10 = Color(red = 28, green = 27, blue = 31)
+    val Neutral100 = Color(red = 255, green = 255, blue = 255)
+    val Neutral20 = Color(red = 49, green = 48, blue = 51)
+    val Neutral30 = Color(red = 72, green = 70, blue = 73)
+    val Neutral40 = Color(red = 96, green = 93, blue = 98)
+    val Neutral50 = Color(red = 120, green = 117, blue = 121)
+    val Neutral60 = Color(red = 147, green = 144, blue = 148)
+    val Neutral70 = Color(red = 174, green = 170, blue = 174)
+    val Neutral80 = Color(red = 201, green = 197, blue = 202)
+    val Neutral90 = Color(red = 230, green = 225, blue = 229)
+    val Neutral95 = Color(red = 244, green = 239, blue = 244)
+    val Neutral99 = Color(red = 255, green = 251, blue = 254)
+    val NeutralVariant0 = Color(red = 0, green = 0, blue = 0)
+    val NeutralVariant10 = Color(red = 29, green = 26, blue = 34)
+    val NeutralVariant100 = Color(red = 255, green = 255, blue = 255)
+    val NeutralVariant20 = Color(red = 50, green = 47, blue = 55)
+    val NeutralVariant30 = Color(red = 73, green = 69, blue = 79)
+    val NeutralVariant40 = Color(red = 96, green = 93, blue = 102)
+    val NeutralVariant50 = Color(red = 121, green = 116, blue = 126)
+    val NeutralVariant60 = Color(red = 147, green = 143, blue = 153)
+    val NeutralVariant70 = Color(red = 174, green = 169, blue = 180)
+    val NeutralVariant80 = Color(red = 202, green = 196, blue = 208)
+    val NeutralVariant90 = Color(red = 231, green = 224, blue = 236)
+    val NeutralVariant95 = Color(red = 245, green = 238, blue = 250)
+    val NeutralVariant99 = Color(red = 255, green = 251, blue = 254)
+    val Primary0 = Color(red = 0, green = 0, blue = 0)
+    val Primary10 = Color(red = 33, green = 0, blue = 93)
+    val Primary100 = Color(red = 255, green = 255, blue = 255)
+    val Primary20 = Color(red = 56, green = 30, blue = 114)
+    val Primary30 = Color(red = 79, green = 55, blue = 139)
+    val Primary40 = Color(red = 103, green = 80, blue = 164)
+    val Primary50 = Color(red = 127, green = 103, blue = 190)
+    val Primary60 = Color(red = 154, green = 130, blue = 219)
+    val Primary70 = Color(red = 182, green = 157, blue = 248)
+    val Primary80 = Color(red = 208, green = 188, blue = 255)
+    val Primary90 = Color(red = 234, green = 221, blue = 255)
+    val Primary95 = Color(red = 246, green = 237, blue = 255)
+    val Primary99 = Color(red = 255, green = 251, blue = 254)
+    val Secondary0 = Color(red = 0, green = 0, blue = 0)
+    val Secondary10 = Color(red = 29, green = 25, blue = 43)
+    val Secondary100 = Color(red = 255, green = 255, blue = 255)
+    val Secondary20 = Color(red = 51, green = 45, blue = 65)
+    val Secondary30 = Color(red = 74, green = 68, blue = 88)
+    val Secondary40 = Color(red = 98, green = 91, blue = 113)
+    val Secondary50 = Color(red = 122, green = 114, blue = 137)
+    val Secondary60 = Color(red = 149, green = 141, blue = 165)
+    val Secondary70 = Color(red = 176, green = 167, blue = 192)
+    val Secondary80 = Color(red = 204, green = 194, blue = 220)
+    val Secondary90 = Color(red = 232, green = 222, blue = 248)
+    val Secondary95 = Color(red = 246, green = 237, blue = 255)
+    val Secondary99 = Color(red = 255, green = 251, blue = 254)
+    val Tertiary0 = Color(red = 0, green = 0, blue = 0)
+    val Tertiary10 = Color(red = 49, green = 17, blue = 29)
+    val Tertiary100 = Color(red = 255, green = 255, blue = 255)
+    val Tertiary20 = Color(red = 73, green = 37, blue = 50)
+    val Tertiary30 = Color(red = 99, green = 59, blue = 72)
+    val Tertiary40 = Color(red = 125, green = 82, blue = 96)
+    val Tertiary50 = Color(red = 152, green = 105, blue = 119)
+    val Tertiary60 = Color(red = 181, green = 131, blue = 146)
+    val Tertiary70 = Color(red = 210, green = 157, blue = 172)
+    val Tertiary80 = Color(red = 239, green = 184, blue = 200)
+    val Tertiary90 = Color(red = 255, green = 216, blue = 228)
+    val Tertiary95 = Color(red = 255, green = 236, blue = 241)
+    val Tertiary99 = Color(red = 255, green = 251, blue = 250)
+    val White = Color(red = 255, green = 255, blue = 255)
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ShapeKeyTokens.kt b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ShapeKeyTokens.kt
new file mode 100644
index 0000000..43ef4de
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ShapeKeyTokens.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_001 (Inspired by androidx.compose.material3.tokens v0_103)
+
+package androidx.tv.material3.tokens
+
+internal enum class ShapeKeyTokens {
+    CornerExtraLarge,
+    CornerExtraLargeTop,
+    CornerExtraSmall,
+    CornerExtraSmallTop,
+    CornerFull,
+    CornerLarge,
+    CornerLargeEnd,
+    CornerLargeTop,
+    CornerMedium,
+    CornerNone,
+    CornerSmall,
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ShapeTokens.kt b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ShapeTokens.kt
new file mode 100644
index 0000000..c027d0b
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ShapeTokens.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_001 (Inspired by androidx.compose.material3.tokens v0_103)
+
+package androidx.tv.material3.tokens
+
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.unit.dp
+
+internal object ShapeTokens {
+    val CornerExtraLarge = RoundedCornerShape(28.0.dp)
+    val CornerExtraLargeTop =
+        RoundedCornerShape(
+            topStart = 28.0.dp,
+            topEnd = 28.0.dp,
+            bottomEnd = 0.0.dp,
+            bottomStart = 0.0.dp
+        )
+    val CornerExtraSmall = RoundedCornerShape(4.0.dp)
+    val CornerExtraSmallTop = RoundedCornerShape(
+        topStart = 4.0.dp,
+        topEnd = 4.0.dp,
+        bottomEnd = 0.0.dp,
+        bottomStart = 0.0.dp
+    )
+    val CornerFull = CircleShape
+    val CornerLarge = RoundedCornerShape(16.0.dp)
+    val CornerLargeEnd =
+        RoundedCornerShape(
+            topStart = 0.0.dp,
+            topEnd = 16.0.dp,
+            bottomEnd = 16.0.dp,
+            bottomStart = 0.0.dp
+        )
+    val CornerLargeTop =
+        RoundedCornerShape(
+            topStart = 16.0.dp,
+            topEnd = 16.0.dp,
+            bottomEnd = 0.0.dp,
+            bottomStart = 0.0.dp
+        )
+    val CornerMedium = RoundedCornerShape(12.0.dp)
+    val CornerNone = RectangleShape
+    val CornerSmall = RoundedCornerShape(8.0.dp)
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypeScaleTokens.kt b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypeScaleTokens.kt
new file mode 100644
index 0000000..7091c76
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypeScaleTokens.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_001 (Inspired by androidx.compose.material3.tokens v0_103)
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.tv.material3.tokens
+
+import androidx.compose.ui.unit.sp
+
+internal object TypeScaleTokens {
+    val BodyLargeFont = TypefaceTokens.Plain
+    val BodyLargeLineHeight = 24.0.sp
+    val BodyLargeSize = 16.sp
+    val BodyLargeTracking = 0.5.sp
+    val BodyLargeWeight = TypefaceTokens.WeightRegular
+    val BodyMediumFont = TypefaceTokens.Plain
+    val BodyMediumLineHeight = 20.0.sp
+    val BodyMediumSize = 14.sp
+    val BodyMediumTracking = 0.2.sp
+    val BodyMediumWeight = TypefaceTokens.WeightRegular
+    val BodySmallFont = TypefaceTokens.Plain
+    val BodySmallLineHeight = 16.0.sp
+    val BodySmallSize = 12.sp
+    val BodySmallTracking = 0.4.sp
+    val BodySmallWeight = TypefaceTokens.WeightRegular
+    val DisplayLargeFont = TypefaceTokens.Brand
+    val DisplayLargeLineHeight = 64.0.sp
+    val DisplayLargeSize = 57.sp
+    val DisplayLargeTracking = -0.2.sp
+    val DisplayLargeWeight = TypefaceTokens.WeightRegular
+    val DisplayMediumFont = TypefaceTokens.Brand
+    val DisplayMediumLineHeight = 52.0.sp
+    val DisplayMediumSize = 45.sp
+    val DisplayMediumTracking = 0.0.sp
+    val DisplayMediumWeight = TypefaceTokens.WeightRegular
+    val DisplaySmallFont = TypefaceTokens.Brand
+    val DisplaySmallLineHeight = 44.0.sp
+    val DisplaySmallSize = 36.sp
+    val DisplaySmallTracking = 0.0.sp
+    val DisplaySmallWeight = TypefaceTokens.WeightRegular
+    val HeadlineLargeFont = TypefaceTokens.Brand
+    val HeadlineLargeLineHeight = 40.0.sp
+    val HeadlineLargeSize = 32.sp
+    val HeadlineLargeTracking = 0.0.sp
+    val HeadlineLargeWeight = TypefaceTokens.WeightRegular
+    val HeadlineMediumFont = TypefaceTokens.Brand
+    val HeadlineMediumLineHeight = 36.0.sp
+    val HeadlineMediumSize = 28.sp
+    val HeadlineMediumTracking = 0.0.sp
+    val HeadlineMediumWeight = TypefaceTokens.WeightRegular
+    val HeadlineSmallFont = TypefaceTokens.Brand
+    val HeadlineSmallLineHeight = 32.0.sp
+    val HeadlineSmallSize = 24.sp
+    val HeadlineSmallTracking = 0.0.sp
+    val HeadlineSmallWeight = TypefaceTokens.WeightRegular
+    val LabelLargeFont = TypefaceTokens.Plain
+    val LabelLargeLineHeight = 20.0.sp
+    val LabelLargeSize = 14.sp
+    val LabelLargeTracking = 0.1.sp
+    val LabelLargeWeight = TypefaceTokens.WeightMedium
+    val LabelMediumFont = TypefaceTokens.Plain
+    val LabelMediumLineHeight = 16.0.sp
+    val LabelMediumSize = 12.sp
+    val LabelMediumTracking = 0.5.sp
+    val LabelMediumWeight = TypefaceTokens.WeightMedium
+    val LabelSmallFont = TypefaceTokens.Plain
+    val LabelSmallLineHeight = 16.0.sp
+    val LabelSmallSize = 11.sp
+    val LabelSmallTracking = 0.5.sp
+    val LabelSmallWeight = TypefaceTokens.WeightMedium
+    val TitleLargeFont = TypefaceTokens.Brand
+    val TitleLargeLineHeight = 28.0.sp
+    val TitleLargeSize = 22.sp
+    val TitleLargeTracking = 0.0.sp
+    val TitleLargeWeight = TypefaceTokens.WeightRegular
+    val TitleMediumFont = TypefaceTokens.Plain
+    val TitleMediumLineHeight = 24.0.sp
+    val TitleMediumSize = 16.sp
+    val TitleMediumTracking = 0.2.sp
+    val TitleMediumWeight = TypefaceTokens.WeightMedium
+    val TitleSmallFont = TypefaceTokens.Plain
+    val TitleSmallLineHeight = 20.0.sp
+    val TitleSmallSize = 14.sp
+    val TitleSmallTracking = 0.1.sp
+    val TitleSmallWeight = TypefaceTokens.WeightMedium
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypefaceTokens.kt b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypefaceTokens.kt
new file mode 100644
index 0000000..7d0084f
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypefaceTokens.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_001 (Inspired by androidx.compose.material3.tokens v0_103)
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.tv.material3.tokens
+
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+
+internal object TypefaceTokens {
+    val Brand = FontFamily.SansSerif
+    val Plain = FontFamily.SansSerif
+    val WeightBold = FontWeight.Bold
+    val WeightMedium = FontWeight.Medium
+    val WeightRegular = FontWeight.Normal
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypographyKeyTokens.kt b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypographyKeyTokens.kt
new file mode 100644
index 0000000..a913a5a
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypographyKeyTokens.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_001 (Inspired by androidx.compose.material3.tokens v0_103)
+
+package androidx.tv.material3.tokens
+
+internal enum class TypographyKeyTokens {
+    BodyLarge,
+    BodyMedium,
+    BodySmall,
+    DisplayLarge,
+    DisplayMedium,
+    DisplaySmall,
+    HeadlineLarge,
+    HeadlineMedium,
+    HeadlineSmall,
+    LabelLarge,
+    LabelMedium,
+    LabelSmall,
+    TitleLarge,
+    TitleMedium,
+    TitleSmall,
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypographyTokens.kt b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypographyTokens.kt
new file mode 100644
index 0000000..c7b5519
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypographyTokens.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_001 (Inspired by androidx.compose.material3.tokens v0_103)
+
+package androidx.tv.material3.tokens
+
+import androidx.compose.ui.text.TextStyle
+
+internal object TypographyTokens {
+    val BodyLarge =
+        TextStyle(
+            fontFamily = TypeScaleTokens.BodyLargeFont,
+            fontWeight = TypeScaleTokens.BodyLargeWeight,
+            fontSize = TypeScaleTokens.BodyLargeSize,
+            lineHeight = TypeScaleTokens.BodyLargeLineHeight,
+            letterSpacing = TypeScaleTokens.BodyLargeTracking
+        )
+    val BodyMedium =
+        TextStyle(
+            fontFamily = TypeScaleTokens.BodyMediumFont,
+            fontWeight = TypeScaleTokens.BodyMediumWeight,
+            fontSize = TypeScaleTokens.BodyMediumSize,
+            lineHeight = TypeScaleTokens.BodyMediumLineHeight,
+            letterSpacing = TypeScaleTokens.BodyMediumTracking
+        )
+    val BodySmall =
+        TextStyle(
+            fontFamily = TypeScaleTokens.BodySmallFont,
+            fontWeight = TypeScaleTokens.BodySmallWeight,
+            fontSize = TypeScaleTokens.BodySmallSize,
+            lineHeight = TypeScaleTokens.BodySmallLineHeight,
+            letterSpacing = TypeScaleTokens.BodySmallTracking
+        )
+    val DisplayLarge =
+        TextStyle(
+            fontFamily = TypeScaleTokens.DisplayLargeFont,
+            fontWeight = TypeScaleTokens.DisplayLargeWeight,
+            fontSize = TypeScaleTokens.DisplayLargeSize,
+            lineHeight = TypeScaleTokens.DisplayLargeLineHeight,
+            letterSpacing = TypeScaleTokens.DisplayLargeTracking
+        )
+    val DisplayMedium =
+        TextStyle(
+            fontFamily = TypeScaleTokens.DisplayMediumFont,
+            fontWeight = TypeScaleTokens.DisplayMediumWeight,
+            fontSize = TypeScaleTokens.DisplayMediumSize,
+            lineHeight = TypeScaleTokens.DisplayMediumLineHeight,
+            letterSpacing = TypeScaleTokens.DisplayMediumTracking
+        )
+    val DisplaySmall =
+        TextStyle(
+            fontFamily = TypeScaleTokens.DisplaySmallFont,
+            fontWeight = TypeScaleTokens.DisplaySmallWeight,
+            fontSize = TypeScaleTokens.DisplaySmallSize,
+            lineHeight = TypeScaleTokens.DisplaySmallLineHeight,
+            letterSpacing = TypeScaleTokens.DisplaySmallTracking
+        )
+    val HeadlineLarge =
+        TextStyle(
+            fontFamily = TypeScaleTokens.HeadlineLargeFont,
+            fontWeight = TypeScaleTokens.HeadlineLargeWeight,
+            fontSize = TypeScaleTokens.HeadlineLargeSize,
+            lineHeight = TypeScaleTokens.HeadlineLargeLineHeight,
+            letterSpacing = TypeScaleTokens.HeadlineLargeTracking
+        )
+    val HeadlineMedium =
+        TextStyle(
+            fontFamily = TypeScaleTokens.HeadlineMediumFont,
+            fontWeight = TypeScaleTokens.HeadlineMediumWeight,
+            fontSize = TypeScaleTokens.HeadlineMediumSize,
+            lineHeight = TypeScaleTokens.HeadlineMediumLineHeight,
+            letterSpacing = TypeScaleTokens.HeadlineMediumTracking
+        )
+    val HeadlineSmall =
+        TextStyle(
+            fontFamily = TypeScaleTokens.HeadlineSmallFont,
+            fontWeight = TypeScaleTokens.HeadlineSmallWeight,
+            fontSize = TypeScaleTokens.HeadlineSmallSize,
+            lineHeight = TypeScaleTokens.HeadlineSmallLineHeight,
+            letterSpacing = TypeScaleTokens.HeadlineSmallTracking
+        )
+    val LabelLarge =
+        TextStyle(
+            fontFamily = TypeScaleTokens.LabelLargeFont,
+            fontWeight = TypeScaleTokens.LabelLargeWeight,
+            fontSize = TypeScaleTokens.LabelLargeSize,
+            lineHeight = TypeScaleTokens.LabelLargeLineHeight,
+            letterSpacing = TypeScaleTokens.LabelLargeTracking
+        )
+    val LabelMedium =
+        TextStyle(
+            fontFamily = TypeScaleTokens.LabelMediumFont,
+            fontWeight = TypeScaleTokens.LabelMediumWeight,
+            fontSize = TypeScaleTokens.LabelMediumSize,
+            lineHeight = TypeScaleTokens.LabelMediumLineHeight,
+            letterSpacing = TypeScaleTokens.LabelMediumTracking
+        )
+    val LabelSmall =
+        TextStyle(
+            fontFamily = TypeScaleTokens.LabelSmallFont,
+            fontWeight = TypeScaleTokens.LabelSmallWeight,
+            fontSize = TypeScaleTokens.LabelSmallSize,
+            lineHeight = TypeScaleTokens.LabelSmallLineHeight,
+            letterSpacing = TypeScaleTokens.LabelSmallTracking
+        )
+    val TitleLarge =
+        TextStyle(
+            fontFamily = TypeScaleTokens.TitleLargeFont,
+            fontWeight = TypeScaleTokens.TitleLargeWeight,
+            fontSize = TypeScaleTokens.TitleLargeSize,
+            lineHeight = TypeScaleTokens.TitleLargeLineHeight,
+            letterSpacing = TypeScaleTokens.TitleLargeTracking
+        )
+    val TitleMedium =
+        TextStyle(
+            fontFamily = TypeScaleTokens.TitleMediumFont,
+            fontWeight = TypeScaleTokens.TitleMediumWeight,
+            fontSize = TypeScaleTokens.TitleMediumSize,
+            lineHeight = TypeScaleTokens.TitleMediumLineHeight,
+            letterSpacing = TypeScaleTokens.TitleMediumTracking
+        )
+    val TitleSmall =
+        TextStyle(
+            fontFamily = TypeScaleTokens.TitleSmallFont,
+            fontWeight = TypeScaleTokens.TitleSmallWeight,
+            fontSize = TypeScaleTokens.TitleSmallSize,
+            lineHeight = TypeScaleTokens.TitleSmallLineHeight,
+            letterSpacing = TypeScaleTokens.TitleSmallTracking
+        )
+}
diff --git a/viewpager2/OWNERS b/viewpager2/OWNERS
index 6ee1c5c..859a7b9 100644
--- a/viewpager2/OWNERS
+++ b/viewpager2/OWNERS
@@ -1,2 +1,2 @@
-# Bug component: 561920
+# Bug component: 607924
 jgielzak@google.com
\ No newline at end of file
diff --git a/viewpager2/integration-tests/testapp/src/androidTest/java/androidx/viewpager2/integration/testapp/test/FakeDragTest.kt b/viewpager2/integration-tests/testapp/src/androidTest/java/androidx/viewpager2/integration/testapp/test/FakeDragTest.kt
index 7bae47b..95aee13 100644
--- a/viewpager2/integration-tests/testapp/src/androidTest/java/androidx/viewpager2/integration/testapp/test/FakeDragTest.kt
+++ b/viewpager2/integration-tests/testapp/src/androidTest/java/androidx/viewpager2/integration/testapp/test/FakeDragTest.kt
@@ -44,6 +44,7 @@
 import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_DRAGGING
 import org.hamcrest.CoreMatchers.equalTo
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -94,6 +95,7 @@
         selectOrientation(config.orientation)
     }
 
+    @Ignore // b/266477436
     @Test
     fun testFakeDragging() {
         // test if ViewPager2 goes to the next page when fake dragging
diff --git a/viewpager2/integration-tests/testapp/src/androidTest/java/androidx/viewpager2/integration/testapp/test/MarginPageTransformerTest.kt b/viewpager2/integration-tests/testapp/src/androidTest/java/androidx/viewpager2/integration/testapp/test/MarginPageTransformerTest.kt
index 24d9b2c..db37dc6 100644
--- a/viewpager2/integration-tests/testapp/src/androidTest/java/androidx/viewpager2/integration/testapp/test/MarginPageTransformerTest.kt
+++ b/viewpager2/integration-tests/testapp/src/androidTest/java/androidx/viewpager2/integration/testapp/test/MarginPageTransformerTest.kt
@@ -55,6 +55,7 @@
         testMargin(null)
     }
 
+    @Ignore // b/266476890
     @Test
     fun testMargin_offscreenLimit_default() {
         testMargin(ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT)
diff --git a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/DragWhileSmoothScrollTest.kt b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/DragWhileSmoothScrollTest.kt
index 8273c04..1879ee12 100644
--- a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/DragWhileSmoothScrollTest.kt
+++ b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/DragWhileSmoothScrollTest.kt
@@ -35,6 +35,7 @@
 import org.hamcrest.MatcherAssert.assertThat
 import org.hamcrest.Matchers.greaterThan
 import org.junit.Assert.fail
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -67,6 +68,7 @@
 
     private lateinit var test: Context
 
+    @Ignore // b/266613081
     @Test
     fun test() {
         // given
diff --git a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/FragmentLifecycleTest.kt b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/FragmentLifecycleTest.kt
index 2492604..3e6ff55 100644
--- a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/FragmentLifecycleTest.kt
+++ b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/FragmentLifecycleTest.kt
@@ -24,6 +24,7 @@
 import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
 import org.hamcrest.CoreMatchers.equalTo
 import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import java.util.concurrent.CountDownLatch
@@ -83,6 +84,7 @@
         }
     }
 
+    @Ignore // b/266478005
     @Test
     fun test_setCurrentItem_offscreenPageLimit_default() {
         test_setCurrentItem_offscreenPageLimit(OFFSCREEN_PAGE_LIMIT_DEFAULT)
diff --git a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/FragmentTransactionCallbackTest.kt b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/FragmentTransactionCallbackTest.kt
index eca228f..5b49e68 100644
--- a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/FragmentTransactionCallbackTest.kt
+++ b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/FragmentTransactionCallbackTest.kt
@@ -31,6 +31,7 @@
 import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
 import org.hamcrest.MatcherAssert.assertThat
 import org.hamcrest.Matchers.equalTo
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import java.util.concurrent.CountDownLatch
@@ -224,6 +225,7 @@
         }
     }
 
+    @Ignore // b/266975014
     @Test
     fun test_fragmentSaveSateCallback() {
         setUpTest(ORIENTATION_HORIZONTAL).apply {
diff --git a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/HostFragmentBackStackTest.kt b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/HostFragmentBackStackTest.kt
index 8a57fd6..92bed54 100644
--- a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/HostFragmentBackStackTest.kt
+++ b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/HostFragmentBackStackTest.kt
@@ -51,6 +51,7 @@
 class HostFragmentBackStackTest : BaseTest() {
     @Test
     fun test_sameFragment_multipleBackStackEntries() {
+        @Suppress("DEPRECATION")
         FragmentManager.enableDebugLogging(true)
         val containerId = ViewCompat.generateViewId()
         setUpTest(ORIENTATION_HORIZONTAL).apply {
diff --git a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PageChangeCallbackTest.kt b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PageChangeCallbackTest.kt
index b18447e..355402c 100644
--- a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PageChangeCallbackTest.kt
+++ b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PageChangeCallbackTest.kt
@@ -46,6 +46,7 @@
 import org.hamcrest.Matchers.greaterThan
 import org.hamcrest.Matchers.greaterThanOrEqualTo
 import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -872,6 +873,7 @@
         )
     }
 
+    @Ignore // b/266613027
     @Test
     fun test_getScrollState() {
         val test = setUpTest(config.orientation)
@@ -1087,6 +1089,7 @@
         recorder.assertAllPagesSelected(testPages.flatMap { listOf(it, it + 1) })
     }
 
+    @Ignore // b/266613027
     @Test
     fun test_setCurrentItemWhileScrolling_maxIntItems() {
         val test = setUpTest(config.orientation)
diff --git a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/SetItemWhileScrollInProgressTest.kt b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/SetItemWhileScrollInProgressTest.kt
index 49debd1..fc0b05d1 100644
--- a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/SetItemWhileScrollInProgressTest.kt
+++ b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/SetItemWhileScrollInProgressTest.kt
@@ -32,6 +32,7 @@
 import org.hamcrest.CoreMatchers.not
 import org.hamcrest.CoreMatchers.nullValue
 import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -133,6 +134,7 @@
         fun spec(): List<TestConfig> = createTestSet()
     }
 
+    @Ignore // b/266974735
     @Test
     fun test() {
         config.apply {
diff --git a/wear/compose/compose-foundation/api/current.txt b/wear/compose/compose-foundation/api/current.txt
index 60ca06a..9f9bb95 100644
--- a/wear/compose/compose-foundation/api/current.txt
+++ b/wear/compose/compose-foundation/api/current.txt
@@ -182,3 +182,145 @@
 
 }
 
+package androidx.wear.compose.foundation.lazy {
+
+  @androidx.compose.runtime.Immutable public final class AutoCenteringParams {
+    ctor public AutoCenteringParams(optional int itemIndex, optional int itemOffset);
+  }
+
+  public final class ScalingLazyColumnDefaults {
+    method public androidx.wear.compose.foundation.lazy.ScalingParams scalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
+    method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior snapFlingBehavior(androidx.wear.compose.foundation.lazy.ScalingLazyListState state, optional float snapOffset, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decay);
+    field public static final androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults INSTANCE;
+  }
+
+  public final class ScalingLazyColumnKt {
+    method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.lazy.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.foundation.lazy.ScalingParams scalingParams, optional int anchorType, optional androidx.wear.compose.foundation.lazy.AutoCenteringParams? autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit> content);
+    method public static inline <T> void items(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.foundation.lazy.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void items(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.foundation.lazy.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.foundation.lazy.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.foundation.lazy.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+  }
+
+  public final class ScalingLazyColumnMeasureKt {
+  }
+
+  @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class ScalingLazyListAnchorType {
+    field public static final androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType.Companion Companion;
+  }
+
+  public static final class ScalingLazyListAnchorType.Companion {
+    method public int getItemCenter();
+    method public int getItemStart();
+    property public final int ItemCenter;
+    property public final int ItemStart;
+  }
+
+  public sealed interface ScalingLazyListItemInfo {
+    method public float getAlpha();
+    method public int getIndex();
+    method public Object getKey();
+    method public int getOffset();
+    method public float getScale();
+    method public int getSize();
+    method public int getUnadjustedOffset();
+    method public int getUnadjustedSize();
+    property public abstract float alpha;
+    property public abstract int index;
+    property public abstract Object key;
+    property public abstract int offset;
+    property public abstract float scale;
+    property public abstract int size;
+    property public abstract int unadjustedOffset;
+    property public abstract int unadjustedSize;
+  }
+
+  @androidx.compose.runtime.Stable @androidx.wear.compose.foundation.lazy.ScalingLazyScopeMarker public sealed interface ScalingLazyListItemScope {
+    method public androidx.compose.ui.Modifier fillParentMaxHeight(androidx.compose.ui.Modifier, optional float fraction);
+    method public androidx.compose.ui.Modifier fillParentMaxSize(androidx.compose.ui.Modifier, optional float fraction);
+    method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional float fraction);
+  }
+
+  public sealed interface ScalingLazyListLayoutInfo {
+    method public int getAfterAutoCenteringPadding();
+    method public int getAfterContentPadding();
+    method public int getAnchorType();
+    method public int getBeforeAutoCenteringPadding();
+    method public int getBeforeContentPadding();
+    method public androidx.compose.foundation.gestures.Orientation getOrientation();
+    method public boolean getReverseLayout();
+    method public int getTotalItemsCount();
+    method public int getViewportEndOffset();
+    method public long getViewportSize();
+    method public int getViewportStartOffset();
+    method public java.util.List<androidx.wear.compose.foundation.lazy.ScalingLazyListItemInfo> getVisibleItemsInfo();
+    property public abstract int afterAutoCenteringPadding;
+    property public abstract int afterContentPadding;
+    property public abstract int anchorType;
+    property public abstract int beforeAutoCenteringPadding;
+    property public abstract int beforeContentPadding;
+    property public abstract androidx.compose.foundation.gestures.Orientation orientation;
+    property public abstract boolean reverseLayout;
+    property public abstract int totalItemsCount;
+    property public abstract int viewportEndOffset;
+    property public abstract long viewportSize;
+    property public abstract int viewportStartOffset;
+    property public abstract java.util.List<androidx.wear.compose.foundation.lazy.ScalingLazyListItemInfo> visibleItemsInfo;
+  }
+
+  @androidx.wear.compose.foundation.lazy.ScalingLazyScopeMarker public sealed interface ScalingLazyListScope {
+    method public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListItemScope,kotlin.Unit> content);
+    method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.foundation.lazy.ScalingLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+  }
+
+  @androidx.compose.runtime.Stable public final class ScalingLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
+    ctor public ScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
+    method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public float dispatchRawDelta(float delta);
+    method public int getCenterItemIndex();
+    method public int getCenterItemScrollOffset();
+    method public androidx.wear.compose.foundation.lazy.ScalingLazyListLayoutInfo getLayoutInfo();
+    method public boolean isScrollInProgress();
+    method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
+    property public final int centerItemIndex;
+    property public final int centerItemScrollOffset;
+    property public boolean isScrollInProgress;
+    property public final androidx.wear.compose.foundation.lazy.ScalingLazyListLayoutInfo layoutInfo;
+    field public static final androidx.wear.compose.foundation.lazy.ScalingLazyListState.Companion Companion;
+  }
+
+  public static final class ScalingLazyListState.Companion {
+    method public androidx.compose.runtime.saveable.Saver<androidx.wear.compose.foundation.lazy.ScalingLazyListState,java.lang.Object> getSaver();
+    property public final androidx.compose.runtime.saveable.Saver<androidx.wear.compose.foundation.lazy.ScalingLazyListState,java.lang.Object> Saver;
+  }
+
+  public final class ScalingLazyListStateKt {
+    method @androidx.compose.runtime.Composable public static androidx.wear.compose.foundation.lazy.ScalingLazyListState rememberScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
+  }
+
+  @kotlin.DslMarker public @interface ScalingLazyScopeMarker {
+  }
+
+  @androidx.compose.runtime.Stable public interface ScalingParams {
+    method public float getEdgeAlpha();
+    method public float getEdgeScale();
+    method public float getMaxElementHeight();
+    method public float getMaxTransitionArea();
+    method public float getMinElementHeight();
+    method public float getMinTransitionArea();
+    method public androidx.compose.animation.core.Easing getScaleInterpolator();
+    method public int resolveViewportVerticalOffset(long viewportConstraints);
+    property public abstract float edgeAlpha;
+    property public abstract float edgeScale;
+    property public abstract float maxElementHeight;
+    property public abstract float maxTransitionArea;
+    property public abstract float minElementHeight;
+    property public abstract float minTransitionArea;
+    property public abstract androidx.compose.animation.core.Easing scaleInterpolator;
+  }
+
+}
+
diff --git a/wear/compose/compose-foundation/api/public_plus_experimental_current.txt b/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
index 60ca06a..9f9bb95 100644
--- a/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
@@ -182,3 +182,145 @@
 
 }
 
+package androidx.wear.compose.foundation.lazy {
+
+  @androidx.compose.runtime.Immutable public final class AutoCenteringParams {
+    ctor public AutoCenteringParams(optional int itemIndex, optional int itemOffset);
+  }
+
+  public final class ScalingLazyColumnDefaults {
+    method public androidx.wear.compose.foundation.lazy.ScalingParams scalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
+    method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior snapFlingBehavior(androidx.wear.compose.foundation.lazy.ScalingLazyListState state, optional float snapOffset, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decay);
+    field public static final androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults INSTANCE;
+  }
+
+  public final class ScalingLazyColumnKt {
+    method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.lazy.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.foundation.lazy.ScalingParams scalingParams, optional int anchorType, optional androidx.wear.compose.foundation.lazy.AutoCenteringParams? autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit> content);
+    method public static inline <T> void items(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.foundation.lazy.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void items(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.foundation.lazy.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.foundation.lazy.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.foundation.lazy.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+  }
+
+  public final class ScalingLazyColumnMeasureKt {
+  }
+
+  @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class ScalingLazyListAnchorType {
+    field public static final androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType.Companion Companion;
+  }
+
+  public static final class ScalingLazyListAnchorType.Companion {
+    method public int getItemCenter();
+    method public int getItemStart();
+    property public final int ItemCenter;
+    property public final int ItemStart;
+  }
+
+  public sealed interface ScalingLazyListItemInfo {
+    method public float getAlpha();
+    method public int getIndex();
+    method public Object getKey();
+    method public int getOffset();
+    method public float getScale();
+    method public int getSize();
+    method public int getUnadjustedOffset();
+    method public int getUnadjustedSize();
+    property public abstract float alpha;
+    property public abstract int index;
+    property public abstract Object key;
+    property public abstract int offset;
+    property public abstract float scale;
+    property public abstract int size;
+    property public abstract int unadjustedOffset;
+    property public abstract int unadjustedSize;
+  }
+
+  @androidx.compose.runtime.Stable @androidx.wear.compose.foundation.lazy.ScalingLazyScopeMarker public sealed interface ScalingLazyListItemScope {
+    method public androidx.compose.ui.Modifier fillParentMaxHeight(androidx.compose.ui.Modifier, optional float fraction);
+    method public androidx.compose.ui.Modifier fillParentMaxSize(androidx.compose.ui.Modifier, optional float fraction);
+    method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional float fraction);
+  }
+
+  public sealed interface ScalingLazyListLayoutInfo {
+    method public int getAfterAutoCenteringPadding();
+    method public int getAfterContentPadding();
+    method public int getAnchorType();
+    method public int getBeforeAutoCenteringPadding();
+    method public int getBeforeContentPadding();
+    method public androidx.compose.foundation.gestures.Orientation getOrientation();
+    method public boolean getReverseLayout();
+    method public int getTotalItemsCount();
+    method public int getViewportEndOffset();
+    method public long getViewportSize();
+    method public int getViewportStartOffset();
+    method public java.util.List<androidx.wear.compose.foundation.lazy.ScalingLazyListItemInfo> getVisibleItemsInfo();
+    property public abstract int afterAutoCenteringPadding;
+    property public abstract int afterContentPadding;
+    property public abstract int anchorType;
+    property public abstract int beforeAutoCenteringPadding;
+    property public abstract int beforeContentPadding;
+    property public abstract androidx.compose.foundation.gestures.Orientation orientation;
+    property public abstract boolean reverseLayout;
+    property public abstract int totalItemsCount;
+    property public abstract int viewportEndOffset;
+    property public abstract long viewportSize;
+    property public abstract int viewportStartOffset;
+    property public abstract java.util.List<androidx.wear.compose.foundation.lazy.ScalingLazyListItemInfo> visibleItemsInfo;
+  }
+
+  @androidx.wear.compose.foundation.lazy.ScalingLazyScopeMarker public sealed interface ScalingLazyListScope {
+    method public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListItemScope,kotlin.Unit> content);
+    method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.foundation.lazy.ScalingLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+  }
+
+  @androidx.compose.runtime.Stable public final class ScalingLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
+    ctor public ScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
+    method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public float dispatchRawDelta(float delta);
+    method public int getCenterItemIndex();
+    method public int getCenterItemScrollOffset();
+    method public androidx.wear.compose.foundation.lazy.ScalingLazyListLayoutInfo getLayoutInfo();
+    method public boolean isScrollInProgress();
+    method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
+    property public final int centerItemIndex;
+    property public final int centerItemScrollOffset;
+    property public boolean isScrollInProgress;
+    property public final androidx.wear.compose.foundation.lazy.ScalingLazyListLayoutInfo layoutInfo;
+    field public static final androidx.wear.compose.foundation.lazy.ScalingLazyListState.Companion Companion;
+  }
+
+  public static final class ScalingLazyListState.Companion {
+    method public androidx.compose.runtime.saveable.Saver<androidx.wear.compose.foundation.lazy.ScalingLazyListState,java.lang.Object> getSaver();
+    property public final androidx.compose.runtime.saveable.Saver<androidx.wear.compose.foundation.lazy.ScalingLazyListState,java.lang.Object> Saver;
+  }
+
+  public final class ScalingLazyListStateKt {
+    method @androidx.compose.runtime.Composable public static androidx.wear.compose.foundation.lazy.ScalingLazyListState rememberScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
+  }
+
+  @kotlin.DslMarker public @interface ScalingLazyScopeMarker {
+  }
+
+  @androidx.compose.runtime.Stable public interface ScalingParams {
+    method public float getEdgeAlpha();
+    method public float getEdgeScale();
+    method public float getMaxElementHeight();
+    method public float getMaxTransitionArea();
+    method public float getMinElementHeight();
+    method public float getMinTransitionArea();
+    method public androidx.compose.animation.core.Easing getScaleInterpolator();
+    method public int resolveViewportVerticalOffset(long viewportConstraints);
+    property public abstract float edgeAlpha;
+    property public abstract float edgeScale;
+    property public abstract float maxElementHeight;
+    property public abstract float maxTransitionArea;
+    property public abstract float minElementHeight;
+    property public abstract float minTransitionArea;
+    property public abstract androidx.compose.animation.core.Easing scaleInterpolator;
+  }
+
+}
+
diff --git a/wear/compose/compose-foundation/api/restricted_current.txt b/wear/compose/compose-foundation/api/restricted_current.txt
index 60ca06a..9f9bb95 100644
--- a/wear/compose/compose-foundation/api/restricted_current.txt
+++ b/wear/compose/compose-foundation/api/restricted_current.txt
@@ -182,3 +182,145 @@
 
 }
 
+package androidx.wear.compose.foundation.lazy {
+
+  @androidx.compose.runtime.Immutable public final class AutoCenteringParams {
+    ctor public AutoCenteringParams(optional int itemIndex, optional int itemOffset);
+  }
+
+  public final class ScalingLazyColumnDefaults {
+    method public androidx.wear.compose.foundation.lazy.ScalingParams scalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
+    method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior snapFlingBehavior(androidx.wear.compose.foundation.lazy.ScalingLazyListState state, optional float snapOffset, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decay);
+    field public static final androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults INSTANCE;
+  }
+
+  public final class ScalingLazyColumnKt {
+    method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.lazy.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.foundation.lazy.ScalingParams scalingParams, optional int anchorType, optional androidx.wear.compose.foundation.lazy.AutoCenteringParams? autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit> content);
+    method public static inline <T> void items(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.foundation.lazy.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void items(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.foundation.lazy.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.foundation.lazy.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.foundation.lazy.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+  }
+
+  public final class ScalingLazyColumnMeasureKt {
+  }
+
+  @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class ScalingLazyListAnchorType {
+    field public static final androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType.Companion Companion;
+  }
+
+  public static final class ScalingLazyListAnchorType.Companion {
+    method public int getItemCenter();
+    method public int getItemStart();
+    property public final int ItemCenter;
+    property public final int ItemStart;
+  }
+
+  public sealed interface ScalingLazyListItemInfo {
+    method public float getAlpha();
+    method public int getIndex();
+    method public Object getKey();
+    method public int getOffset();
+    method public float getScale();
+    method public int getSize();
+    method public int getUnadjustedOffset();
+    method public int getUnadjustedSize();
+    property public abstract float alpha;
+    property public abstract int index;
+    property public abstract Object key;
+    property public abstract int offset;
+    property public abstract float scale;
+    property public abstract int size;
+    property public abstract int unadjustedOffset;
+    property public abstract int unadjustedSize;
+  }
+
+  @androidx.compose.runtime.Stable @androidx.wear.compose.foundation.lazy.ScalingLazyScopeMarker public sealed interface ScalingLazyListItemScope {
+    method public androidx.compose.ui.Modifier fillParentMaxHeight(androidx.compose.ui.Modifier, optional float fraction);
+    method public androidx.compose.ui.Modifier fillParentMaxSize(androidx.compose.ui.Modifier, optional float fraction);
+    method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional float fraction);
+  }
+
+  public sealed interface ScalingLazyListLayoutInfo {
+    method public int getAfterAutoCenteringPadding();
+    method public int getAfterContentPadding();
+    method public int getAnchorType();
+    method public int getBeforeAutoCenteringPadding();
+    method public int getBeforeContentPadding();
+    method public androidx.compose.foundation.gestures.Orientation getOrientation();
+    method public boolean getReverseLayout();
+    method public int getTotalItemsCount();
+    method public int getViewportEndOffset();
+    method public long getViewportSize();
+    method public int getViewportStartOffset();
+    method public java.util.List<androidx.wear.compose.foundation.lazy.ScalingLazyListItemInfo> getVisibleItemsInfo();
+    property public abstract int afterAutoCenteringPadding;
+    property public abstract int afterContentPadding;
+    property public abstract int anchorType;
+    property public abstract int beforeAutoCenteringPadding;
+    property public abstract int beforeContentPadding;
+    property public abstract androidx.compose.foundation.gestures.Orientation orientation;
+    property public abstract boolean reverseLayout;
+    property public abstract int totalItemsCount;
+    property public abstract int viewportEndOffset;
+    property public abstract long viewportSize;
+    property public abstract int viewportStartOffset;
+    property public abstract java.util.List<androidx.wear.compose.foundation.lazy.ScalingLazyListItemInfo> visibleItemsInfo;
+  }
+
+  @androidx.wear.compose.foundation.lazy.ScalingLazyScopeMarker public sealed interface ScalingLazyListScope {
+    method public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListItemScope,kotlin.Unit> content);
+    method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.foundation.lazy.ScalingLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+  }
+
+  @androidx.compose.runtime.Stable public final class ScalingLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
+    ctor public ScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
+    method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public float dispatchRawDelta(float delta);
+    method public int getCenterItemIndex();
+    method public int getCenterItemScrollOffset();
+    method public androidx.wear.compose.foundation.lazy.ScalingLazyListLayoutInfo getLayoutInfo();
+    method public boolean isScrollInProgress();
+    method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
+    property public final int centerItemIndex;
+    property public final int centerItemScrollOffset;
+    property public boolean isScrollInProgress;
+    property public final androidx.wear.compose.foundation.lazy.ScalingLazyListLayoutInfo layoutInfo;
+    field public static final androidx.wear.compose.foundation.lazy.ScalingLazyListState.Companion Companion;
+  }
+
+  public static final class ScalingLazyListState.Companion {
+    method public androidx.compose.runtime.saveable.Saver<androidx.wear.compose.foundation.lazy.ScalingLazyListState,java.lang.Object> getSaver();
+    property public final androidx.compose.runtime.saveable.Saver<androidx.wear.compose.foundation.lazy.ScalingLazyListState,java.lang.Object> Saver;
+  }
+
+  public final class ScalingLazyListStateKt {
+    method @androidx.compose.runtime.Composable public static androidx.wear.compose.foundation.lazy.ScalingLazyListState rememberScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
+  }
+
+  @kotlin.DslMarker public @interface ScalingLazyScopeMarker {
+  }
+
+  @androidx.compose.runtime.Stable public interface ScalingParams {
+    method public float getEdgeAlpha();
+    method public float getEdgeScale();
+    method public float getMaxElementHeight();
+    method public float getMaxTransitionArea();
+    method public float getMinElementHeight();
+    method public float getMinTransitionArea();
+    method public androidx.compose.animation.core.Easing getScaleInterpolator();
+    method public int resolveViewportVerticalOffset(long viewportConstraints);
+    property public abstract float edgeAlpha;
+    property public abstract float edgeScale;
+    property public abstract float maxElementHeight;
+    property public abstract float maxTransitionArea;
+    property public abstract float minElementHeight;
+    property public abstract float minTransitionArea;
+    property public abstract androidx.compose.animation.core.Easing scaleInterpolator;
+  }
+
+}
+
diff --git a/wear/compose/compose-foundation/benchmark/benchmark-proguard-rules.pro b/wear/compose/compose-foundation/benchmark/benchmark-proguard-rules.pro
new file mode 100644
index 0000000..e4061d2
--- /dev/null
+++ b/wear/compose/compose-foundation/benchmark/benchmark-proguard-rules.pro
@@ -0,0 +1,37 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
+-dontobfuscate
+
+-ignorewarnings
+
+-keepattributes *Annotation*
+
+-dontnote junit.framework.**
+-dontnote junit.runner.**
+
+-dontwarn androidx.test.**
+-dontwarn org.junit.**
+-dontwarn org.hamcrest.**
+-dontwarn com.squareup.javawriter.JavaWriter
+
+-keepclasseswithmembers @org.junit.runner.RunWith public class *
\ No newline at end of file
diff --git a/wear/compose/compose-foundation/benchmark/build.gradle b/wear/compose/compose-foundation/benchmark/build.gradle
new file mode 100644
index 0000000..516348d
--- /dev/null
+++ b/wear/compose/compose-foundation/benchmark/build.gradle
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("AndroidXComposePlugin")
+    id("org.jetbrains.kotlin.android")
+    id("androidx.benchmark")
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 25
+    }
+    buildTypes.all {
+        consumerProguardFiles "benchmark-proguard-rules.pro"
+    }
+    namespace "androidx.wear.compose.foundation.benchmark"
+}
+
+dependencies {
+
+    androidTestImplementation project(":benchmark:benchmark-junit4")
+    androidTestImplementation project(":compose:runtime:runtime")
+    androidTestImplementation project(":compose:ui:ui-text:ui-text-benchmark")
+    androidTestImplementation project(":compose:foundation:foundation")
+    androidTestImplementation project(":compose:runtime:runtime")
+    androidTestImplementation project(":compose:benchmark-utils")
+    androidTestImplementation project(":wear:compose:compose-foundation")
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(libs.kotlinStdlib)
+    androidTestImplementation(libs.kotlinReflect)
+    androidTestImplementation(libs.kotlinTestCommon)
+    androidTestImplementation(libs.truth)
+}
+androidx {
+    type = LibraryType.INTERNAL_TEST_LIBRARY
+}
diff --git a/wear/compose/compose-foundation/benchmark/src/androidTest/AndroidManifest.xml b/wear/compose/compose-foundation/benchmark/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..39f7fc3
--- /dev/null
+++ b/wear/compose/compose-foundation/benchmark/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<manifest />
\ No newline at end of file
diff --git a/wear/compose/compose-foundation/benchmark/src/androidTest/java/androidx/wear/compose/foundation/benchmark/ScalingLazyColumnBenchmark.kt b/wear/compose/compose-foundation/benchmark/src/androidTest/java/androidx/wear/compose/foundation/benchmark/ScalingLazyColumnBenchmark.kt
new file mode 100644
index 0000000..c7fc419
--- /dev/null
+++ b/wear/compose/compose-foundation/benchmark/src/androidTest/java/androidx/wear/compose/foundation/benchmark/ScalingLazyColumnBenchmark.kt
@@ -0,0 +1,233 @@
+/*
+ * 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.wear.compose.foundation.benchmark
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.ComposeTestCase
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.assertNoPendingChanges
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkDrawPerf
+import androidx.compose.testutils.benchmark.benchmarkFirstCompose
+import androidx.compose.testutils.benchmark.benchmarkLayoutPerf
+import androidx.compose.testutils.benchmark.recomposeUntilNoChangesPending
+import androidx.compose.testutils.doFramesUntilNoChangesPending
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Benchmark for Wear Compose ScalingLazyColumn.
+ */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class ScalingLazyColumnBenchmark {
+
+    @get:Rule
+    val benchmarkRule = ComposeBenchmarkRule()
+
+    private val scalingLazyColumnCaseFactory = { ScalingLazyColumnTestCase() }
+
+    @Test
+    fun first_compose() {
+        benchmarkRule.benchmarkFirstCompose(scalingLazyColumnCaseFactory)
+    }
+
+    @Test
+    fun first_measure() {
+        benchmarkRule.benchmarkFirstScalingLazyColumnMeasure(scalingLazyColumnCaseFactory)
+    }
+
+    @Test
+    fun first_layout() {
+        benchmarkRule.benchmarkFirstScalingLazyColumnLayout(scalingLazyColumnCaseFactory)
+    }
+
+    @Test
+    fun first_draw() {
+        benchmarkRule.benchmarkFirstScalingLazyColumnDraw(scalingLazyColumnCaseFactory)
+    }
+
+    @Test
+    fun layout() {
+        benchmarkRule.benchmarkLayoutPerf(scalingLazyColumnCaseFactory)
+    }
+
+    @Test
+    fun draw() {
+        benchmarkRule.benchmarkDrawPerf(scalingLazyColumnCaseFactory)
+    }
+}
+
+internal class ScalingLazyColumnTestCase : LayeredComposeTestCase() {
+    private var itemSizeDp: Dp = 10.dp
+    private var defaultItemSpacingDp: Dp = 4.dp
+
+    @Composable
+    override fun MeasuredContent() {
+        ScalingLazyColumn(
+            state = rememberScalingLazyListState(),
+            modifier = Modifier.requiredSize(
+                itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+            ),
+        ) {
+            items(10) { it ->
+                Box(Modifier.requiredSize(itemSizeDp)) {
+                    BasicText(text = "Item $it",
+                        Modifier
+                            .background(Color.White)
+                            .padding(2.dp),
+                        TextStyle(
+                            color = Color.Black,
+                            fontSize = 16.sp,
+                        )
+                    )
+                }
+            }
+        }
+    }
+}
+
+// TODO (b/210654937): Should be able to get rid of this workaround in the future once able to call
+// LaunchedEffect directly on underlying LazyColumn rather than via a 2-stage initialization via
+// onGloballyPositioned().
+fun ComposeBenchmarkRule.benchmarkFirstScalingLazyColumnMeasure(
+    caseFactory: () -> LayeredComposeTestCase
+) {
+    runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) {
+        measureRepeated {
+            runWithTimingDisabled {
+                doFramesUntilNoChangesPending()
+                // Add the content to benchmark
+                getTestCase().addMeasuredContent()
+                recomposeUntilNoChangesPending()
+                requestLayout()
+            }
+
+            measure()
+            recomposeUntilNoChangesPending()
+
+            runWithTimingDisabled {
+                assertNoPendingChanges()
+                disposeContent()
+            }
+        }
+    }
+}
+
+// TODO (b/210654937): Should be able to get rid of this workaround in the future once able to call
+// LaunchedEffect directly on underlying LazyColumn rather than via a 2-stage initialization via
+// onGloballyPositioned().
+fun ComposeBenchmarkRule.benchmarkFirstScalingLazyColumnLayout(
+    caseFactory: () -> LayeredComposeTestCase
+) {
+    runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) {
+        measureRepeated {
+            runWithTimingDisabled {
+                doFramesUntilNoChangesPending()
+                // Add the content to benchmark
+                getTestCase().addMeasuredContent()
+                recomposeUntilNoChangesPending()
+                requestLayout()
+                measure()
+            }
+
+            layout()
+            recomposeUntilNoChangesPending()
+
+            runWithTimingDisabled {
+                assertNoPendingChanges()
+                disposeContent()
+            }
+        }
+    }
+}
+
+// TODO (b/210654937): Should be able to get rid of this workaround in the future once able to call
+// LaunchedEffect directly on underlying LazyColumn rather than via a 2-stage initialization via
+// onGloballyPositioned().
+fun ComposeBenchmarkRule.benchmarkFirstScalingLazyColumnDraw(
+    caseFactory: () -> LayeredComposeTestCase
+) {
+    runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) {
+        measureRepeated {
+            runWithTimingDisabled {
+                doFramesUntilNoChangesPending()
+                // Add the content to benchmark
+                getTestCase().addMeasuredContent()
+                recomposeUntilNoChangesPending()
+                requestLayout()
+                measure()
+                layout()
+                drawPrepare()
+            }
+
+            draw()
+            drawFinish()
+            recomposeUntilNoChangesPending()
+
+            runWithTimingDisabled {
+                assertNoPendingChanges()
+                disposeContent()
+            }
+        }
+    }
+}
+
+private class LayeredCaseAdapter(private val innerCase: LayeredComposeTestCase) : ComposeTestCase {
+
+    companion object {
+        fun of(caseFactory: () -> LayeredComposeTestCase): () -> LayeredCaseAdapter = {
+            LayeredCaseAdapter(caseFactory())
+        }
+    }
+
+    var isComposed by mutableStateOf(false)
+
+    @Composable
+    override fun Content() {
+        innerCase.ContentWrappers {
+            if (isComposed) {
+                innerCase.MeasuredContent()
+            }
+        }
+    }
+
+    fun addMeasuredContent() {
+        Assert.assertTrue(!isComposed)
+        isComposed = true
+    }
+}
\ No newline at end of file
diff --git a/wear/compose/compose-foundation/benchmark/src/main/AndroidManifest.xml b/wear/compose/compose-foundation/benchmark/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..490f800
--- /dev/null
+++ b/wear/compose/compose-foundation/benchmark/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <application/>
+</manifest>
\ No newline at end of file
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index a2ff8da..5a85f33 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -46,6 +46,7 @@
     androidTestImplementation(project(":compose:test-utils"))
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.kotlinTest)
+    androidTestImplementation(libs.truth)
 
     samples(project(":wear:compose:compose-foundation-samples"))
 }
diff --git a/wear/compose/compose-foundation/samples/build.gradle b/wear/compose/compose-foundation/samples/build.gradle
index 5cd0e39..e1bc9aa 100644
--- a/wear/compose/compose-foundation/samples/build.gradle
+++ b/wear/compose/compose-foundation/samples/build.gradle
@@ -34,6 +34,7 @@
     implementation(project(":compose:ui:ui"))
     implementation(project(":compose:ui:ui-text"))
     implementation(project(":wear:compose:compose-foundation"))
+    implementation(project(":wear:compose:compose-material"))
 }
 
 android {
diff --git a/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/ScalingLazyColumnSample.kt b/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/ScalingLazyColumnSample.kt
new file mode 100644
index 0000000..b6b06ce
--- /dev/null
+++ b/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/ScalingLazyColumnSample.kt
@@ -0,0 +1,146 @@
+/*
+ * 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.wear.compose.foundation.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults
+import androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
+import androidx.wear.compose.material.Chip
+import androidx.wear.compose.material.ChipDefaults
+import androidx.wear.compose.material.ListHeader
+import androidx.wear.compose.material.Text
+import kotlinx.coroutines.launch
+
+@Sampled
+@Composable
+fun SimpleScalingLazyColumn() {
+    ScalingLazyColumn(
+        modifier = Modifier.fillMaxWidth()
+    ) {
+        item {
+            ListHeader {
+                Text(text = "List Header")
+            }
+        }
+        items(20) {
+            Chip(
+                onClick = { },
+                label = { Text("List item $it") },
+                colors = ChipDefaults.secondaryChipColors()
+            )
+        }
+    }
+}
+
+@Sampled
+@Composable
+fun SimpleScalingLazyColumnWithSnap() {
+    val state = rememberScalingLazyListState()
+    ScalingLazyColumn(
+        modifier = Modifier.fillMaxWidth(),
+        state = state,
+        flingBehavior = ScalingLazyColumnDefaults.snapFlingBehavior(state = state)
+    ) {
+        item {
+            ListHeader {
+                Text(text = "List Header")
+            }
+        }
+        items(20) {
+            Chip(
+                onClick = { },
+                label = { Text("List item $it") },
+                colors = ChipDefaults.secondaryChipColors()
+            )
+        }
+    }
+}
+
+@Sampled
+@Composable
+fun ScalingLazyColumnEdgeAnchoredAndAnimatedScrollTo() {
+    val coroutineScope = rememberCoroutineScope()
+    val itemSpacing = 6.dp
+    // Line up the gap between the items on the center-line
+    val scrollOffset = with(LocalDensity.current) {
+        -(itemSpacing / 2).roundToPx()
+    }
+    val state = rememberScalingLazyListState(
+        initialCenterItemIndex = 1,
+        initialCenterItemScrollOffset = scrollOffset
+    )
+
+    ScalingLazyColumn(
+        modifier = Modifier.fillMaxWidth(),
+        anchorType = ScalingLazyListAnchorType.ItemStart,
+        verticalArrangement = Arrangement.spacedBy(itemSpacing),
+        state = state,
+        autoCentering = AutoCenteringParams(itemOffset = scrollOffset)
+    ) {
+        item {
+            ListHeader {
+                Text(text = "List Header")
+            }
+        }
+        items(20) {
+            Chip(
+                onClick = {
+                    coroutineScope.launch {
+                        // Add +1 to allow for the ListHeader
+                        state.animateScrollToItem(it + 1, scrollOffset)
+                    }
+                },
+                label = { Text("List item $it") },
+                colors = ChipDefaults.secondaryChipColors()
+            )
+        }
+    }
+}
+
+@Sampled
+@Composable
+fun SimpleScalingLazyColumnWithContentPadding() {
+    ScalingLazyColumn(
+        modifier = Modifier.fillMaxWidth(),
+        contentPadding = PaddingValues(top = 20.dp, bottom = 20.dp),
+        autoCentering = null
+    ) {
+        item {
+            ListHeader {
+                Text(text = "List Header")
+            }
+        }
+        items(20) {
+            Chip(
+                onClick = { },
+                label = { Text("List item $it") },
+                colors = ChipDefaults.secondaryChipColors()
+            )
+        }
+    }
+}
diff --git a/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/CurvedLayoutTest.kt b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/CurvedLayoutTest.kt
index 34fa6cf..02cf6cc 100644
--- a/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/CurvedLayoutTest.kt
+++ b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/CurvedLayoutTest.kt
@@ -414,8 +414,6 @@
     }
 }
 
-internal const val TEST_TAG = "test-item"
-
 fun checkAngle(expected: Float, actual: Float) {
     var d = abs(expected - actual)
     d = min(d, 360 - d)
diff --git a/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/FoundationTest.kt b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/FoundationTest.kt
new file mode 100644
index 0000000..4590238
--- /dev/null
+++ b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/FoundationTest.kt
@@ -0,0 +1,19 @@
+/*
+ * 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.wear.compose.foundation
+
+internal const val TEST_TAG = "test-item"
diff --git a/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnIndexedTest.kt b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnIndexedTest.kt
new file mode 100644
index 0000000..9246678
--- /dev/null
+++ b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnIndexedTest.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.wear.compose.foundation.lazy
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.dp
+import org.junit.Rule
+import org.junit.Test
+
+class ScalingLazyColumnIndexedTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun scalingLazyColumnShowsIndexedItems() {
+        lateinit var state: ScalingLazyListState
+        val items = (1..4).map { it.toString() }
+        val viewPortHeight = 100.dp
+        val itemHeight = 51.dp
+        val itemWidth = 50.dp
+        val gapBetweenItems = 2.dp
+
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(initialCenterItemIndex = 0)
+                    .also { state = it },
+                modifier = Modifier.height(viewPortHeight),
+                verticalArrangement = Arrangement.spacedBy(gapBetweenItems),
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(
+                    edgeScale = 1.0f,
+                    // Create some extra composables to check that extraPadding works.
+                    viewportVerticalOffsetResolver = { (it.maxHeight / 10f).toInt() }
+                )
+            ) {
+                itemsIndexed(items) { index, item ->
+                    Spacer(
+                        Modifier.height(itemHeight).width(itemWidth)
+                            .testTag("$index-$item")
+                    )
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        // Fully visible
+        rule.onNodeWithTag("0-1")
+            .assertIsDisplayed()
+
+        // Partially visible
+        rule.onNodeWithTag("1-2")
+            .assertIsDisplayed()
+
+        // Will have been composed but should not be visible
+        rule.onNodeWithTag("2-3")
+            .assertIsNotDisplayed()
+
+        // Should not have been composed
+        rule.onNodeWithTag("3-4")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun columnWithIndexesComposedWithCorrectIndexAndItem() {
+        lateinit var state: ScalingLazyListState
+        val items = (0..1).map { it.toString() }
+
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(initialCenterItemIndex = 0)
+                    .also { state = it },
+                modifier = Modifier.height(200.dp),
+                autoCentering = null,
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(edgeScale = 1.0f)
+            ) {
+                itemsIndexed(items) { index, item ->
+                    BasicText(
+                        "${index}x$item", Modifier.requiredHeight(100.dp)
+                    )
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.onNodeWithText("0x0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithText("1x1")
+            .assertTopPositionInRootIsEqualTo(104.dp)
+    }
+
+    @Test
+    fun columnWithIndexesComposedWithCorrectIndexAndItemWithAutoCentering() {
+        lateinit var state: ScalingLazyListState
+        val items = (0..1).map { it.toString() }
+        val viewPortHeight = 100.dp
+        val itemHeight = 50.dp
+        val gapBetweenItems = 2.dp
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(initialCenterItemIndex = 0)
+                    .also { state = it },
+                modifier = Modifier.height(viewPortHeight),
+                autoCentering = AutoCenteringParams(itemIndex = 0),
+                verticalArrangement = Arrangement.spacedBy(gapBetweenItems),
+                // No scaling as we are doing maths with expected item sizes
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(edgeScale = 1.0f)
+            ) {
+                itemsIndexed(items) { index, item ->
+                    BasicText(
+                        "${index}x$item", Modifier.requiredHeight(itemHeight)
+                    )
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        // Check that first item is in the center of the viewport
+        val firstItemStart = viewPortHeight / 2f - itemHeight / 2f
+        rule.onNodeWithText("0x0")
+            .assertTopPositionInRootIsEqualTo(firstItemStart)
+
+        // And that the second item is item height + gap between items below it
+        rule.onNodeWithText("1x1")
+            .assertTopPositionInRootIsEqualTo(firstItemStart + itemHeight + gapBetweenItems)
+    }
+}
diff --git a/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt
new file mode 100644
index 0000000..bee6208
--- /dev/null
+++ b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt
@@ -0,0 +1,1029 @@
+/*
+ * 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.wear.compose.foundation.lazy
+
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.WithTouchSlop
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeDown
+import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.test.swipeWithVelocity
+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.wear.compose.foundation.TEST_TAG
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
+import kotlinx.coroutines.runBlocking
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+// These tests are in addition to ScalingLazyListLayoutInfoTest which handles scroll events at an
+// absolute level and is designed to exercise scrolling through the UI directly.
+public class ScalingLazyColumnTest {
+    private val scalingLazyColumnTag = "scalingLazyColumnTag"
+    private val firstItemTag = "firstItemTag"
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private var itemSizePx: Int = 50
+    private var itemSizeDp: Dp = Dp.Infinity
+    private var defaultItemSpacingDp: Dp = 4.dp
+    private var defaultItemSpacingPx = Int.MAX_VALUE
+
+    @Before
+    fun before() {
+        with(rule.density) {
+            itemSizeDp = itemSizePx.toDp()
+            defaultItemSpacingPx = defaultItemSpacingDp.roundToPx()
+        }
+    }
+
+    @Test
+    fun initializationCorrectWithAutoCenteringAndNormalItemPaddingForZeroItemList() {
+        initializationCorrectWithAutoCenteringAndNormalItemPaddingForNItemList(0)
+    }
+
+    @Test
+    fun initializationCorrectWithAutoCenteringAndNormalItemPaddingForOneItemList() {
+        initializationCorrectWithAutoCenteringAndNormalItemPaddingForNItemList(1)
+    }
+
+    @Test
+    fun initializationCorrectWithAutoCenteringAndNormalItemPaddingForTwoItemList() {
+        initializationCorrectWithAutoCenteringAndNormalItemPaddingForNItemList(2)
+    }
+
+    @Test
+    fun initializationCorrectWithAutoCenteringAndNormalItemPaddingForThreeItemList() {
+        initializationCorrectWithAutoCenteringAndNormalItemPaddingForNItemList(3)
+    }
+
+    @Test
+    fun initializationCorrectWithAutoCenteringAndNormalItemPaddingForFourItemList() {
+        initializationCorrectWithAutoCenteringAndNormalItemPaddingForNItemList(4)
+    }
+
+    @Test
+    fun initializationCorrectWithAutoCenteringAndNormalItemPaddingForFiveItemList() {
+        initializationCorrectWithAutoCenteringAndNormalItemPaddingForNItemList(5)
+    }
+
+    private fun initializationCorrectWithAutoCenteringAndNormalItemPaddingForNItemList(
+        itemCount: Int
+    ) {
+        lateinit var state: ScalingLazyListState
+        val listSize = itemSizeDp * 3.5f
+        rule.setContent {
+            WithTouchSlop(0f) {
+                ScalingLazyColumn(
+                    state = rememberScalingLazyListState().also { state = it },
+                    modifier = Modifier.testTag(TEST_TAG).requiredSize(listSize),
+                ) {
+                    items(itemCount) {
+                        Box(Modifier.requiredSize(itemSizeDp))
+                    }
+                }
+            }
+        }
+
+        rule.waitUntil { state.initialized.value }
+    }
+
+    @Test
+    fun initializationCorrectWithAutoCenteringAndCustomInterItemPadding() {
+        lateinit var state: ScalingLazyListState
+        val listSize = itemSizeDp * 3.5f
+        rule.setContent {
+            WithTouchSlop(0f) {
+                ScalingLazyColumn(
+                    state = rememberScalingLazyListState().also { state = it },
+                    modifier = Modifier.testTag(TEST_TAG).requiredSize(listSize),
+                    scalingParams = ScalingLazyColumnDefaults.scalingParams(edgeScale = 1f),
+                    // We want to arrange for the autoCentering spacer to be of size zero, but
+                    // also for the gap between items to be needed.
+                    verticalArrangement = Arrangement.spacedBy(itemSizeDp * 0.25f * 0.75f),
+                ) {
+                    items(5) {
+                        Box(Modifier.requiredSize(itemSizeDp))
+                    }
+                }
+            }
+        }
+
+        rule.waitUntil { state.initialized.value }
+    }
+
+    @Test
+    fun initializationCorrectWithAutoCenteringAndLargeFirstItem() {
+        lateinit var state: ScalingLazyListState
+        val listSize = itemSizeDp * 3.5f
+        rule.setContent {
+            WithTouchSlop(0f) {
+                ScalingLazyColumn(
+                    state = rememberScalingLazyListState().also { state = it },
+                    modifier = Modifier.testTag(TEST_TAG).requiredSize(listSize),
+                    scalingParams = ScalingLazyColumnDefaults.scalingParams(edgeScale = 1f),
+                ) {
+                    item {
+                        Box(Modifier.requiredSize(itemSizeDp * 2))
+                    }
+                    items(5) {
+                        Box(Modifier.requiredSize(itemSizeDp))
+                    }
+                }
+            }
+        }
+
+        rule.waitUntil { state.initialized.value }
+    }
+
+    @Test
+    fun autoCenteringCorrectSizeWithCenterAnchor() {
+        lateinit var state: ScalingLazyListState
+        val listSize = itemSizeDp * 3.5f
+        rule.setContent {
+            WithTouchSlop(0f) {
+                ScalingLazyColumn(
+                    state = rememberScalingLazyListState().also { state = it },
+                    modifier = Modifier.testTag(TEST_TAG).requiredSize(listSize),
+                    anchorType = ScalingLazyListAnchorType.ItemCenter,
+                    autoCentering = AutoCenteringParams(),
+                    verticalArrangement = Arrangement.spacedBy(0.dp),
+                    scalingParams = ScalingLazyColumnDefaults.scalingParams(
+                        edgeScale = 0f,
+                        minTransitionArea = 0.5f,
+                        maxTransitionArea = 0.5f
+                    )
+                ) {
+                    items(5) {
+                        Box(Modifier.requiredSize(itemSizeDp))
+                    }
+                }
+            }
+        }
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.waitForIdle()
+
+        val listSizePx = with(rule.density) {
+            listSize.roundToPx()
+        }
+        rule.runOnIdle {
+            // Make sure that the edge items have been scaled
+            assertThat(state.layoutInfo.visibleItemsInfo.first().scale).isLessThan(1.0f)
+            // But that size of the Spacer is as expected - it should be half the viewport size
+            // rounded down minus half the size of the center item rounded down minus the full size
+            // of the 0th item
+            assertThat(state.lazyListState.layoutInfo.visibleItemsInfo.first().size)
+                .isEqualTo((listSizePx / 2) - (itemSizePx + itemSizePx / 2))
+        }
+        rule.onNodeWithTag(TEST_TAG).performTouchInput {
+            swipeUp()
+        }
+        rule.runOnIdle {
+            // Check that the last item has been scrolled into view
+            assertThat(state.lazyListState.layoutInfo.visibleItemsInfo.last().index)
+                .isEqualTo(state.lazyListState.layoutInfo.totalItemsCount - 1)
+            // And that size of the Spacer is as expected
+            assertThat(state.lazyListState.layoutInfo.visibleItemsInfo.last().size)
+                .isEqualTo(((listSizePx / 2f) - (itemSizePx / 2f)).roundToInt())
+        }
+    }
+
+    @Test
+    fun autoCenteringCorrectSizeWithStartAnchor() {
+        lateinit var state: ScalingLazyListState
+        val listSize = itemSizeDp * 3.5f
+        rule.setContent {
+            WithTouchSlop(0f) {
+                ScalingLazyColumn(
+                    state = rememberScalingLazyListState().also { state = it },
+                    modifier = Modifier.testTag(TEST_TAG).requiredSize(listSize),
+                    anchorType = ScalingLazyListAnchorType.ItemStart,
+                    autoCentering = AutoCenteringParams(),
+                    verticalArrangement = Arrangement.spacedBy(0.dp),
+                    scalingParams = ScalingLazyColumnDefaults.scalingParams(
+                        edgeScale = 0f,
+                        minTransitionArea = 0.5f,
+                        maxTransitionArea = 0.5f
+                    )
+                ) {
+                    items(5) {
+                        Box(Modifier.requiredSize(itemSizeDp))
+                    }
+                }
+            }
+        }
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.waitForIdle()
+
+        val listSizePx = with(rule.density) {
+            listSize.roundToPx()
+        }
+
+        rule.runOnIdle {
+            // Make sure that the edge items have been scaled
+            assertThat(state.layoutInfo.visibleItemsInfo.first().scale).isLessThan(1.0f)
+            // But that size of the Spacer is as expected, it should be half the viewport size
+            // rounded down minus the size of zeroth item in the list
+            assertThat(state.lazyListState.layoutInfo.visibleItemsInfo.first().size)
+                .isEqualTo((listSizePx / 2) - itemSizePx)
+        }
+        rule.onNodeWithTag(TEST_TAG).performTouchInput {
+            swipeUp(endY = top)
+        }
+        rule.runOnIdle {
+            // Check that the last item has been scrolled into view
+            assertThat(state.lazyListState.layoutInfo.visibleItemsInfo.last().index)
+                .isEqualTo(state.lazyListState.layoutInfo.totalItemsCount - 1)
+            // And that size of the Spacer is as expected
+            assertThat(state.lazyListState.layoutInfo.visibleItemsInfo.last().size)
+                .isEqualTo(((listSizePx / 2f) - itemSizePx).roundToInt())
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectAfterScrolling() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            WithTouchSlop(0f) {
+                ScalingLazyColumn(
+                    state = rememberScalingLazyListState().also { state = it },
+                    modifier = Modifier.testTag(TEST_TAG).requiredSize(
+                        itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                    ),
+                    autoCentering = null
+                ) {
+                    items(5) {
+                        Box(Modifier.requiredSize(itemSizeDp))
+                    }
+                }
+            }
+        }
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.waitForIdle()
+
+        state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput {
+            swipeUp(endY = bottom - (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()))
+        }
+
+        rule.waitForIdle()
+        state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
+    }
+
+    @Test
+    fun visibleItemsAreCorrectAfterAttemptedScrollWithUserScrollDisabled() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            WithTouchSlop(0f) {
+                ScalingLazyColumn(
+                    state = rememberScalingLazyListState().also { state = it },
+                    modifier = Modifier.testTag(TEST_TAG).requiredSize(
+                        itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                    ),
+                    autoCentering = null,
+                    userScrollEnabled = false
+                ) {
+                    items(5) {
+                        Box(Modifier.requiredSize(itemSizeDp))
+                    }
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.waitForIdle()
+
+        state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput {
+            swipeUp(endY = bottom - (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()))
+        }
+
+        rule.waitForIdle()
+        state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
+    }
+
+    @Test
+    fun visibleItemsAreCorrectAfterScrollingWithAutoCentering() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            WithTouchSlop(0f) {
+                ScalingLazyColumn(
+                    state = rememberScalingLazyListState(initialCenterItemIndex = 0)
+                        .also { state = it },
+                    modifier = Modifier.testTag(TEST_TAG).requiredSize(
+                        itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                    ),
+                    autoCentering = AutoCenteringParams(itemIndex = 0)
+                ) {
+                    items(5) {
+                        Box(Modifier.requiredSize(itemSizeDp))
+                    }
+                }
+            }
+        }
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.waitForIdle()
+        state.layoutInfo.assertVisibleItems(count = 3, startIndex = 0)
+
+        rule.waitForIdle()
+        rule.onNodeWithTag(TEST_TAG).performTouchInput {
+            swipeUp(endY = bottom - (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()))
+        }
+
+        rule.waitForIdle()
+        state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
+    }
+
+    @Test
+    fun visibleItemsAreCorrectAfterScrollingWithSnap() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            WithTouchSlop(0f) {
+                ScalingLazyColumn(
+                    state = rememberScalingLazyListState(initialCenterItemIndex = 0)
+                        .also { state = it },
+                    modifier = Modifier.testTag(TEST_TAG).requiredSize(
+                        itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                    ),
+                    autoCentering = AutoCenteringParams(itemIndex = 0),
+                    flingBehavior = ScalingLazyColumnDefaults.snapFlingBehavior(state)
+                ) {
+                    items(5) {
+                        Box(Modifier.requiredSize(itemSizeDp))
+                    }
+                }
+            }
+        }
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.waitForIdle()
+
+        state.layoutInfo.assertVisibleItems(count = 3, startIndex = 0)
+
+        rule.waitForIdle()
+        rule.onNodeWithTag(TEST_TAG).performTouchInput {
+            // Swipe by an amount that is not a whole item + gap
+            swipeWithVelocity(
+                start = Offset(centerX, bottom),
+                end = Offset(centerX, bottom - (itemSizePx.toFloat())),
+                endVelocity = 1f, // Ensure it's not a fling.
+            )
+        }
+
+        rule.waitForIdle()
+        state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
+        assertThat(state.centerItemIndex).isEqualTo(1)
+        assertThat(state.centerItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
+    fun visibleItemsAreCorrectAfterScrollingWithSnapAndOffset() {
+        lateinit var state: ScalingLazyListState
+        val snapOffset = 5.dp
+        var snapOffsetPx = 0
+        rule.setContent {
+            WithTouchSlop(0f) {
+                snapOffsetPx = with(LocalDensity.current) { snapOffset.roundToPx() }
+
+                ScalingLazyColumn(
+                    state = rememberScalingLazyListState(initialCenterItemIndex = 0)
+                        .also { state = it },
+                    modifier = Modifier.testTag(TEST_TAG).requiredSize(
+                        itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                    ),
+                    autoCentering = AutoCenteringParams(itemIndex = 0),
+                    flingBehavior = ScalingLazyColumnDefaults.snapFlingBehavior(
+                        state = state,
+                        snapOffset = snapOffset
+                    )
+                ) {
+                    items(5) {
+                        Box(Modifier.requiredSize(itemSizeDp))
+                    }
+                }
+            }
+        }
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.waitForIdle()
+        state.layoutInfo.assertVisibleItems(count = 3, startIndex = 0)
+
+        rule.waitForIdle()
+        rule.onNodeWithTag(TEST_TAG).performTouchInput {
+            // Swipe by an amount that is not a whole item + gap
+            swipeWithVelocity(
+                start = Offset(centerX, bottom),
+                end = Offset(centerX, bottom - (itemSizePx.toFloat())),
+                endVelocity = 1f, // Ensure it's not a fling.
+            )
+        }
+
+        rule.waitForIdle()
+        state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
+        assertThat(state.centerItemIndex).isEqualTo(1)
+        assertThat(state.centerItemScrollOffset).isEqualTo(snapOffsetPx)
+    }
+
+    @Test
+    fun visibleItemsAreCorrectAfterScrollingReverseLayout() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            WithTouchSlop(0f) {
+                ScalingLazyColumn(
+                    state = rememberScalingLazyListState().also { state = it },
+                    modifier = Modifier.testTag(TEST_TAG).requiredSize(
+                        itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                    ),
+                    reverseLayout = true,
+                    autoCentering = null
+                ) {
+                    items(5) {
+                        Box(Modifier.requiredSize(itemSizeDp))
+                    }
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.onNodeWithTag(TEST_TAG).performTouchInput {
+            swipeDown(
+                startY = top,
+                endY = top + (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
+            )
+        }
+        rule.waitForIdle()
+        state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
+    }
+
+    @Test
+    fun visibleItemsAreCorrectAfterScrollNoScaling() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            WithTouchSlop(0f) {
+                ScalingLazyColumn(
+                    state = rememberScalingLazyListState(initialCenterItemIndex = 0)
+                        .also { state = it },
+                    modifier = Modifier.testTag(TEST_TAG).requiredSize(
+                        itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                    ),
+                    scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+                    autoCentering = AutoCenteringParams(itemIndex = 0)
+                ) {
+                    items(5) {
+                        Box(Modifier.requiredSize(itemSizeDp).testTag("Item:" + it))
+                    }
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.waitForIdle()
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag(TEST_TAG).performTouchInput {
+            swipeUp(
+                startY = bottom,
+                endY = bottom - (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()),
+            )
+        }
+        rule.waitForIdle()
+        state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
+        assertThat(state.centerItemIndex).isEqualTo(1)
+    }
+
+    @Test
+    fun visibleItemsAreCorrectAfterScrollNoScalingForReverseLayout() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            WithTouchSlop(0f) {
+                ScalingLazyColumn(
+                    state = rememberScalingLazyListState(8).also { state = it },
+                    modifier = Modifier.testTag(TEST_TAG).requiredSize(
+                        itemSizeDp * 4f + defaultItemSpacingDp * 3f
+                    ),
+                    scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+                    reverseLayout = true
+                ) {
+                    items(15) {
+                        Box(Modifier.requiredSize(itemSizeDp).testTag("Item:" + it))
+                    }
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.waitForIdle()
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag(TEST_TAG).performTouchInput {
+            swipeDown(
+                startY = top,
+                endY = top + (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
+            )
+        }
+        rule.waitForIdle()
+        state.layoutInfo.assertVisibleItems(count = 5, startIndex = 7)
+        assertThat(state.centerItemIndex).isEqualTo(9)
+        assertThat(state.centerItemScrollOffset).isEqualTo(0)
+    }
+
+    @Composable
+    fun ObservingFun(
+        state: ScalingLazyListState,
+        currentInfo: StableRef<ScalingLazyListLayoutInfo?>
+    ) {
+        currentInfo.value = state.layoutInfo
+    }
+
+    @Test
+    fun visibleItemsAreObservableWhenWeScroll() {
+        lateinit var state: ScalingLazyListState
+        val currentInfo = StableRef<ScalingLazyListLayoutInfo?>(null)
+        rule.setContent {
+            WithTouchSlop(0f) {
+                ScalingLazyColumn(
+                    state = rememberScalingLazyListState().also { state = it },
+                    modifier = Modifier
+                        .testTag(TEST_TAG)
+                        .requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f),
+                    autoCentering = null
+                ) {
+                    items(6) {
+                        Box(Modifier.requiredSize(itemSizeDp))
+                    }
+                }
+                ObservingFun(state, currentInfo)
+            }
+        }
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.waitForIdle()
+        rule.mainClock.autoAdvance = false
+        currentInfo.value = null
+        rule.onNodeWithTag(TEST_TAG).performTouchInput {
+            swipeUp(
+                startY = bottom,
+                endY = bottom - (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()),
+            )
+        }
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeBy(milliseconds = 1000)
+        assertThat(currentInfo.value).isNotNull()
+        currentInfo.value!!.assertVisibleItems(count = 4, startIndex = 1)
+    }
+
+    fun ScalingLazyListLayoutInfo.assertVisibleItems(
+        count: Int,
+        startIndex: Int = 0,
+        unscaledSize: Int = itemSizePx,
+        spacing: Int = defaultItemSpacingPx,
+        anchorType: ScalingLazyListAnchorType = ScalingLazyListAnchorType.ItemCenter
+    ) {
+        assertThat(visibleItemsInfo.size).isEqualTo(count)
+        var currentIndex = startIndex
+        var previousEndOffset = -1
+        visibleItemsInfo.forEach {
+            assertThat(it.index).isEqualTo(currentIndex)
+            assertThat(it.size).isEqualTo((unscaledSize * it.scale).roundToInt())
+            currentIndex++
+            val startOffset = it.startOffset(anchorType).roundToInt()
+            if (previousEndOffset != -1) {
+                assertThat(spacing).isEqualTo(startOffset - previousEndOffset)
+            }
+            previousEndOffset = startOffset + it.size
+        }
+    }
+
+    @Test
+    fun itemFillingParentWidth() {
+        lateinit var state: ScalingLazyListState
+        rule.setContentWithTestViewConfiguration {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(8).also { state = it },
+                modifier = Modifier.requiredSize(width = 100.dp, height = 150.dp),
+                contentPadding = PaddingValues(horizontal = 0.dp),
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f)
+            ) {
+                items(listOf(0)) {
+                    Spacer(
+                        Modifier.fillParentMaxWidth().requiredHeight(50.dp).testTag(firstItemTag)
+                    )
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(100.dp)
+            .assertHeightIsEqualTo(50.dp)
+    }
+
+    @Test
+    fun itemFillingParentHeight() {
+        lateinit var state: ScalingLazyListState
+        rule.setContentWithTestViewConfiguration {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(8).also { state = it },
+                modifier = Modifier.requiredSize(width = 100.dp, height = 150.dp),
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f)
+            ) {
+                items(listOf(0)) {
+                    Spacer(
+                        Modifier.requiredWidth(50.dp).fillParentMaxHeight().testTag(firstItemTag)
+                    )
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(50.dp)
+            .assertHeightIsEqualTo(150.dp)
+    }
+
+    @Test
+    fun itemFillingParentSize() {
+        lateinit var state: ScalingLazyListState
+        rule.setContentWithTestViewConfiguration {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(8).also { state = it },
+                modifier = Modifier.requiredSize(width = 100.dp, height = 150.dp),
+                contentPadding = PaddingValues(horizontal = 0.dp),
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f)
+            ) {
+                items(listOf(0)) {
+                    Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(100.dp)
+            .assertHeightIsEqualTo(150.dp)
+    }
+
+    @Test
+    fun itemFillingParentWidthFraction() {
+        lateinit var state: ScalingLazyListState
+        rule.setContentWithTestViewConfiguration {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(8).also { state = it },
+                modifier = Modifier.requiredSize(width = 100.dp, height = 150.dp),
+                contentPadding = PaddingValues(horizontal = 0.dp),
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f)
+            ) {
+                items(listOf(0)) {
+                    Spacer(
+                        Modifier.fillParentMaxWidth(0.7f)
+                            .requiredHeight(50.dp)
+                            .testTag(firstItemTag)
+                    )
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(70.dp)
+            .assertHeightIsEqualTo(50.dp)
+    }
+
+    @Test
+    fun itemFillingParentHeightFraction() {
+        lateinit var state: ScalingLazyListState
+        rule.setContentWithTestViewConfiguration {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(8).also { state = it },
+                modifier = Modifier.requiredSize(width = 100.dp, height = 150.dp)
+            ) {
+                items(listOf(0)) {
+                    Spacer(
+                        Modifier.requiredWidth(50.dp)
+                            .fillParentMaxHeight(0.3f)
+                            .testTag(firstItemTag)
+                    )
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(50.dp)
+            .assertHeightIsEqualTo(45.dp)
+    }
+
+    @Test
+    fun itemFillingParentSizeFraction() {
+        lateinit var state: ScalingLazyListState
+        rule.setContentWithTestViewConfiguration {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(8).also { state = it },
+                modifier = Modifier.requiredSize(width = 100.dp, height = 150.dp),
+                contentPadding = PaddingValues(horizontal = 0.dp)
+            ) {
+                items(listOf(0)) {
+                    Spacer(Modifier.fillParentMaxSize(0.5f).testTag(firstItemTag))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(50.dp)
+            .assertHeightIsEqualTo(75.dp)
+    }
+
+    @Test
+    fun itemFillingParentSizeParentResized() {
+        lateinit var state: ScalingLazyListState
+        var parentSize by mutableStateOf(100.dp)
+        rule.setContentWithTestViewConfiguration {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(8).also { state = it },
+                modifier = Modifier.requiredSize(parentSize),
+                contentPadding = PaddingValues(horizontal = 0.dp),
+            ) {
+                items(listOf(0)) {
+                    Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            parentSize = 150.dp
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(150.dp)
+            .assertHeightIsEqualTo(150.dp)
+    }
+
+    @Test
+    fun listSizeFitsContentsIfNotSet() {
+        lateinit var state: ScalingLazyListState
+        var itemSize by mutableStateOf(100.dp)
+        rule.setContentWithTestViewConfiguration {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(8).also { state = it },
+                modifier = Modifier.testTag(scalingLazyColumnTag),
+                contentPadding = PaddingValues(horizontal = 0.dp),
+            ) {
+                items(listOf(0)) {
+                    Spacer(Modifier.size(itemSize).testTag(firstItemTag))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+
+        rule.onNodeWithTag(scalingLazyColumnTag)
+            .assertWidthIsEqualTo(itemSize)
+
+        rule.runOnIdle {
+            itemSize = 150.dp
+        }
+
+        rule.onNodeWithTag(scalingLazyColumnTag)
+            .assertWidthIsEqualTo(itemSize)
+
+        rule.runOnIdle {
+            itemSize = 50.dp
+        }
+
+        rule.onNodeWithTag(scalingLazyColumnTag)
+            .assertWidthIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun listStateUsesInitialCenterItemIndex() {
+        val startIndexValue = 5
+        lateinit var state: ScalingLazyListState
+
+        rule.setContent {
+            state = rememberScalingLazyListState(initialCenterItemIndex = startIndexValue)
+        }
+
+        assertThat(state.centerItemIndex).isEqualTo(startIndexValue)
+    }
+
+    @Test
+    fun listStateUsesInitialCenterItemScrollOffset() {
+        val startScrollValue = 5
+        lateinit var state: ScalingLazyListState
+
+        rule.setContent {
+            state = rememberScalingLazyListState(initialCenterItemScrollOffset = startScrollValue)
+        }
+
+        assertThat(state.centerItemScrollOffset).isEqualTo(startScrollValue)
+    }
+
+    @Test
+    fun scrollToNotVisibleListWorks() {
+        lateinit var state: ScalingLazyListState
+        lateinit var showList: MutableState<Boolean>
+        rule.setContent {
+            showList = remember { mutableStateOf(true) }
+            state = rememberScalingLazyListState()
+            if (showList.value) {
+                ScalingLazyColumn(
+                    state = state,
+                    modifier = Modifier.testTag(TEST_TAG).requiredSize(
+                        itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                    ),
+                    autoCentering = AutoCenteringParams()
+                ) {
+                    items(25) {
+                        Box(Modifier.requiredSize(itemSizeDp))
+                    }
+                }
+            } else {
+                Box(modifier = Modifier.requiredSize(
+                    itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                ))
+            }
+        }
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+        rule.onNodeWithTag(TEST_TAG).assertIsDisplayed()
+
+        showList.value = false
+
+        rule.waitForIdle()
+        rule.onNodeWithTag(TEST_TAG).assertDoesNotExist()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(10)
+            }
+        }
+
+        rule.waitForIdle()
+        showList.value = true
+
+        rule.waitForIdle()
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+
+        state.layoutInfo.assertVisibleItems(count = 5, startIndex = 8)
+        assertThat(state.centerItemIndex).isEqualTo(10)
+        assertThat(state.centerItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
+    fun scrollToNonExistentItemWorks() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            state = rememberScalingLazyListState()
+            ScalingLazyColumn(
+                state = state,
+                modifier = Modifier.testTag(TEST_TAG).requiredSize(
+                    itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                ),
+                autoCentering = AutoCenteringParams()
+            ) {
+                items(25) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(50)
+            }
+        }
+
+        state.layoutInfo.assertVisibleItems(count = 3, startIndex = 22)
+        assertThat(state.centerItemIndex).isEqualTo(24)
+        assertThat(state.centerItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
+    fun centerItemIndexPublishesUpdatesOnChangeOnly() {
+        lateinit var state: ScalingLazyListState
+        var recompositionCount = 0
+
+        rule.setContent {
+            state = rememberScalingLazyListState(initialCenterItemIndex = 0)
+
+            WithTouchSlop(0f) {
+                state.centerItemIndex
+                recompositionCount++
+
+                ScalingLazyColumn(
+                    state = state,
+                    modifier = Modifier.testTag(TEST_TAG).requiredSize(
+                        itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                    ),
+                    autoCentering = AutoCenteringParams(itemIndex = 0)
+                ) {
+                    items(5) {
+                        Box(Modifier.requiredSize(itemSizeDp))
+                    }
+                }
+            }
+        }
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput {
+            swipeUp(endY = bottom - (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()))
+        }
+
+        rule.waitForIdle()
+        assertThat(recompositionCount).isEqualTo(2)
+    }
+
+    @Test
+    fun scalingLazyColumnCanBeNestedOnHorizontalScrollingComponent() {
+        rule.setContent {
+            val horizontalScrollState = rememberScrollState()
+            Box(
+                modifier = Modifier.horizontalScroll(horizontalScrollState),
+            ) {
+                ScalingLazyColumn {
+                    item { Box(Modifier.size(10.dp)) }
+                }
+            }
+        }
+    }
+}
+
+internal const val TestTouchSlop = 18f
+
+internal fun ComposeContentTestRule.setContentWithTestViewConfiguration(
+    composable: @Composable () -> Unit
+) {
+    this.setContent {
+        WithTouchSlop(TestTouchSlop, composable)
+    }
+}
diff --git a/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyListLayoutInfoTest.kt b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyListLayoutInfoTest.kt
new file mode 100644
index 0000000..913c3ce7
--- /dev/null
+++ b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyListLayoutInfoTest.kt
@@ -0,0 +1,1247 @@
+/*
+ * 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.wear.compose.foundation.lazy
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.math.roundToInt
+import org.junit.Ignore
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+public class ScalingLazyListLayoutInfoTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    private var itemSizePx: Int = 50
+    private var itemSizeDp: Dp = Dp.Infinity
+    private var defaultItemSpacingDp: Dp = 4.dp
+    private var defaultItemSpacingPx = Int.MAX_VALUE
+
+    @Before
+    fun before() {
+        with(rule.density) {
+            itemSizeDp = itemSizePx.toDp()
+            defaultItemSpacingPx = defaultItemSpacingDp.roundToPx()
+        }
+    }
+
+    @Ignore("Awaiting fix for b/236217874")
+    @Test
+    fun visibleItemsAreCorrect() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                ),
+                autoCentering = AutoCenteringParams()
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            assertThat(state.centerItemIndex).isEqualTo(1)
+            assertThat(state.centerItemScrollOffset).isEqualTo(0)
+            state.layoutInfo.assertVisibleItems(count = 4)
+        }
+    }
+
+    @Test
+    fun centerItemIndexIsCorrectAfterScrolling() {
+        lateinit var state: ScalingLazyListState
+        var itemSpacingPx: Int = -1
+        val itemSpacingDp = 20.dp
+        var scope: CoroutineScope? = null
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            itemSpacingPx = with(LocalDensity.current) { itemSpacingDp.roundToPx() }
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 3.5f + itemSpacingDp * 2.5f
+                ),
+                verticalArrangement = Arrangement.spacedBy(itemSpacingDp),
+                autoCentering = AutoCenteringParams()
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            assertThat(state.centerItemIndex).isEqualTo(1)
+            assertThat(state.centerItemScrollOffset).isEqualTo(0)
+            state.layoutInfo.assertVisibleItems(count = 3, spacing = itemSpacingPx)
+        }
+
+        // Scroll so that the center item is just above the center line and check that it is still
+        // the correct center item
+        val scrollDistance = (itemSizePx / 2) + 1
+        scope!!.launch {
+            state.animateScrollBy(scrollDistance.toFloat())
+        }
+        rule.runOnIdle {
+            assertThat(state.centerItemIndex).isEqualTo(1)
+            assertThat(state.centerItemScrollOffset).isEqualTo(scrollDistance)
+        }
+    }
+
+    @Test
+    fun orientationIsCorrect() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                ),
+                autoCentering = AutoCenteringParams(),
+                contentPadding = PaddingValues(all = 0.dp)
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.orientation).isEqualTo(Orientation.Vertical)
+        }
+    }
+
+    @Test
+    fun reverseLayoutIsCorrectWhenNotReversed() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                ),
+                autoCentering = AutoCenteringParams(),
+                contentPadding = PaddingValues(all = 0.dp)
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.reverseLayout).isEqualTo(false)
+        }
+    }
+
+    @Test
+    fun reverseLayoutIsCorrectWhenReversed() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                ),
+                autoCentering = AutoCenteringParams(),
+                contentPadding = PaddingValues(all = 0.dp),
+                reverseLayout = true
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.reverseLayout).isEqualTo(true)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectSetExplicitInitialItemIndex() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(initialCenterItemIndex = 0)
+                    .also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                ),
+                autoCentering = AutoCenteringParams(itemIndex = 0)
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            assertThat(state.centerItemIndex).isEqualTo(0)
+            assertThat(state.centerItemScrollOffset).isEqualTo(0)
+            state.layoutInfo.assertVisibleItems(count = 3)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectNoAutoCentering() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                ),
+                autoCentering = null
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            state.layoutInfo.assertVisibleItems(count = 4)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectForReverseLayout() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                ),
+                reverseLayout = true,
+                autoCentering = null
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            assertThat(state.centerItemIndex).isEqualTo(1)
+            state.layoutInfo.assertVisibleItems(count = 4)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectForReverseLayoutWithAutoCentering() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(initialCenterItemIndex = 0)
+                    .also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                ),
+                reverseLayout = true,
+                autoCentering = AutoCenteringParams(itemIndex = 0)
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            assertThat(state.centerItemIndex).isEqualTo(0)
+            assertThat(state.centerItemScrollOffset).isEqualTo(0)
+            state.layoutInfo.assertVisibleItems(count = 3)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectAfterScrolling() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                ),
+                autoCentering = null
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
+            }
+            state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
+        }
+    }
+
+    @Test
+    fun itemsCorrectScrollPastStartEndAutoCenterItemZeroOddHeightViewportOddHeightItems() {
+        visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 41, false)
+    }
+
+    @Test
+    fun itemsCorrectScrollPastStartEndAutoCenterItemZeroOddHeightViewportEvenHeightItems() {
+        visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 40, false)
+    }
+
+    @Test
+    fun itemsCorrectScrollPastStartEndAutoCenterItemZeroEvenHeightViewportOddHeightItems() {
+        visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 41, true)
+    }
+
+    @Test
+    fun itemsCorrectScrollPastStartEndAutoCenterItemZeroEvenHeightViewportEvenHeightItems() {
+        visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 40, true)
+    }
+
+    @Test
+    fun itemsCorrectScrollPastStartEndAutoCenterItemOneOddHeightViewportOddHeightItems() {
+        visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 41, false)
+    }
+
+    @Test
+    fun itemsCorrectScrollPastStartEndAutoCenterItemOneOddHeightViewportEvenHeightItems() {
+        visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 40, false)
+    }
+
+    @Test
+    fun itemsCorrectScrollPastStartEndAutoCenterItemOneEvenHeightViewportOddHeightItems() {
+        visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 41, true)
+    }
+
+    @Test
+    fun itemsCorrectScrollPastStartEndAutoCenterItemOneEvenHeightViewportEvenHeightItems() {
+        visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 40, true)
+    }
+
+    private fun visibleItemsAreCorrectAfterScrollingPastEndOfItems(
+        autoCenterItem: Int,
+        localItemSizePx: Int,
+        viewPortSizeEven: Boolean
+    ) {
+        lateinit var state: ScalingLazyListState
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            with(LocalDensity.current) {
+                val viewportSizePx =
+                    (((localItemSizePx * 4 + defaultItemSpacingPx * 3) / 2) * 2) +
+                        if (viewPortSizeEven) 0 else 1
+                scope = rememberCoroutineScope()
+                ScalingLazyColumn(
+                    state = rememberScalingLazyListState(
+                        initialCenterItemIndex = autoCenterItem
+                    ).also { state = it },
+                    modifier = Modifier.requiredSize(
+                        viewportSizePx.toDp()
+                    ),
+                    autoCentering = AutoCenteringParams(itemIndex = autoCenterItem)
+                ) {
+                    items(5) {
+                        Box(Modifier.requiredSize(localItemSizePx.toDp()))
+                    }
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        scope.launch {
+            state.animateScrollBy(localItemSizePx.toFloat() * 10)
+        }
+
+        rule.waitUntil { !state.isScrollInProgress }
+        assertThat(state.centerItemIndex).isEqualTo(4)
+        assertThat(state.centerItemScrollOffset).isEqualTo(0)
+
+        scope.launch {
+            state.animateScrollBy(- localItemSizePx.toFloat() * 10)
+        }
+
+        rule.waitUntil { !state.isScrollInProgress }
+        assertThat(state.centerItemIndex).isEqualTo(autoCenterItem)
+        assertThat(state.centerItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
+    fun largeItemLargerThanViewPortDoesNotGetScaled() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp
+                ),
+                autoCentering = null
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp * 5))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
+            }
+            val firstItem = state.layoutInfo.visibleItemsInfo.first()
+            assertThat(firstItem.offset).isLessThan(0)
+            assertThat(firstItem.offset + firstItem.size).isGreaterThan(itemSizePx)
+            assertThat(state.layoutInfo.visibleItemsInfo.first().scale).isEqualTo(1.0f)
+        }
+    }
+
+    @Test
+    fun itemInsideScalingLinesDoesNotGetScaled() {
+        lateinit var state: ScalingLazyListState
+        val centerItemIndex = 2
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(centerItemIndex).also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 3
+                ),
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            // Get the middle item on the screen
+            val centerScreenItem =
+                state.layoutInfo.visibleItemsInfo.find { it.index == centerItemIndex }
+            // and confirm its offset is 0
+            assertThat(centerScreenItem!!.offset).isEqualTo(0)
+            // And that it is not scaled
+            assertThat(centerScreenItem.scale).isEqualTo(1.0f)
+        }
+    }
+
+    @Test
+    fun itemOutsideScalingLinesDoesGetScaled() {
+        lateinit var state: ScalingLazyListState
+        val centerItemIndex = 2
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(centerItemIndex).also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 4 + defaultItemSpacingDp * 3
+                ),
+            ) {
+                items(6) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            // Get the middle item on the screen
+            val edgeScreenItem =
+                state.layoutInfo.visibleItemsInfo.find { it.index == 0 }
+
+            // And that it is it scaled
+            assertThat(edgeScreenItem!!.scale).isLessThan(1.0f)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectAfterScrollingReverseLayout() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                ),
+                reverseLayout = true,
+                autoCentering = null
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
+            }
+            state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectCenterPivotNoOffset() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(2).also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 2f + defaultItemSpacingDp * 1f
+                ),
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f)
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            state.layoutInfo.assertVisibleItems(count = 3, startIndex = 1)
+            assertThat(state.centerItemIndex).isEqualTo(2)
+            assertThat(state.centerItemScrollOffset).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectCenterPivotWithOffset() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(2, -5).also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 2f + defaultItemSpacingDp * 1f
+                ),
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f)
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            state.layoutInfo.assertVisibleItems(count = 3, startIndex = 1)
+            assertThat(state.centerItemIndex).isEqualTo(2)
+            assertThat(state.centerItemScrollOffset).isEqualTo(-5)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectCenterPivotNoOffsetReverseLayout() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(2).also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 2f + defaultItemSpacingDp * 1f
+                ),
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+                reverseLayout = true
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            state.layoutInfo.assertVisibleItems(count = 3, startIndex = 1)
+            assertThat(state.centerItemIndex).isEqualTo(2)
+            assertThat(state.centerItemScrollOffset).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectCenterPivotWithOffsetReverseLayout() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(2, -5).also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 2f + defaultItemSpacingDp * 1f
+                ),
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+                reverseLayout = true
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            state.layoutInfo.assertVisibleItems(count = 3, startIndex = 1)
+            assertThat(state.centerItemIndex).isEqualTo(2)
+            assertThat(state.centerItemScrollOffset).isEqualTo(-5)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectNoScalingForReverseLayout() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(8).also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 4f + defaultItemSpacingDp * 3f
+                ),
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+                reverseLayout = true
+            ) {
+                items(15) {
+                    Box(Modifier.requiredSize(itemSizeDp).testTag("Item:$it"))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.waitForIdle()
+
+        // Assert that items are being shown at the end of the parent as this is reverseLayout
+        rule.onNodeWithTag(testTag = "Item:8").assertIsDisplayed()
+
+        rule.runOnIdle {
+            state.layoutInfo.assertVisibleItems(count = 5, startIndex = 6)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectAfterScrollNoScaling() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(initialCenterItemIndex = 0)
+                    .also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                ),
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+                autoCentering = AutoCenteringParams(itemIndex = 0)
+            ) {
+                items(5) {
+                    Box(
+                        Modifier
+                            .requiredSize(itemSizeDp)
+                            .testTag("Item:$it"))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(testTag = "Item:0").assertIsDisplayed()
+
+        val scrollAmount = (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()).roundToInt()
+        rule.runOnIdle {
+            assertThat(state.centerItemIndex).isEqualTo(0)
+            assertThat(state.centerItemScrollOffset).isEqualTo(0)
+
+            runBlocking {
+                state.scrollBy(scrollAmount.toFloat())
+            }
+            state.layoutInfo.assertVisibleItems(count = 4)
+            assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(-scrollAmount)
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(-scrollAmount.toFloat())
+            }
+            state.layoutInfo.assertVisibleItems(count = 3)
+            assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectAfterScrollNoScalingForReverseLayout() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(8).also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 4f + defaultItemSpacingDp * 3f
+                ),
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+                reverseLayout = true
+            ) {
+                items(15) {
+                    Box(Modifier.requiredSize(itemSizeDp).testTag("Item:$it"))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(testTag = "Item:8").assertIsDisplayed()
+
+        val scrollAmount = (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()).roundToInt()
+        rule.runOnIdle {
+            state.layoutInfo.assertVisibleItems(count = 5, startIndex = 6)
+            assertThat(state.centerItemIndex).isEqualTo(8)
+            assertThat(state.centerItemScrollOffset).isEqualTo(0)
+
+            runBlocking {
+                state.scrollBy(scrollAmount.toFloat())
+            }
+            state.layoutInfo.assertVisibleItems(count = 5, startIndex = 7)
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(-scrollAmount.toFloat())
+            }
+            state.layoutInfo.assertVisibleItems(count = 5, startIndex = 6)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectAfterDispatchRawDeltaScrollNoScaling() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(initialCenterItemIndex = 0)
+                    .also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                ),
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+                autoCentering = AutoCenteringParams(itemIndex = 0)
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        val scrollAmount = itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()
+        rule.runOnIdle {
+            runBlocking {
+                state.dispatchRawDelta(scrollAmount)
+            }
+            state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
+            assertThat(state.layoutInfo.visibleItemsInfo.first().offset)
+                .isEqualTo(-scrollAmount.roundToInt())
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.dispatchRawDelta(-scrollAmount)
+            }
+            state.layoutInfo.assertVisibleItems(count = 3, startIndex = 0)
+            assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectAfterDispatchRawDeltaScrollNoScalingForReverseLayout() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                ),
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+                reverseLayout = true,
+                autoCentering = null
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        val firstItemOffset = state.layoutInfo.visibleItemsInfo.first().offset
+        rule.runOnIdle {
+            runBlocking {
+                state.dispatchRawDelta(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
+            }
+            state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
+            assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(firstItemOffset)
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.dispatchRawDelta(-(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()))
+            }
+            state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
+            assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(firstItemOffset)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectWithCustomSpacing() {
+        lateinit var state: ScalingLazyListState
+        val spacing: Dp = 10.dp
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(itemSizeDp * 3.5f + spacing * 2.5f),
+                verticalArrangement = Arrangement.spacedBy(spacing),
+                autoCentering = null
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            val spacingPx = with(rule.density) {
+                spacing.roundToPx()
+            }
+            state.layoutInfo.assertVisibleItems(
+                count = 4,
+                spacing = spacingPx
+            )
+        }
+    }
+
+    @Composable
+    fun ObservingFun(
+        state: ScalingLazyListState,
+        currentInfo: StableRef<ScalingLazyListLayoutInfo?>
+    ) {
+        currentInfo.value = state.layoutInfo
+    }
+
+    @Test
+    fun visibleItemsAreObservableWhenWeScroll() {
+        lateinit var state: ScalingLazyListState
+        val currentInfo = StableRef<ScalingLazyListLayoutInfo?>(null)
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f),
+                autoCentering = null
+            ) {
+                items(6) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+            ObservingFun(state, currentInfo)
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            // empty it here and scrolling should invoke observingFun again
+            currentInfo.value = null
+            runBlocking {
+                state.scrollBy(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo.value).isNotNull()
+            currentInfo.value!!.assertVisibleItems(count = 4, startIndex = 1)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreObservableWhenWeDispatchRawDeltaScroll() {
+        lateinit var state: ScalingLazyListState
+        val currentInfo = StableRef<ScalingLazyListLayoutInfo?>(null)
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f),
+                autoCentering = null
+            ) {
+                items(6) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+            ObservingFun(state, currentInfo)
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            // empty it here and scrolling should invoke observingFun again
+            currentInfo.value = null
+            runBlocking {
+                state.dispatchRawDelta(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo.value).isNotNull()
+            currentInfo.value!!.assertVisibleItems(count = 4, startIndex = 1)
+        }
+    }
+
+    @Composable
+    fun ObservingIsScrollInProgressTrueFun(
+        state: ScalingLazyListState,
+        currentInfo: StableRef<Boolean?>
+    ) {
+        // If isScrollInProgress is ever true record it - otherwise leave the value as null
+        if (state.isScrollInProgress) {
+            currentInfo.value = true
+        }
+    }
+
+    @Test
+    fun isScrollInProgressIsObservableWhenWeScroll() {
+        lateinit var state: ScalingLazyListState
+        lateinit var scope: CoroutineScope
+        val currentInfo = StableRef<Boolean?>(null)
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f)
+            ) {
+                items(6) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+            ObservingIsScrollInProgressTrueFun(state, currentInfo)
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        scope.launch {
+            // empty it here and scrolling should invoke observingFun again
+            currentInfo.value = null
+            state.animateScrollBy(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo.value).isNotNull()
+            assertThat(currentInfo.value).isTrue()
+        }
+    }
+
+    @Composable
+    fun ObservingCentralItemIndexFun(
+        state: ScalingLazyListState,
+        currentInfo: StableRef<Int?>
+    ) {
+        currentInfo.value = state.centerItemIndex
+    }
+
+    @Test
+    fun isCentralListItemIndexObservableWhenWeScroll() {
+        lateinit var state: ScalingLazyListState
+        lateinit var scope: CoroutineScope
+        val currentInfo = StableRef<Int?>(null)
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f),
+                autoCentering = null
+            ) {
+                items(6) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+            ObservingCentralItemIndexFun(state, currentInfo)
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        scope.launch {
+            // empty it here and scrolling should invoke observingFun again
+            currentInfo.value = null
+            state.animateScrollBy(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo.value).isNotNull()
+            assertThat(currentInfo.value).isEqualTo(2)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreObservableWhenResize() {
+        lateinit var state: ScalingLazyListState
+        var size by mutableStateOf(itemSizeDp * 2)
+        var currentInfo: ScalingLazyListLayoutInfo? = null
+        @Composable
+        fun observingFun() {
+            currentInfo = state.layoutInfo
+        }
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it }
+            ) {
+                item {
+                    Box(Modifier.requiredSize(size))
+                }
+            }
+            observingFun()
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            assertThat(currentInfo).isNotNull()
+            currentInfo!!.assertVisibleItems(count = 1, unscaledSize = itemSizePx * 2)
+            currentInfo = null
+            size = itemSizeDp
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo).isNotNull()
+            currentInfo!!.assertVisibleItems(count = 1, unscaledSize = itemSizePx)
+        }
+    }
+
+    @Test
+    fun viewportOffsetsAndSizeAreCorrect() {
+        val sizePx = 45
+        val sizeDp = with(rule.density) { sizePx.toDp() }
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                Modifier.requiredSize(sizeDp),
+                state = rememberScalingLazyListState().also { state = it }
+            ) {
+                items(4) {
+                    Box(Modifier.requiredSize(sizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(0)
+            assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx)
+            assertThat(state.layoutInfo.viewportSize).isEqualTo(IntSize(sizePx, sizePx))
+            assertThat(state.layoutInfo.beforeContentPadding).isEqualTo(0)
+            assertThat(state.layoutInfo.afterContentPadding).isEqualTo(0)
+            assertThat(state.layoutInfo.beforeAutoCenteringPadding).isEqualTo(0)
+            assertThat(state.layoutInfo.afterAutoCenteringPadding).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun viewportOffsetsAndSizeAreCorrectWithContentPadding() {
+        val sizePx = 45
+        val startPaddingPx = 10
+        val endPaddingPx = 15
+        val sizeDp = with(rule.density) { sizePx.toDp() }
+        val topPaddingDp = with(rule.density) { startPaddingPx.toDp() }
+        val bottomPaddingDp = with(rule.density) { endPaddingPx.toDp() }
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                Modifier.requiredSize(sizeDp),
+                contentPadding = PaddingValues(top = topPaddingDp, bottom = bottomPaddingDp),
+                state = rememberScalingLazyListState().also { state = it }
+            ) {
+                items(4) {
+                    Box(Modifier.requiredSize(sizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
+            assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
+            assertThat(state.layoutInfo.viewportSize).isEqualTo(IntSize(sizePx, sizePx))
+            assertThat(state.layoutInfo.beforeContentPadding).isEqualTo(10)
+            assertThat(state.layoutInfo.afterContentPadding).isEqualTo(15)
+            assertThat(state.layoutInfo.beforeAutoCenteringPadding).isEqualTo(0)
+            assertThat(state.layoutInfo.afterAutoCenteringPadding).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun viewportOffsetsAreCorrectWithAutoCentering() {
+        val itemSizePx = 45
+        val itemSizeDp = with(rule.density) { itemSizePx.toDp() }
+        val viewPortSizePx = itemSizePx * 4
+        val viewPortSizeDp = with(rule.density) { viewPortSizePx.toDp() }
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                Modifier.requiredSize(viewPortSizeDp),
+                state = rememberScalingLazyListState(
+                    initialCenterItemIndex = 0
+                ).also { state = it },
+                autoCentering = AutoCenteringParams()
+            ) {
+                items(7) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(0)
+            assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(viewPortSizePx)
+            assertThat(state.layoutInfo.beforeContentPadding).isEqualTo(0)
+            assertThat(state.layoutInfo.afterContentPadding).isEqualTo(0)
+            assertThat(state.layoutInfo.beforeAutoCenteringPadding).isGreaterThan(0)
+            assertThat(state.layoutInfo.afterAutoCenteringPadding).isEqualTo(0)
+
+            runBlocking {
+                state.scrollToItem(3)
+            }
+            assertThat(state.layoutInfo.beforeAutoCenteringPadding).isEqualTo(0)
+            assertThat(state.layoutInfo.afterAutoCenteringPadding).isEqualTo(0)
+
+            runBlocking {
+                state.scrollToItem(5)
+            }
+            assertThat(state.layoutInfo.beforeAutoCenteringPadding).isEqualTo(0)
+            assertThat(state.layoutInfo.afterAutoCenteringPadding).isGreaterThan(0)
+        }
+    }
+
+    @Test
+    fun totalCountIsCorrect() {
+        var count by mutableStateOf(10)
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it }
+            ) {
+                items(count) {
+                    Box(Modifier.requiredSize(10.dp))
+                }
+            }
+        }
+
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.totalItemsCount).isEqualTo(10)
+            count = 20
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.totalItemsCount).isEqualTo(20)
+        }
+    }
+
+    private fun ScalingLazyListLayoutInfo.assertVisibleItems(
+        count: Int,
+        startIndex: Int = 0,
+        unscaledSize: Int = itemSizePx,
+        spacing: Int = defaultItemSpacingPx,
+        anchorType: ScalingLazyListAnchorType = ScalingLazyListAnchorType.ItemCenter
+    ) {
+        assertThat(visibleItemsInfo.size).isEqualTo(count)
+        var currentIndex = startIndex
+        var previousEndOffset = -1
+        visibleItemsInfo.forEach {
+            assertThat(it.index).isEqualTo(currentIndex)
+            assertThat(it.size).isEqualTo((unscaledSize * it.scale).roundToInt())
+            currentIndex++
+            val startOffset = it.startOffset(anchorType).roundToInt()
+            if (previousEndOffset != -1) {
+                assertThat(spacing).isEqualTo(startOffset - previousEndOffset)
+            }
+            previousEndOffset = startOffset + it.size
+        }
+    }
+}
+
+@Stable
+public class StableRef<T>(var value: T)
\ No newline at end of file
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumn.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumn.kt
new file mode 100644
index 0000000..ac02f9e
--- /dev/null
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumn.kt
@@ -0,0 +1,738 @@
+/*
+ * 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.wear.compose.foundation.lazy
+
+import androidx.annotation.RestrictTo
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.Easing
+import androidx.compose.animation.core.exponentialDecay
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.ScrollableDefaults
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.offset
+
+/**
+ * Receiver scope which is used by [ScalingLazyColumn].
+ */
+@ScalingLazyScopeMarker
+public sealed interface ScalingLazyListScope {
+    /**
+     * Adds a single item.
+     *
+     * @param key a stable and unique key representing the item. Using the same key
+     * for multiple items in the list is not allowed. Type of the key should be saveable
+     * via Bundle on Android. If null is passed the position in the list will represent the key.
+     * When you specify the key the scroll position will be maintained based on the key, which
+     * means if you add/remove items before the current visible item the item with the given key
+     * will be kept as the first visible one.
+     * @param content the content of the item
+     */
+    fun item(key: Any? = null, content: @Composable ScalingLazyListItemScope.() -> Unit)
+
+    /**
+     * Adds a [count] of items.
+     *
+     * @param count the items count
+     * @param key a factory of stable and unique keys representing the item. Using the same key
+     * for multiple items in the list is not allowed. Type of the key should be saveable
+     * via Bundle on Android. If null is passed the position in the list will represent the key.
+     * When you specify the key the scroll position will be maintained based on the key, which
+     * means if you add/remove items before the current visible item the item with the given key
+     * will be kept as the first visible one.
+     * @param itemContent the content displayed by a single item
+     */
+    fun items(
+        count: Int,
+        key: ((index: Int) -> Any)? = null,
+        itemContent: @Composable ScalingLazyListItemScope.(index: Int) -> Unit
+    )
+}
+
+/**
+ * Adds a list of items.
+ *
+ * @param items the data list
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the list is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the list will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> ScalingLazyListScope.items(
+    items: List<T>,
+    noinline key: ((item: T) -> Any)? = null,
+    crossinline itemContent: @Composable ScalingLazyListItemScope.(item: T) -> Unit
+) = items(items.size, if (key != null) { index: Int -> key(items[index]) } else null) {
+    itemContent(items[it])
+}
+
+/**
+ * Adds a list of items where the content of an item is aware of its index.
+ *
+ * @param items the data list
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the list is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the list will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> ScalingLazyListScope.itemsIndexed(
+    items: List<T>,
+    noinline key: ((index: Int, item: T) -> Any)? = null,
+    crossinline itemContent: @Composable ScalingLazyListItemScope.(index: Int, item: T) -> Unit
+) = items(items.size, if (key != null) { index: Int -> key(index, items[index]) } else null) {
+    itemContent(it, items[it])
+}
+
+/**
+ * Adds an array of items.
+ *
+ * @param items the data array
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the list is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the list will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> ScalingLazyListScope.items(
+    items: Array<T>,
+    noinline key: ((item: T) -> Any)? = null,
+    crossinline itemContent: @Composable ScalingLazyListItemScope.(item: T) -> Unit
+) = items(items.size, if (key != null) { index: Int -> key(items[index]) } else null) {
+    itemContent(items[it])
+}
+
+/**
+ * Adds an array of items where the content of an item is aware of its index.
+ *
+ * @param items the data array
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the list is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the list will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param itemContent the content displayed by a single item
+ */
+public inline fun <T> ScalingLazyListScope.itemsIndexed(
+    items: Array<T>,
+    noinline key: ((index: Int, item: T) -> Any)? = null,
+    crossinline itemContent: @Composable ScalingLazyListItemScope.(index: Int, item: T) -> Unit
+) = items(items.size, if (key != null) { index: Int -> key(index, items[index]) } else null) {
+    itemContent(it, items[it])
+}
+
+@Immutable
+@kotlin.jvm.JvmInline
+public value class ScalingLazyListAnchorType internal constructor(internal val type: Int) {
+
+    companion object {
+        /**
+         * Place the center of the item on (or as close to) the center line of the viewport
+         */
+        val ItemCenter = ScalingLazyListAnchorType(0)
+
+        /**
+         * Place the start (edge) of the item on, or as close to as possible, the center line of the
+         * viewport. For normal layout this will be the top edge of the item, for reverseLayout it
+         * will be the bottom edge.
+         */
+        val ItemStart = ScalingLazyListAnchorType(1)
+    }
+
+    override fun toString(): String {
+        return when (this) {
+            ItemStart -> "ScalingLazyListAnchorType.ItemStart"
+            else -> "ScalingLazyListAnchorType.ItemCenter"
+        }
+    }
+}
+
+/**
+ * Parameters to determine which list item and offset to calculate auto-centering spacing for. The
+ * default values are [itemIndex] = 1 and [itemOffset] = 0. This will provide sufficient padding for
+ * the second item (index = 1) in the list being centerable. This is to match the Wear UX
+ * guidelines that a typical list will have a ListHeader item as the first item in the list
+ * (index = 0) and that this should not be scrollable into the middle of the viewport, instead the
+ * first list item that a user can interact with (index = 1) would be the first that would be in the
+ * center.
+ *
+ * If your use case is different and you want all list items to be able to be scrolled to the
+ * viewport middle, including the first item in the list then set [itemIndex] = 0.
+ *
+ * The higher the value for [itemIndex] you provide the less auto centering padding will be
+ * provided as the amount of padding needed to allow that item to be centered will reduce.
+ * Even for a list of short items setting [itemIndex] above 3 or 4 is likely
+ * to result in no auto-centering padding being provided as items with index 3 or 4 will probably
+ * already be naturally scrollable to the center of the viewport.
+ *
+ * [itemOffset] allows adjustment of the items position relative the [ScalingLazyColumn]s
+ * [ScalingLazyListAnchorType]. This can be useful if you need fine grained control over item
+ * positioning and spacing, e.g. If you are lining up the gaps between two items on the viewport
+ * center line where you would want to set the offset to half the distance between listItems in
+ * pixels.
+ *
+ * See also [rememberScalingLazyListState] where similar fields are provided to allow control over
+ * the initially selected centered item index and offset. By default these match the auto centering
+ * defaults meaning that the second item (index = 1) will be the item scrolled to the viewport
+ * center.
+ *
+ * @param itemIndex Which list item index to enable auto-centering from. Space (padding) will be
+ * added such that items with index [itemIndex] or greater will be able to be scrolled to the center
+ * of the viewport. If the developer wants to add additional space to allow other list items to also
+ * be scrollable to the center they can use contentPadding on the ScalingLazyColumn. If the
+ * developer wants custom control over position and spacing they can switch off autoCentering
+ * and provide contentPadding.
+ *
+ * @param itemOffset What offset, if any, to apply when calculating space for auto-centering
+ * the [itemIndex] item. E.g. itemOffset can be used if the developer wants to align the viewport
+ * center in the gap between two list items.
+ *
+ * For an example of a [ScalingLazyColumn] with an explicit itemOffset see:
+ * @sample androidx.wear.compose.foundation.samples.ScalingLazyColumnEdgeAnchoredAndAnimatedScrollTo
+ */
+@Immutable
+public class AutoCenteringParams(
+    // @IntRange(from = 0)
+    internal val itemIndex: Int = 1,
+    internal val itemOffset: Int = 0,
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        return (other is AutoCenteringParams) &&
+            itemIndex == other.itemIndex &&
+            itemOffset == other.itemOffset
+    }
+
+    override fun hashCode(): Int {
+        var result = itemIndex
+        result = 31 * result + itemOffset
+        return result
+    }
+}
+
+/**
+ * A scrolling scaling/fisheye list component that forms a key part of the Wear Material Design
+ * language. Provides scaling and transparency effects to the content items.
+ *
+ * [ScalingLazyColumn] is designed to be able to handle potentially large numbers of content
+ * items. Content items are only materialized and composed when needed.
+ *
+ * If scaling/fisheye functionality is not required then a [LazyColumn] should be considered
+ * instead to avoid any overhead of measuring and calculating scaling and transparency effects for
+ * the content items.
+ *
+ * Example of a [ScalingLazyColumn] with default parameters:
+ * @sample androidx.wear.compose.foundation.samples.SimpleScalingLazyColumn
+ *
+ * Example of a [ScalingLazyColumn] using [ScalingLazyListAnchorType.ItemStart] anchoring, in this
+ * configuration the edge of list items is aligned to the center of the screen. Also this example
+ * shows scrolling to a clicked list item with [ScalingLazyListState.animateScrollToItem]:
+ * @sample androidx.wear.compose.foundation.samples.ScalingLazyColumnEdgeAnchoredAndAnimatedScrollTo
+ *
+ * Example of a [ScalingLazyColumn] with snap of items to the viewport center:
+ * @sample androidx.wear.compose.foundation.samples.SimpleScalingLazyColumnWithSnap
+ *
+ * Example of a [ScalingLazyColumn] where [autoCentering] has been disabled and explicit
+ * [contentPadding] provided to ensure there is space above the first and below the last list item
+ * to allow them to be scrolled into view on circular screens:
+ * @sample androidx.wear.compose.foundation.samples.SimpleScalingLazyColumnWithContentPadding
+ *
+ * For more information, see the
+ * [Lists](https://developer.android.com/training/wearables/components/lists)
+ * guide.
+ *
+ * @param modifier The modifier to be applied to the component
+ * @param state The state of the component
+ * @param contentPadding The padding to apply around the contents
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the bottom to the top
+ * @param verticalArrangement The vertical arrangement of the layout's children. This allows us
+ * to add spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size
+ * @param horizontalAlignment the horizontal alignment applied to the items
+ * @param flingBehavior Logic describing fling behavior. If snapping is required use
+ * [ScalingLazyColumnDefaults.snapFlingBehavior].
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using the state even when it is disabled.
+ * @param scalingParams The parameters to configure the scaling and transparency effects for the
+ * component
+ * @param anchorType How to anchor list items to the center-line of the viewport
+ * @param autoCentering AutoCenteringParams parameter to control whether space/padding should be
+ * automatically added to make sure that list items can be scrolled into the center of the viewport
+ * (based on their [anchorType]). If non-null then space will be added before the first list item,
+ * if needed, to ensure that items with indexes greater than or equal to the itemIndex (offset by
+ * itemOffset pixels) will be able to be scrolled to the center of the viewport. Similarly space
+ * will be added at the end of the list to ensure that items can be scrolled up to the center. If
+ * null no automatic space will be added and instead the developer can use [contentPadding] to
+ * manually arrange the items.
+ * @param content The content of the [ScalingLazyColumn]
+ */
+@Composable
+public fun ScalingLazyColumn(
+    modifier: Modifier = Modifier,
+    state: ScalingLazyListState = rememberScalingLazyListState(),
+    contentPadding: PaddingValues = PaddingValues(horizontal = 10.dp),
+    reverseLayout: Boolean = false,
+    verticalArrangement: Arrangement.Vertical =
+        Arrangement.spacedBy(
+            space = 4.dp,
+            alignment = if (!reverseLayout) Alignment.Top else Alignment.Bottom
+        ),
+    horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
+    flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+    userScrollEnabled: Boolean = true,
+    scalingParams: ScalingParams = ScalingLazyColumnDefaults.scalingParams(),
+    anchorType: ScalingLazyListAnchorType = ScalingLazyListAnchorType.ItemCenter,
+    autoCentering: AutoCenteringParams? = AutoCenteringParams(),
+    content: ScalingLazyListScope.() -> Unit
+) {
+    var initialized by remember { mutableStateOf(false) }
+    BoxWithConstraints(modifier = modifier, propagateMinConstraints = true) {
+        val density = LocalDensity.current
+        val layoutDirection = LocalLayoutDirection.current
+        val extraPaddingInPixels = scalingParams.resolveViewportVerticalOffset(constraints)
+
+        with(density) {
+            val extraPadding = extraPaddingInPixels.toDp()
+            val combinedPaddingValues = CombinedPaddingValues(
+                contentPadding = contentPadding,
+                extraPadding = extraPadding
+            )
+
+            val beforeContentPaddingInPx =
+                if (reverseLayout) contentPadding.calculateBottomPadding().roundToPx()
+                else contentPadding.calculateTopPadding().roundToPx()
+
+            val afterContentPaddingInPx =
+                if (reverseLayout) contentPadding.calculateTopPadding().roundToPx()
+                else contentPadding.calculateBottomPadding().roundToPx()
+
+            val itemScope =
+                ScalingLazyListItemScopeImpl(
+                    density = density,
+                    constraints = constraints.offset(
+                        horizontal = -(
+                            contentPadding.calculateStartPadding(layoutDirection) +
+                                contentPadding.calculateEndPadding(layoutDirection)
+                            ).toPx().toInt(),
+                        vertical = -(
+                            contentPadding.calculateTopPadding() +
+                                contentPadding.calculateBottomPadding()
+                            ).roundToPx()
+                    )
+                )
+
+            // Set up transient state
+            state.scalingParams.value = scalingParams
+            state.extraPaddingPx.value = extraPaddingInPixels
+            state.beforeContentPaddingPx.value = beforeContentPaddingInPx
+            state.afterContentPaddingPx.value = afterContentPaddingInPx
+            state.viewportHeightPx.value = constraints.maxHeight
+            state.gapBetweenItemsPx.value =
+                verticalArrangement.spacing.roundToPx()
+            state.anchorType.value = anchorType
+            state.autoCentering.value = autoCentering
+            state.reverseLayout.value = reverseLayout
+            state.localInspectionMode.value = LocalInspectionMode.current
+
+            LazyColumn(
+                modifier = Modifier
+                    .clipToBounds()
+                    .verticalNegativePadding(extraPadding)
+                    .onGloballyPositioned {
+                        val layoutInfo = state.layoutInfo
+                        if (!initialized &&
+                            layoutInfo is DefaultScalingLazyListLayoutInfo &&
+                            layoutInfo.readyForInitialScroll
+                        ) {
+                            initialized = true
+                        }
+                    },
+                horizontalAlignment = horizontalAlignment,
+                contentPadding = combinedPaddingValues,
+                reverseLayout = reverseLayout,
+                verticalArrangement = verticalArrangement,
+                state = state.lazyListState,
+                flingBehavior = flingBehavior,
+                userScrollEnabled = userScrollEnabled,
+            ) {
+                val scope = ScalingLazyListScopeImpl(
+                    state = state,
+                    scope = this,
+                    itemScope = itemScope
+                )
+                // Only add spacers if autoCentering == true as we have to consider the impact of
+                // vertical spacing between items.
+                if (autoCentering != null) {
+                    item {
+                        Spacer(
+                            modifier = Modifier.height(state.topAutoCenteringItemSizePx.toDp())
+                        )
+                    }
+                }
+                scope.content()
+                if (autoCentering != null) {
+                    item {
+                        Spacer(
+                            modifier = Modifier.height(state.bottomAutoCenteringItemSizePx.toDp())
+                        )
+                    }
+                }
+            }
+            if (initialized) {
+                LaunchedEffect(state) {
+                    state.scrollToInitialItem()
+                }
+            }
+        }
+    }
+}
+
+/**
+ * Contains the default values used by [ScalingLazyColumn]
+ */
+public object ScalingLazyColumnDefaults {
+    /**
+     * Creates a [ScalingParams] that represents the scaling and alpha properties for a
+     * [ScalingLazyColumn].
+     *
+     * Items in the ScalingLazyColumn have scaling and alpha effects applied to them depending on
+     * their position in the viewport. The closer to the edge (top or bottom) of the viewport that
+     * they are the greater the down scaling and transparency that is applied. Note that scaling and
+     * transparency effects are applied from the center of the viewport (full size and normal
+     * transparency) towards the edge (items can be smaller and more transparent).
+     *
+     * Deciding how much scaling and alpha to apply is based on the position and size of the item
+     * and on a series of properties that are used to determine the transition area for each item.
+     *
+     * The transition area is defined by the edge of the screen and a transition line which is
+     * calculated for each item in the list. The items transition line is based upon its size with
+     * the potential for larger list items to start their transition earlier (closer to the center)
+     * than smaller items.
+     *
+     * [minTransitionArea] and [maxTransitionArea] are both in the range [0f..1f] and are
+     * the fraction of the distance between the edge of the viewport and the center of
+     * the viewport. E.g. a value of 0.2f for minTransitionArea and 0.75f for maxTransitionArea
+     * determines that all transition lines will fall between 1/5th (20%) and 3/4s (75%) of the
+     * distance between the viewport edge and center.
+     *
+     * The size of the each item is used to determine where within the transition area range
+     * minTransitionArea..maxTransitionArea the actual transition line will be. [minElementHeight]
+     * and [maxElementHeight] are used along with the item height (as a fraction of the viewport
+     * height in the range [0f..1f]) to find the transition line. So if the items size is 0.25f
+     * (25%) of way between minElementSize..maxElementSize then the transition line will be 0.25f
+     * (25%) of the way between minTransitionArea..maxTransitionArea.
+     *
+     * A list item smaller than minElementHeight is rounded up to minElementHeight and larger than
+     * maxElementHeight is rounded down to maxElementHeight. Whereabouts the items height sits
+     * between minElementHeight..maxElementHeight is then used to determine where the transition
+     * line sits between minTransitionArea..maxTransition area.
+     *
+     * If an item is smaller than or equal to minElementSize its transition line with be at
+     * minTransitionArea and if it is larger than or equal to maxElementSize its transition line
+     * will be  at maxTransitionArea.
+     *
+     * For example, if we take the default values for minTransitionArea = 0.2f and
+     * maxTransitionArea = 0.6f and minElementSize = 0.2f and maxElementSize= 0.8f then an item
+     * with a height of 0.4f (40%) of the viewport height is one third of way between
+     * minElementSize and maxElementSize, (0.4f - 0.2f) / (0.8f - 0.2f) = 0.33f. So its transition
+     * line would be one third of way between 0.2f and 0.6f, transition line = 0.2f + (0.6f -
+     * 0.2f) * 0.33f = 0.33f.
+     *
+     * Once the position of the transition line is established we now have a transition area
+     * for the item, e.g. in the example above the item will start/finish its transitions when it
+     * is 0.33f (33%) of the distance from the edge of the viewport and will start/finish its
+     * transitions at the viewport edge.
+     *
+     * The scaleInterpolator is used to determine how much of the scaling and alpha to apply
+     * as the item transits through the transition area.
+     *
+     * The edge of the item furthest from the edge of the screen is used as a scaling trigger
+     * point for each item.
+     *
+     * @param edgeScale What fraction of the full size of the item to scale it by when most
+     * scaled, e.g. at the edge of the viewport. A value between [0f,1f], so a value of 0.2f
+     * means to scale an item to 20% of its normal size.
+     *
+     * @param edgeAlpha What fraction of the full transparency of the item to draw it with
+     * when closest to the edge of the screen. A value between [0f,1f], so a value of
+     * 0.2f means to set the alpha of an item to 20% of its normal value.
+     *
+     * @param minElementHeight The minimum element height as a ratio of the viewport size to use
+     * for determining the transition point within ([minTransitionArea], [maxTransitionArea])
+     * that a given content item will start to be transitioned. Items smaller than
+     * [minElementHeight] will be treated as if [minElementHeight]. Must be less than or equal to
+     * [maxElementHeight].
+     *
+     * @param maxElementHeight The maximum element height as a ratio of the viewport size to use
+     * for determining the transition point within ([minTransitionArea], [maxTransitionArea])
+     * that a given content item will start to be transitioned. Items larger than [maxElementHeight]
+     * will be treated as if [maxElementHeight]. Must be greater than or equal to
+     * [minElementHeight].
+     *
+     * @param minTransitionArea The lower bound of the transition line area, closest to the
+     * edge of the viewport. Defined as a fraction (value between 0f..1f) of the distance between
+     * the viewport edge and viewport center line. Must be less than or equal to
+     * [maxTransitionArea].
+     *
+     * @param maxTransitionArea The upper bound of the transition line area, closest to the
+     * center of the viewport. The fraction (value between 0f..1f) of the distance
+     * between the viewport edge and viewport center line. Must be greater
+     * than or equal to [minTransitionArea].
+     *
+     * @param scaleInterpolator An interpolator to use to determine how to apply scaling as a
+     * item transitions across the scaling transition area.
+     *
+     * @param viewportVerticalOffsetResolver The additional padding to consider above and below the
+     * viewport of a [ScalingLazyColumn] when considering which items to draw in the viewport. If
+     * set to 0 then no additional padding will be provided and only the items which would appear
+     * in the viewport before any scaling is applied will be considered for drawing, this may
+     * leave blank space at the top and bottom of the viewport where the next available item
+     * could have been drawn once other items have been scaled down in size. The larger this
+     * value is set to will allow for more content items to be considered for drawing in the
+     * viewport, however there is a performance cost associated with materializing items that are
+     * subsequently not drawn. The higher/more extreme the scaling parameters that are applied to
+     * the [ScalingLazyColumn] the more padding may be needed to ensure there are always enough
+     * content items available to be rendered. By default will be 5% of the maxHeight of the
+     * viewport above and below the content.
+     */
+    fun scalingParams(
+        edgeScale: Float = 0.7f,
+        edgeAlpha: Float = 0.5f,
+        minElementHeight: Float = 0.2f,
+        maxElementHeight: Float = 0.6f,
+        minTransitionArea: Float = 0.35f,
+        maxTransitionArea: Float = 0.55f,
+        scaleInterpolator: Easing = CubicBezierEasing(0.3f, 0f, 0.7f, 1f),
+        viewportVerticalOffsetResolver: (Constraints) -> Int = { (it.maxHeight / 20f).toInt() }
+    ): ScalingParams = DefaultScalingParams(
+        edgeScale = edgeScale,
+        edgeAlpha = edgeAlpha,
+        minElementHeight = minElementHeight,
+        maxElementHeight = maxElementHeight,
+        minTransitionArea = minTransitionArea,
+        maxTransitionArea = maxTransitionArea,
+        scaleInterpolator = scaleInterpolator,
+        viewportVerticalOffsetResolver = viewportVerticalOffsetResolver
+    )
+
+    /**
+     * Create and remember a [FlingBehavior] that will represent natural fling curve with snap to
+     * central item as the fling decays.
+     *
+     * @param state the state of the [ScalingLazyColumn]
+     * @param snapOffset an optional offset to be applied when snapping the item. After the snap the
+     * snapped items offset will be [snapOffset].
+     * @param decay the decay to use
+     */
+    @Composable
+    public fun snapFlingBehavior(
+        state: ScalingLazyListState,
+        snapOffset: Dp = 0.dp,
+        decay: DecayAnimationSpec<Float> = exponentialDecay()
+    ): FlingBehavior {
+        val snapOffsetPx = with(LocalDensity.current) { snapOffset.roundToPx() }
+        return remember(state, snapOffset, decay) {
+            ScalingLazyColumnSnapFlingBehavior(
+                state = state,
+                snapOffset = snapOffsetPx,
+                decay = decay
+            )
+        }
+    }
+}
+
+private class ScalingLazyListScopeImpl(
+    private val state: ScalingLazyListState,
+    private val scope: LazyListScope,
+    private val itemScope: ScalingLazyListItemScope
+) : ScalingLazyListScope {
+
+    private var currentStartIndex = 0
+
+    override fun item(key: Any?, content: @Composable (ScalingLazyListItemScope.() -> Unit)) {
+        val startIndex = currentStartIndex
+        scope.item(key = key) {
+            ScalingLazyColumnItemWrapper(
+                startIndex,
+                state,
+                itemScope,
+                content
+            )
+        }
+        currentStartIndex++
+    }
+
+    override fun items(
+        count: Int,
+        key: ((index: Int) -> Any)?,
+        itemContent: @Composable (ScalingLazyListItemScope.(index: Int) -> Unit)
+    ) {
+        val startIndex = currentStartIndex
+        scope.items(count = count, key = key) {
+            ScalingLazyColumnItemWrapper(
+                startIndex + it,
+                state = state,
+                itemScope = itemScope
+            ) {
+                itemContent(it)
+            }
+        }
+        currentStartIndex += count
+    }
+}
+
+@Composable
+private fun ScalingLazyColumnItemWrapper(
+    index: Int,
+    state: ScalingLazyListState,
+    itemScope: ScalingLazyListItemScope,
+    content: @Composable (ScalingLazyListItemScope.() -> Unit)
+) {
+    Box(
+        modifier = Modifier.graphicsLayer {
+            val reverseLayout = state.reverseLayout.value!!
+            val anchorType = state.anchorType.value!!
+            val items = state.layoutInfo.internalVisibleItemInfo()
+            val currentItem = items.find { it.index == index }
+            if (currentItem != null) {
+                alpha = currentItem.alpha
+                scaleX = currentItem.scale
+                scaleY = currentItem.scale
+                // Calculate how much to adjust/translate the position of the list item by
+                // determining the different between the unadjusted start position based on the
+                // underlying LazyList layout and the start position adjusted to take into account
+                // scaling of the list items. Items further from the middle of the visible viewport
+                // will be subject to more adjustment.
+                if (currentItem.scale > 0f) {
+                    val offsetAdjust = currentItem.startOffset(anchorType) -
+                        currentItem.unadjustedStartOffset(anchorType)
+                    translationY = if (reverseLayout) -offsetAdjust else offsetAdjust
+                    transformOrigin = TransformOrigin(
+                        pivotFractionX = 0.5f,
+                        pivotFractionY = if (reverseLayout) 1.0f else 0.0f
+                    )
+                }
+            }
+        }
+    ) {
+        itemScope.content()
+    }
+}
+
+/** @hide **/
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Immutable
+public class CombinedPaddingValues(
+    @Stable
+    val contentPadding: PaddingValues,
+    @Stable
+    val extraPadding: Dp
+) : PaddingValues {
+    override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp =
+        contentPadding.calculateLeftPadding(layoutDirection)
+
+    override fun calculateTopPadding(): Dp =
+        contentPadding.calculateTopPadding() + extraPadding
+
+    override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp =
+        contentPadding.calculateRightPadding(layoutDirection)
+
+    override fun calculateBottomPadding(): Dp =
+        contentPadding.calculateBottomPadding() + extraPadding
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null) return false
+        if (this::class != other::class) return false
+
+        other as CombinedPaddingValues
+
+        if (contentPadding != other.contentPadding) return false
+        if (extraPadding != other.extraPadding) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = contentPadding.hashCode()
+        result = 31 * result + extraPadding.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "CombinedPaddingValuesImpl(contentPadding=$contentPadding, " +
+            "extraPadding=$extraPadding)"
+    }
+}
+
+/** @hide **/
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public fun Modifier.verticalNegativePadding(
+    extraPadding: Dp,
+) = layout { measurable, constraints ->
+    require(constraints.hasBoundedHeight)
+    val topAndBottomPadding = (extraPadding * 2).roundToPx()
+    val placeable = measurable.measure(
+        constraints.copy(
+            minHeight = constraints.minHeight + topAndBottomPadding,
+            maxHeight = constraints.maxHeight + topAndBottomPadding
+        )
+    )
+
+    layout(placeable.measuredWidth, constraints.maxHeight) {
+        placeable.place(0, -extraPadding.roundToPx())
+    }
+}
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnMeasure.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnMeasure.kt
new file mode 100644
index 0000000..40e7a6a
--- /dev/null
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnMeasure.kt
@@ -0,0 +1,517 @@
+/*
+ * 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.wear.compose.foundation.lazy
+
+import androidx.annotation.RestrictTo
+import androidx.compose.animation.core.Easing
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.lazy.LazyListItemInfo
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.util.lerp
+import kotlin.math.min
+import kotlin.math.roundToInt
+
+/**
+ * Parameters to control the scaling of the contents of a [ScalingLazyColumn].
+ *
+ * Items in the ScalingLazyColumn have scaling and alpha effects applied to them depending on
+ * their position in the viewport. The closer to the edge (top or bottom) of the viewport that
+ * they are the greater the down scaling and transparency that is applied. Note that scaling and
+ * transparency effects are applied from the center of the viewport (nearest to full size and normal
+ * transparency) towards the edge (items can be smaller and more transparent).
+ *
+ * Deciding how much scaling and alpha to apply is based on the position and size of the item
+ * and on a series of properties that are used to determine the transition area for each item.
+ *
+ * The transition area is defined by the edge of the screen and a transition line which is
+ * calculated for each item in the list. There are two symmetrical transition lines/areas one at the
+ * top of the viewport and one at the bottom.
+ *
+ * The items transition line is based upon its size with the potential for larger list items to
+ * start their transition earlier (further from the edge they are transitioning towards) than
+ * smaller items.
+ *
+ * It is possible for the transition line to be closer to the edge that the list item is moving away
+ * from, i.e. the opposite side of the center line of the viewport. This may seem counter-intuitive
+ * at first as it means that the transition lines can appear inverted. But as the two transition
+ * lines interact with the opposite edges of the list item top with bottom, bottom with top it is
+ * often desirable to have inverted transition lines for large list items.
+ *
+ * [minTransitionArea] and [maxTransitionArea] are both in the range [0f..1f] and are
+ * the fraction of the distance between the edges of the viewport. E.g. a value of 0.2f for
+ * minTransitionArea and 0.75f for maxTransitionArea determines that all transition lines will fall
+ * between 1/5th (20%) and 3/4s (75%) of the height of the viewport.
+ *
+ * The size of the each item is used to determine where within the transition area range
+ * minTransitionArea..maxTransitionArea the actual transition line will be. [minElementHeight]
+ * and [maxElementHeight] are used along with the item height (as a fraction of the viewport
+ * height in the range [0f..1f]) to find the transition line. So if the items size is 0.25f
+ * (25%) of way between minElementSize..maxElementSize then the transition line will be 0.25f
+ * (25%) of the way between minTransitionArea..maxTransitionArea.
+ *
+ * A list item smaller than minElementHeight is rounded up to minElementHeight and larger than
+ * maxElementHeight is rounded down to maxElementHeight. Whereabouts the items height sits
+ * between minElementHeight..maxElementHeight is then used to determine where the transition
+ * line sits between minTransitionArea..maxTransition area.
+ *
+ * If an item is smaller than or equal to minElementSize its transition line with be at
+ * minTransitionArea and if it is larger than or equal to maxElementSize its transition line
+ * will be  at maxTransitionArea.
+ *
+ * For example, if we take the default values for minTransitionArea = 0.2f and
+ * maxTransitionArea = 0.6f and minElementSize = 0.2f and maxElementSize= 0.8f then an item
+ * with a height of 0.4f (40%) of the viewport height is one third of way between
+ * minElementSize and maxElementSize, (0.4f - 0.2f) / (0.8f - 0.2f) = 0.33f. So its transition
+ * line would be one third of way between 0.2f and 0.6f, transition line = 0.2f + (0.6f -
+ * 0.2f) * 0.33f = 0.33f.
+ *
+ * Once the position of the transition line is established we now have a transition area
+ * for the item, e.g. in the example above the item will start/finish its transitions when it
+ * is 0.33f (33%) of the distance from the edge of the viewport and will start/finish its
+ * transitions at the viewport edge.
+ *
+ * The scaleInterpolator is used to determine how much of the scaling and alpha to apply
+ * as the item transits through the transition area.
+ *
+ * The edge of the item furthest from the edge of the screen is used as a scaling trigger
+ * point for each item.
+ */
+@Stable
+public interface ScalingParams {
+    /**
+     * What fraction of the full size of the item to scale it by when most
+     * scaled, e.g. at the edge of the viewport. A value between [0f,1f], so a value of 0.2f
+     * means to scale an item to 20% of its normal size.
+     */
+//    @FloatRange(
+//        fromInclusive = true, from = 0.0, toInclusive = true, to = 1.0
+//    )
+    val edgeScale: Float
+
+    /**
+     * What fraction of the full transparency of the item to draw it with
+     * when closest to the edge of the screen. A value between [0f,1f], so a value of
+     * 0.2f means to set the alpha of an item to 20% of its normal value.
+     */
+//    @FloatRange(
+//        fromInclusive = true, from = 0.0, toInclusive = true, to = 1.0
+//    )
+    val edgeAlpha: Float
+
+    /**
+     * The maximum element height as a ratio of the viewport size to use
+     * for determining the transition point within ([minTransitionArea], [maxTransitionArea])
+     * that a given content item will start to be transitioned. Items larger than [maxElementHeight]
+     * will be treated as if [maxElementHeight]. Must be greater than or equal to
+     * [minElementHeight].
+     */
+//    @FloatRange(
+//        fromInclusive = true, from = 0.0, toInclusive = true, to = 1.0
+//    )
+    val minElementHeight: Float
+
+    /**
+     * The maximum element height as a ratio of the viewport size to use
+     * for determining the transition point within ([minTransitionArea], [maxTransitionArea])
+     * that a given content item will start to be transitioned. Items larger than [maxElementHeight]
+     * will be treated as if [maxElementHeight]. Must be greater than or equal to
+     * [minElementHeight].
+     */
+//    @FloatRange(
+//        fromInclusive = true, from = 0.0, toInclusive = true, to = 1.0
+//    )
+    val maxElementHeight: Float
+
+    /**
+     * The lower bound of the transition line area, closest to the edge of the viewport. Defined as
+     * a fraction (value between 0f..1f) of the distance between the viewport edges. Must be less
+     * than or equal to [maxTransitionArea].
+     *
+     * For transition lines a value of 0f means that the transition line is at the viewport edge,
+     * e.g. no transition, a value of 0.25f means that the transition line is 25% of the screen size
+     * away from the viewport edge. A value of .5f or greater will place the transition line in the
+     * other half of the screen to the edge that the item is transitioning towards.
+     *
+     * [minTransitionArea] and [maxTransitionArea] provide an area in which transition lines for all
+     * list items exist. Depending on the size of the list item the specific point in the area is
+     * calculated.
+     */
+//    @FloatRange(
+//        fromInclusive = true, from = 0.0, toInclusive = true, to = 1.0
+//    )
+    val minTransitionArea: Float
+
+    /**
+     * The upper bound of the transition line area, closest to the
+     * center of the viewport. The fraction (value between 0f..1f) of the distance
+     * between the viewport edges. Must be greater than or equal to [minTransitionArea].
+     *
+     * For transition lines a value of 0f means that the transition line is at the viewport edge,
+     * e.g. no transition, a value of 0.25f means that the transition line is 25% of the screen size
+     * away from the viewport edge. A value of .5f or greater will place the transition line in the
+     * other half of the screen to the edge that the item is transitioning towards.
+     *
+     * [minTransitionArea] and [maxTransitionArea] provide an area in which transition lines for all
+     * list items exist. Depending on the size of the list item the specific point in the area is
+     * calculated.
+     */
+//    @FloatRange(
+//        fromInclusive = true, from = 0.0, toInclusive = true, to = 1.0
+//    )
+    val maxTransitionArea: Float
+
+    /**
+     * An interpolator to use to determine how to apply scaling as a item transitions across the
+     * scaling transition area.
+     */
+    val scaleInterpolator: Easing
+
+    /**
+     * The additional padding to consider above and below the
+     * viewport of a [ScalingLazyColumn] when considering which items to draw in the viewport. If
+     * set to 0 then no additional padding will be provided and only the items which would appear
+     * in the viewport before any scaling is applied will be considered for drawing, this may
+     * leave blank space at the top and bottom of the viewport where the next available item
+     * could have been drawn once other items have been scaled down in size. The larger this
+     * value is set to will allow for more content items to be considered for drawing in the
+     * viewport, however there is a performance cost associated with materializing items that are
+     * subsequently not drawn. The higher/more extreme the scaling parameters that are applied to
+     * the [ScalingLazyColumn] the more padding may be needed to ensure there are always enough
+     * content items available to be rendered. By default will be 20% of the maxHeight of the
+     * viewport above and below the content.
+     *
+     * @param viewportConstraints the viewports constraints
+     */
+    public fun resolveViewportVerticalOffset(viewportConstraints: Constraints): Int
+}
+
+@Stable
+internal class DefaultScalingParams(
+    override val edgeScale: Float,
+    override val edgeAlpha: Float,
+    override val minElementHeight: Float,
+    override val maxElementHeight: Float,
+    override val minTransitionArea: Float,
+    override val maxTransitionArea: Float,
+    override val scaleInterpolator: Easing,
+    val viewportVerticalOffsetResolver: (Constraints) -> Int,
+) : ScalingParams {
+
+    init {
+        check(
+            minElementHeight <= maxElementHeight
+        ) { "minElementHeight must be less than or equal to maxElementHeight" }
+        check(
+            minTransitionArea <= maxTransitionArea
+        ) { "minTransitionArea must be less than or equal to maxTransitionArea" }
+    }
+
+    override fun resolveViewportVerticalOffset(viewportConstraints: Constraints): Int {
+        return viewportVerticalOffsetResolver(viewportConstraints)
+    }
+
+    override fun toString(): String {
+        return "ScalingParams(edgeScale=$edgeScale, edgeAlpha=$edgeAlpha, " +
+            "minElementHeight=$minElementHeight, maxElementHeight=$maxElementHeight, " +
+            "minTransitionArea=$minTransitionArea, maxTransitionArea=$maxTransitionArea)"
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null) return false
+        if (this::class != other::class) return false
+
+        other as DefaultScalingParams
+
+        if (edgeScale != other.edgeScale) return false
+        if (edgeAlpha != other.edgeAlpha) return false
+        if (minElementHeight != other.minElementHeight) return false
+        if (maxElementHeight != other.maxElementHeight) return false
+        if (minTransitionArea != other.minTransitionArea) return false
+        if (maxTransitionArea != other.maxTransitionArea) return false
+        if (scaleInterpolator != other.scaleInterpolator) return false
+        if (viewportVerticalOffsetResolver != other.viewportVerticalOffsetResolver) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = edgeScale.hashCode()
+        result = 31 * result + edgeAlpha.hashCode()
+        result = 31 * result + minElementHeight.hashCode()
+        result = 31 * result + maxElementHeight.hashCode()
+        result = 31 * result + minTransitionArea.hashCode()
+        result = 31 * result + maxTransitionArea.hashCode()
+        result = 31 * result + scaleInterpolator.hashCode()
+        result = 31 * result + viewportVerticalOffsetResolver.hashCode()
+        return result
+    }
+}
+
+/**
+ * Calculate the scale and alpha to apply for an item based on the start and end position of the
+ * component's viewport in pixels and top and bottom position of the item, also in pixels.
+ *
+ * Firstly work out if the component is above or below the viewport's center-line which determines
+ * whether the item's top or bottom will be used as the trigger for scaling/alpha.
+ *
+ * Uses the scalingParams to determine where the scaling transition line is for the component.
+ *
+ * Then determines if the component is inside the scaling area, and if so what scaling/alpha effects
+ * to apply.
+ *
+ * @param viewPortStartPx The start position of the component's viewport in pixels
+ * @param viewPortEndPx The end position of the component's viewport in pixels
+ * @param itemTopPx The top of the content item in pixels.
+ * @param itemBottomPx The bottom of the content item in pixels.
+ * @param scalingParams The parameters that determine where the item's scaling transition line
+ * is, how scaling and transparency to apply.
+ */
+internal fun calculateScaleAndAlpha(
+    viewPortStartPx: Int,
+    viewPortEndPx: Int,
+    itemTopPx: Int,
+    itemBottomPx: Int,
+    scalingParams: ScalingParams,
+): ScaleAndAlpha {
+    var scaleToApply = 1.0f
+    var alphaToApply = 1.0f
+
+    val itemHeightPx = (itemBottomPx - itemTopPx).toFloat()
+    val viewPortHeightPx = (viewPortEndPx - viewPortStartPx).toFloat()
+
+    if (viewPortHeightPx > 0) {
+        val itemEdgeDistanceFromViewPortEdge =
+            min(viewPortEndPx - itemTopPx, itemBottomPx - viewPortStartPx)
+        val itemEdgeAsFractionOfViewPort = itemEdgeDistanceFromViewPortEdge / viewPortHeightPx
+        val heightAsFractionOfViewPort = itemHeightPx / viewPortHeightPx
+
+        // Work out the scaling line based on size, this is a value between 0.0..1.0
+        val sizeRatio =
+            inverseLerp(scalingParams.minElementHeight, scalingParams.maxElementHeight,
+                heightAsFractionOfViewPort)
+
+        val scalingLineAsFractionOfViewPort =
+            lerp(scalingParams.minTransitionArea, scalingParams.maxTransitionArea, sizeRatio)
+
+        if (itemEdgeAsFractionOfViewPort < scalingLineAsFractionOfViewPort) {
+            // We are scaling
+            val scalingProgressRaw = 1f - itemEdgeAsFractionOfViewPort /
+                scalingLineAsFractionOfViewPort
+            val scalingInterpolated = scalingParams.scaleInterpolator.transform(scalingProgressRaw)
+
+            scaleToApply = lerp(1.0f, scalingParams.edgeScale, scalingInterpolated)
+            alphaToApply = lerp(1.0f, scalingParams.edgeAlpha, scalingInterpolated)
+        }
+    }
+    return ScaleAndAlpha(scaleToApply, alphaToApply)
+}
+
+/**
+ * Create a [ScalingLazyListItemInfo] given an unscaled start and end position for an item.
+ *
+ * @param itemStart the x-axis position of a list item. The x-axis position takes into account
+ * any adjustment to the original position based on the scaling of other list items.
+ * @param item the original item info used to provide the pre-scaling position and size
+ * information for the item.
+ * @param verticalAdjustment the amount of vertical adjustment to apply to item positions to
+ * allow for content padding in order to determine the adjusted position of the item within the
+ * viewport in order to correctly calculate the scaling to apply.
+ * @param viewportHeightPx the height of the viewport in pixels
+ * @param viewportCenterLinePx the center line of the viewport in pixels
+ * @param scalingParams the scaling params to use for determining the scaled size of the item
+ * @param beforeContentPaddingPx the number of pixels of padding before the first item
+ * @param anchorType the type of pivot to use for the center item when calculating position and
+ * offset
+ * @param visible a flag to determine whether the list items should be visible or transparent.
+ * Items are normally visible, but can be drawn transparently when the list is not yet initialized,
+ * unless we are in preview (LocalInspectionModel) mode.
+ */
+internal fun calculateItemInfo(
+    itemStart: Int,
+    item: LazyListItemInfo,
+    verticalAdjustment: Int,
+    viewportHeightPx: Int,
+    viewportCenterLinePx: Int,
+    scalingParams: ScalingParams,
+    beforeContentPaddingPx: Int,
+    anchorType: ScalingLazyListAnchorType,
+    autoCentering: AutoCenteringParams?,
+    visible: Boolean
+): ScalingLazyListItemInfo {
+    val adjustedItemStart = itemStart - verticalAdjustment
+    val adjustedItemEnd = itemStart + item.size - verticalAdjustment
+
+    val scaleAndAlpha = calculateScaleAndAlpha(
+        viewPortStartPx = 0, viewPortEndPx = viewportHeightPx, itemTopPx = adjustedItemStart,
+        itemBottomPx = adjustedItemEnd, scalingParams = scalingParams
+    )
+
+    val isAboveLine = (adjustedItemEnd + adjustedItemStart) < viewportHeightPx
+    val scaledHeight = (item.size * scaleAndAlpha.scale).roundToInt()
+    val scaledItemTop = if (!isAboveLine) {
+        itemStart
+    } else {
+        itemStart + item.size - scaledHeight
+    }
+
+    val offset = convertToCenterOffset(
+        anchorType = anchorType,
+        itemScrollOffset = scaledItemTop,
+        viewportCenterLinePx = viewportCenterLinePx,
+        beforeContentPaddingInPx = beforeContentPaddingPx,
+        itemSizeInPx = scaledHeight
+    )
+    val unadjustedOffset = convertToCenterOffset(
+        anchorType = anchorType,
+        itemScrollOffset = item.offset,
+        viewportCenterLinePx = viewportCenterLinePx,
+        beforeContentPaddingInPx = beforeContentPaddingPx,
+        itemSizeInPx = item.size
+    )
+    return DefaultScalingLazyListItemInfo(
+        // Adjust index to take into account the Spacer before the first list item
+        index = if (autoCentering != null) item.index - 1 else item.index,
+        key = item.key,
+        unadjustedOffset = unadjustedOffset,
+        offset = offset,
+        size = scaledHeight,
+        scale = scaleAndAlpha.scale,
+        alpha = if (visible) scaleAndAlpha.alpha else 0f,
+        unadjustedSize = item.size
+    )
+}
+
+internal class DefaultScalingLazyListLayoutInfo(
+    internal val internalVisibleItemsInfo: List<ScalingLazyListItemInfo>,
+    override val viewportStartOffset: Int,
+    override val viewportEndOffset: Int,
+    override val totalItemsCount: Int,
+    val centerItemIndex: Int,
+    val centerItemScrollOffset: Int,
+    override val reverseLayout: Boolean,
+    override val orientation: Orientation,
+    override val viewportSize: IntSize,
+    override val beforeContentPadding: Int,
+    override val afterContentPadding: Int,
+    override val beforeAutoCenteringPadding: Int,
+    override val afterAutoCenteringPadding: Int,
+    // Flag to indicate that we are ready to handle scrolling as the list becomes visible. This is
+    // used to either move to the initialCenterItemIndex/Offset or complete any
+    // scrollTo/animatedScrollTo calls that were incomplete due to the component not being visible.
+    internal val readyForInitialScroll: Boolean,
+    // Flag to indicate that initialization is complete and initial scroll index and offset have
+    // been set.
+    internal val initialized: Boolean,
+    override val anchorType: ScalingLazyListAnchorType,
+) : ScalingLazyListLayoutInfo {
+    override val visibleItemsInfo: List<ScalingLazyListItemInfo>
+        // Do not report visible items until initialization is complete and the items are
+        // actually visible and correctly positioned.
+        get() = if (initialized) internalVisibleItemsInfo else emptyList()
+}
+
+internal class DefaultScalingLazyListItemInfo(
+    override val index: Int,
+    override val key: Any,
+    override val unadjustedOffset: Int,
+    override val offset: Int,
+    override val size: Int,
+    override val scale: Float,
+    override val alpha: Float,
+    override val unadjustedSize: Int
+) : ScalingLazyListItemInfo {
+    override fun toString(): String {
+        return "DefaultScalingLazyListItemInfo(index=$index, key=$key, " +
+            "unadjustedOffset=$unadjustedOffset, offset=$offset, size=$size, " +
+            "unadjustedSize=$unadjustedSize, scale=$scale, alpha=$alpha)"
+    }
+}
+
+/** @hide **/
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Immutable
+public data class ScaleAndAlpha(
+    val scale: Float,
+    val alpha: Float
+
+) {
+    companion object {
+        internal val noScaling = ScaleAndAlpha(1.0f, 1.0f)
+    }
+}
+
+/**
+ * Calculate the offset from the viewport center line of the Start|Center of an items unadjusted
+ * or scaled size. The for items with an height that is an odd number and that have
+ * ScalingLazyListAnchorType.Center the offset will be rounded down. E.g. An item which is 19 pixels
+ * in height will have a center offset of 9 pixes.
+ *
+ * @param anchorType the anchor type of the ScalingLazyColumn
+ * @param itemScrollOffset the LazyListItemInfo offset of the item
+ * @param viewportCenterLinePx the value to use as the center line of the viewport
+ * @param beforeContentPaddingInPx any content padding that has been applied before the contents
+ * @param itemSizeInPx the size of the item
+ */
+internal fun convertToCenterOffset(
+    anchorType: ScalingLazyListAnchorType,
+    itemScrollOffset: Int,
+    viewportCenterLinePx: Int,
+    beforeContentPaddingInPx: Int,
+    itemSizeInPx: Int
+): Int {
+    return itemScrollOffset - viewportCenterLinePx + beforeContentPaddingInPx +
+        if (anchorType == ScalingLazyListAnchorType.ItemStart) {
+            0
+        } else {
+            itemSizeInPx / 2
+        }
+}
+
+/**
+ * Find the start offset of the list item w.r.t. the
+ */
+internal fun ScalingLazyListItemInfo.startOffset(anchorType: ScalingLazyListAnchorType) =
+    offset - if (anchorType == ScalingLazyListAnchorType.ItemCenter) {
+        (size / 2f)
+    } else {
+        0f
+    }
+
+/**
+ * Find the start position of the list item from its unadjusted offset w.r.t. the ScalingLazyColumn
+ * center of viewport offset = 0 coordinate model.
+ */
+internal fun ScalingLazyListItemInfo.unadjustedStartOffset(anchorType: ScalingLazyListAnchorType) =
+    unadjustedOffset - if (anchorType == ScalingLazyListAnchorType.ItemCenter) {
+        (unadjustedSize / 2f)
+    } else {
+        0f
+    }
+
+/**
+ * Inverse linearly interpolate, return what fraction (0f..1f) that [value] is between [start] and
+ * [stop]. Returns 0f if value =< start and 1f if value >= stop.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public fun inverseLerp(start: Float, stop: Float, value: Float): Float {
+    return ((value - start) / (stop - start)).coerceIn(0f, 1f)
+}
\ No newline at end of file
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnSnapFlingBehavior.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnSnapFlingBehavior.kt
new file mode 100644
index 0000000..0d57a82
--- /dev/null
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnSnapFlingBehavior.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.foundation.lazy
+
+import androidx.compose.animation.core.AnimationState
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.animateDecay
+import androidx.compose.animation.core.animateTo
+import androidx.compose.animation.core.calculateTargetValue
+import androidx.compose.animation.core.exponentialDecay
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.ui.util.lerp
+import kotlin.math.abs
+import kotlin.math.roundToInt
+import kotlin.math.sqrt
+
+internal class ScalingLazyColumnSnapFlingBehavior(
+    val state: ScalingLazyListState,
+    val snapOffset: Int = 0,
+    val decay: DecayAnimationSpec<Float> = exponentialDecay()
+) : FlingBehavior {
+
+    override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
+        val animationState = AnimationState(
+            initialValue = 0f,
+            initialVelocity = initialVelocity,
+        )
+
+        var lastValue = 0f
+        val visibleItemsInfo = state.layoutInfo.visibleItemsInfo
+        val isAFling = abs(initialVelocity) > 1f && visibleItemsInfo.size > 1
+        val finalTarget = if (isAFling) {
+            // Target we will land on given initialVelocity & decay
+            val decayTarget = decay.calculateTargetValue(0f, initialVelocity)
+            var endOfListReached = false
+
+            animationState.animateDecay(decay) {
+                val delta = value - lastValue
+                val consumed = scrollBy(delta)
+                lastValue = value
+
+                // When we are "slow" enough, switch from decay to the final snap.
+                if (abs(velocity) < SNAP_SPEED_THRESHOLD) cancelAnimation()
+
+                // If we can't consume the scroll, also stop.
+                if (abs(delta - consumed) > 0.1f) {
+                    endOfListReached = true
+                    cancelAnimation()
+                }
+            }
+
+            if (endOfListReached) {
+                // We couldn't scroll as much as we wanted, likely we reached the end of the list,
+                // Snap to the current item and finish.
+                scrollBy((snapOffset - state.centerItemScrollOffset).toFloat())
+                return animationState.velocity
+            } else {
+                // Now that scrolling slowed down, adjust the animation to land in the item closest
+                // to the original target. Note that the target may be off-screen, in that case we
+                // will land on the last visible item in that direction.
+                (state.layoutInfo.visibleItemsInfo
+                    .map { animationState.value + it.unadjustedOffset + snapOffset }
+                    .minByOrNull { abs(it - decayTarget) } ?: decayTarget)
+            }
+        } else {
+            // Not a fling, just snap to the current item.
+            (snapOffset - state.centerItemScrollOffset).toFloat()
+        }
+
+        // We have a velocity (animationState.velocity), and a target (finalTarget),
+        // Construct a cubic bezier with the given initial velocity, and ending at 0 speed,
+        // unless that will mean that we need to accelerate and decelerate.
+        // We can also control the inertia of these speeds, i.e. how much it will accelerate/
+        // decelerate at the beginning and end.
+        val distance = finalTarget - animationState.value
+
+        // If the distance to fling is zero, nothing to do (and must avoid divide-by-zero below).
+        if (distance != 0.0f) {
+            val initialSpeed = animationState.velocity
+
+            // Inertia of the initial speed.
+            val initialInertia = 0.5f
+
+            // Compute how much time we want to spend on the final snap, depending on the speed
+            val finalSnapDuration = lerp(
+                FINAL_SNAP_DURATION_MIN, FINAL_SNAP_DURATION_MAX,
+                abs(initialSpeed) / SNAP_SPEED_THRESHOLD
+            )
+
+            // Initial control point. Has slope (velocity) adjustedSpeed and magnitude (inertia)
+            // initialInertia
+            val adjustedSpeed = initialSpeed * finalSnapDuration / distance
+            val easingX0 = initialInertia / sqrt(1f + adjustedSpeed * adjustedSpeed)
+            val easingY0 = easingX0 * adjustedSpeed
+
+            // Final control point. Has slope 0, unless that will make us accelerate then decelerate,
+            // in that case we set the slope to 1.
+            val easingX1 = 0.8f
+            val easingY1 = if (easingX0 > easingY0) 0.8f else 1f
+
+            animationState.animateTo(
+                finalTarget, tween(
+                    (finalSnapDuration * 1000).roundToInt(),
+                    easing = CubicBezierEasing(easingX0, easingY0, easingX1, easingY1)
+                )
+            ) {
+                scrollBy(value - lastValue)
+                lastValue = value
+            }
+        }
+
+        return animationState.velocity
+    }
+
+    // Speed, in pixels per second, to switch between decay and final snap.
+    private val SNAP_SPEED_THRESHOLD = 1200
+
+    // Minimum duration for the final snap after the fling, in seconds, used when the initial speed
+    // is 0
+    private val FINAL_SNAP_DURATION_MIN = .1f
+
+    // Maximum duration for the final snap after the fling, in seconds, used when the speed is
+    // SNAP_SPEED_THRESHOLD
+    private val FINAL_SNAP_DURATION_MAX = .35f
+}
\ No newline at end of file
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyListItemInfo.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyListItemInfo.kt
new file mode 100644
index 0000000..b70ee81
--- /dev/null
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyListItemInfo.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.wear.compose.foundation.lazy
+
+/**
+ * Contains useful information about an individual item in a [ScalingLazyColumn].
+ *
+ * @see ScalingLazyListLayoutInfo
+ */
+public sealed interface ScalingLazyListItemInfo {
+    /**
+     * The index of the item in the list.
+     */
+    val index: Int
+
+    /**
+     * The key of the item which was passed to the item() or items() function.
+     */
+    val key: Any
+
+    /**
+     * The main axis offset of the item before adjustment for scaling of the items in the viewport.
+     *
+     * The offset is relative to the center-line of the viewport of the lazy list container and
+     * takes the [ScalingLazyListAnchorType] into account.
+     *
+     * For [ScalingLazyListAnchorType.ItemCenter] the offset is from the center of the list item to
+     * the center-line of the viewport.
+     *
+     * For [ScalingLazyListAnchorType.ItemStart] if is the offset
+     * between the start (edge) of the item and the center-line of the viewport, for normal layout
+     * this will be the top edge of the item, for reverseLayout it will be the bottom edge.
+     */
+    val unadjustedOffset: Int
+
+    /**
+     * The main axis offset of the item after adjustment for scaling of the items in the viewport.
+     *
+     * The offset is relative to the center-line of the viewport of the lazy list container and
+     * takes the [ScalingLazyListAnchorType] into account.
+     *
+     * For [ScalingLazyListAnchorType.ItemCenter] the offset is from the center of the list item to
+     * the center-line of the viewport.
+     *
+     * For [ScalingLazyListAnchorType.ItemStart] if is the offset
+     * between the start (edge) of the item and the center-line of the viewport, for normal layout
+     * this will be the top edge of the item, for reverseLayout it will be the bottom edge.
+     *
+     * A positive value indicates that the item's anchor point is below the viewport center-line, a
+     * negative value indicates that the item anchor point is above the viewport center-line.
+     */
+    val offset: Int
+
+    /**
+     * The scaled/adjusted main axis size of the item. Note that if you emit multiple layouts in the
+     * composable slot for the item then this size will be calculated as the sum of their sizes.
+     */
+    val size: Int
+
+    /**
+     * How much scaling has been applied to the item, between 0 and 1
+     */
+    val scale: Float
+
+    /**
+     * How much alpha has been applied to the item, between 0 and 1
+     */
+    val alpha: Float
+
+    /**
+     * The original (before scaling) size of the list item
+     */
+    val unadjustedSize: Int
+}
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyListItemScope.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyListItemScope.kt
new file mode 100644
index 0000000..042e928
--- /dev/null
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyListItemScope.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.wear.compose.foundation.lazy
+
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+
+/**
+ * Receiver scope being used by the item content parameter of ScalingLazyColumn.
+ */
+@Stable
+@ScalingLazyScopeMarker
+public sealed interface ScalingLazyListItemScope {
+    /**
+     * Have the content fill the [Constraints.maxWidth] and [Constraints.maxHeight] of the parent
+     * measurement constraints by setting the [minimum width][Constraints.minWidth] to be equal to
+     * the [maximum width][Constraints.maxWidth] multiplied by [fraction] and the [minimum
+     * height][Constraints.minHeight] to be equal to the [maximum height][Constraints.maxHeight]
+     * multiplied by [fraction]. Note that, by default, the [fraction] is 1, so the modifier will
+     * make the content fill the whole available space. [fraction] must be between `0` and `1`.
+     *
+     * Regular [Modifier.fillMaxSize] can't work inside the scrolling layouts as the items are
+     * measured with [Constraints.Infinity] as the constraints for the main axis.
+     */
+    fun Modifier.fillParentMaxSize(
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        fraction: Float = 1f
+    ): Modifier
+
+    /**
+     * Have the content fill the [Constraints.maxWidth] of the parent measurement constraints
+     * by setting the [minimum width][Constraints.minWidth] to be equal to the
+     * [maximum width][Constraints.maxWidth] multiplied by [fraction]. Note that, by default, the
+     * [fraction] is 1, so the modifier will make the content fill the whole parent width.
+     * [fraction] must be between `0` and `1`.
+     *
+     * Regular [Modifier.fillMaxWidth] can't work inside the scrolling horizontally layouts as the
+     * items are measured with [Constraints.Infinity] as the constraints for the main axis.
+     */
+    fun Modifier.fillParentMaxWidth(
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        fraction: Float = 1f
+    ): Modifier
+
+    /**
+     * Have the content fill the [Constraints.maxHeight] of the incoming measurement constraints
+     * by setting the [minimum height][Constraints.minHeight] to be equal to the
+     * [maximum height][Constraints.maxHeight] multiplied by [fraction]. Note that, by default, the
+     * [fraction] is 1, so the modifier will make the content fill the whole parent height.
+     * [fraction] must be between `0` and `1`.
+     *
+     * Regular [Modifier.fillMaxHeight] can't work inside the scrolling vertically layouts as the
+     * items are measured with [Constraints.Infinity] as the constraints for the main axis.
+     */
+    fun Modifier.fillParentMaxHeight(
+        /*@FloatRange(from = 0.0, to = 1.0)*/
+        fraction: Float = 1f
+    ): Modifier
+}
+
+internal data class ScalingLazyListItemScopeImpl(
+    val density: Density,
+    val constraints: Constraints
+) : ScalingLazyListItemScope {
+    private val maxWidth: Dp = with(density) { constraints.maxWidth.toDp() }
+    private val maxHeight: Dp = with(density) { constraints.maxHeight.toDp() }
+
+    override fun Modifier.fillParentMaxSize(fraction: Float) = size(
+        maxWidth * fraction,
+        maxHeight * fraction
+    )
+
+    override fun Modifier.fillParentMaxWidth(fraction: Float) =
+        width(maxWidth * fraction)
+
+    override fun Modifier.fillParentMaxHeight(fraction: Float) =
+        height(maxHeight * fraction)
+}
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyListLayoutInfo.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyListLayoutInfo.kt
new file mode 100644
index 0000000..dc2ccd6
--- /dev/null
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyListLayoutInfo.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.wear.compose.foundation.lazy
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.IntSize
+
+/**
+ * Contains useful information about the currently displayed layout state of [ScalingLazyColumn].
+ * For example you can get the list of currently displayed item.
+ *
+ * Use [ScalingLazyListState.layoutInfo] to retrieve this
+ */
+public sealed interface ScalingLazyListLayoutInfo {
+    /**
+     * The list of [ScalingLazyListItemInfo] representing all the currently visible items.
+     */
+    val visibleItemsInfo: List<ScalingLazyListItemInfo>
+
+    /**
+     * The start offset of the layout's viewport in pixels. You can think of it as a minimum offset
+     * which would be visible. Usually it is 0, but it can be negative if non-zero
+     * [beforeContentPadding] was applied as the content displayed in the content padding area is
+     * still visible.
+     *
+     * You can use it to understand what items from [visibleItemsInfo] are fully visible.
+     */
+    val viewportStartOffset: Int
+
+    /**
+     * The end offset of the layout's viewport in pixels. You can think of it as a maximum offset
+     * which would be visible. It is the size of the scaling lazy list layout minus
+     * [beforeContentPadding].
+     *
+     * You can use it to understand what items from [visibleItemsInfo] are fully visible.
+     */
+    val viewportEndOffset: Int
+
+    /**
+     * The total count of items passed to [ScalingLazyColumn].
+     */
+    val totalItemsCount: Int
+
+    /**
+     * The size of the viewport in pixels. It is the scaling lazy list layout size including all the
+     * content paddings.
+     */
+    val viewportSize: IntSize
+
+    /**
+     * The orientation of the scaling lazy list.
+     */
+    val orientation: Orientation
+
+    /**
+     * True if the direction of scrolling and layout is reversed.
+     */
+    val reverseLayout: Boolean
+
+    /**
+     * The content padding in pixels applied before the first item in the direction of scrolling.
+     * For example it is a top content padding for ScalingLazyColumn with reverseLayout set to
+     * false.
+     */
+    val beforeContentPadding: Int
+
+    /**
+     * The content padding in pixels applied after the last item in the direction of scrolling.
+     * For example it is a bottom content padding for ScalingLazyColumn with reverseLayout set to
+     * false.
+     */
+    val afterContentPadding: Int
+
+    /**
+     * The auto centering padding in pixels applied before the first item in the direction of
+     * scrolling. For example it is a top auto centering padding for ScalingLazyColumn with
+     * reverseLayout set to false.
+     */
+    val beforeAutoCenteringPadding: Int
+
+    /**
+     * The auto centering padding in pixels applied after the last item in the direction of
+     * scrolling. For example it is a bottom auto centering padding for ScalingLazyColumn with
+     * reverseLayout set to false.
+     */
+    val afterAutoCenteringPadding: Int
+
+    /**
+     * How to anchor list items to the center-line of the viewport
+     */
+    val anchorType: ScalingLazyListAnchorType
+}
\ No newline at end of file
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyListState.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyListState.kt
new file mode 100644
index 0000000..9241dd6
--- /dev/null
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyListState.kt
@@ -0,0 +1,713 @@
+/*
+ * 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.wear.compose.foundation.lazy
+
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.lazy.LazyListItemInfo
+import androidx.compose.foundation.lazy.LazyListLayoutInfo
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.unit.IntSize
+import kotlin.math.roundToInt
+
+/**
+ * Creates a [ScalingLazyListState] that is remembered across compositions.
+ *
+ * @param initialCenterItemIndex the initial value for [ScalingLazyListState.centerItemIndex],
+ * defaults to 1. This will place the 2nd list item (index == 1) in the center of the viewport and
+ * the first item (index == 0) before it.
+ *
+ * @param initialCenterItemScrollOffset the initial value for
+ * [ScalingLazyListState.centerItemScrollOffset] in pixels
+ */
+@Composable
+public fun rememberScalingLazyListState(
+    initialCenterItemIndex: Int = 1,
+    initialCenterItemScrollOffset: Int = 0
+): ScalingLazyListState {
+    return rememberSaveable(saver = ScalingLazyListState.Saver) {
+        ScalingLazyListState(
+            initialCenterItemIndex,
+            initialCenterItemScrollOffset
+        )
+    }
+}
+
+/**
+ * A state object that can be hoisted to control and observe scrolling.
+ *
+ * In most cases, this will be created via [rememberScalingLazyListState].
+ *
+ * @param initialCenterItemIndex the initial value for [ScalingLazyListState.centerItemIndex],
+ * defaults to 1. This will place the 2nd list item (index == 1) in the center of the viewport and
+ * the first item (index == 0) before it.
+ *
+ * If the developer wants custom control over position and spacing they can switch off autoCentering
+ * and provide contentPadding.
+ *
+ * @param initialCenterItemScrollOffset the initial value for
+ * [ScalingLazyListState.centerItemScrollOffset]
+ *
+ * Note that it is not always possible for the values provided by [initialCenterItemIndex] and
+ * [initialCenterItemScrollOffset] to be honored, e.g. If [initialCenterItemIndex] is set to a value
+ * larger than the number of items initially in the list, or to an index that can not be placed in
+ * the middle of the screen due to the contentPadding or autoCentering properties provided to the
+ * [ScalingLazyColumn]. After the [ScalingLazyColumn] is initially drawn the actual values for the
+ * [centerItemIndex] and [centerItemScrollOffset] can be read from the state.
+ */
+@Stable
+class ScalingLazyListState constructor(
+    private var initialCenterItemIndex: Int = 1,
+    private var initialCenterItemScrollOffset: Int = 0
+) : ScrollableState {
+
+    internal var lazyListState: LazyListState = LazyListState(0, 0)
+    internal val extraPaddingPx = mutableStateOf<Int?>(null)
+    internal val beforeContentPaddingPx = mutableStateOf<Int?>(null)
+    internal val afterContentPaddingPx = mutableStateOf<Int?>(null)
+    internal val scalingParams = mutableStateOf<ScalingParams?>(null)
+    internal val gapBetweenItemsPx = mutableStateOf<Int?>(null)
+    internal val viewportHeightPx = mutableStateOf<Int?>(null)
+    internal val reverseLayout = mutableStateOf<Boolean?>(null)
+    internal val anchorType = mutableStateOf<ScalingLazyListAnchorType?>(null)
+    internal val autoCentering = mutableStateOf<AutoCenteringParams?>(null)
+    internal val initialized = mutableStateOf<Boolean>(false)
+    internal val localInspectionMode = mutableStateOf<Boolean>(false)
+
+    // The following three are used together when there is a post-initialization incomplete scroll
+    // to finish next time the ScalingLazyColumn is visible
+    private val incompleteScrollItem = mutableStateOf<Int?>(null)
+    private val incompleteScrollOffset = mutableStateOf<Int?>(null)
+    private val incompleteScrollAnimated = mutableStateOf(false)
+
+    private val _centerItemIndex = derivedStateOf {
+        (layoutInfo as? DefaultScalingLazyListLayoutInfo)?.let {
+            if (it.initialized) it.centerItemIndex else null
+        } ?: initialCenterItemIndex
+    }
+
+    /**
+     * The index of the item positioned closest to the viewport center
+     */
+    public val centerItemIndex: Int
+        get() = _centerItemIndex.value
+
+    internal val topAutoCenteringItemSizePx: Int by derivedStateOf {
+        if (extraPaddingPx.value == null || scalingParams.value == null ||
+            gapBetweenItemsPx.value == null || viewportHeightPx.value == null ||
+            anchorType.value == null || reverseLayout.value == null ||
+            beforeContentPaddingPx.value == null || autoCentering.value == null ||
+            autoCentering.value == null
+        ) {
+            0
+        } else {
+            (layoutInfo.beforeAutoCenteringPadding - gapBetweenItemsPx.value!!).coerceAtLeast(0)
+        }
+    }
+
+    internal val bottomAutoCenteringItemSizePx: Int by derivedStateOf {
+        if (extraPaddingPx.value == null || scalingParams.value == null ||
+            gapBetweenItemsPx.value == null || viewportHeightPx.value == null ||
+            anchorType.value == null || reverseLayout.value == null ||
+            beforeContentPaddingPx.value == null || autoCentering.value == null ||
+            layoutInfo.internalVisibleItemInfo().isEmpty()
+        ) {
+            0
+        } else {
+            (layoutInfo.afterAutoCenteringPadding - gapBetweenItemsPx.value!!).coerceAtLeast(0)
+        }
+    }
+
+    /**
+     * The offset of the item closest to the viewport center. Depending on the
+     * [ScalingLazyListAnchorType] of the [ScalingLazyColumn] the offset will be relative to either
+     * the items Edge or Center.
+     *
+     * A positive value indicates that the center item's anchor point is above the viewport
+     * center-line, a negative value indicates that the center item anchor point is below the
+     * viewport center-line.
+     */
+    public val centerItemScrollOffset: Int
+        get() =
+            (layoutInfo as? DefaultScalingLazyListLayoutInfo)?.let {
+                if (it.initialized) it.centerItemScrollOffset else null
+            } ?: initialCenterItemScrollOffset
+
+    /**
+     * The object of [ScalingLazyListLayoutInfo] calculated during the last layout pass. For
+     * example, you can use it to calculate what items are currently visible.
+     */
+    public val layoutInfo: ScalingLazyListLayoutInfo by derivedStateOf {
+        if (extraPaddingPx.value == null || scalingParams.value == null ||
+            gapBetweenItemsPx.value == null || viewportHeightPx.value == null ||
+            anchorType.value == null || reverseLayout.value == null ||
+            beforeContentPaddingPx.value == null
+        ) {
+            EmptyScalingLazyListLayoutInfo
+        } else {
+            val visibleItemsInfo = mutableListOf<ScalingLazyListItemInfo>()
+            val viewportHeightPx = viewportHeightPx.value!!
+            var newCenterItemIndex = 0
+            var newCenterItemScrollOffset = 0
+            val visible = initialized.value || localInspectionMode.value
+
+            // The verticalAdjustment is used to allow for the extraPadding that the
+            // ScalingLazyColumn employs to ensure that there are sufficient list items composed
+            // by the underlying LazyList even when there is extreme scaling being applied that
+            // could result in additional list items be eligible to be drawn.
+            // It is important to adjust for this extra space when working out the viewport
+            // center-line based coordinate system of the ScalingLazyList.
+            val verticalAdjustment =
+                lazyListState.layoutInfo.viewportStartOffset + extraPaddingPx.value!!
+
+            // Find the item in the middle of the viewport
+            val centralItemArrayIndex =
+                findItemNearestCenter(verticalAdjustment)
+            if (centralItemArrayIndex != null) {
+                val originalVisibleItemsInfo = lazyListState.layoutInfo.visibleItemsInfo
+                val centralItem = originalVisibleItemsInfo[centralItemArrayIndex]
+
+                // Place the center item
+                val centerItemInfo: ScalingLazyListItemInfo = calculateItemInfo(
+                    centralItem.offset,
+                    centralItem,
+                    verticalAdjustment,
+                    viewportHeightPx,
+                    viewportCenterLinePx(),
+                    scalingParams.value!!,
+                    beforeContentPaddingPx.value!!,
+                    anchorType.value!!,
+                    autoCentering.value,
+                    visible
+                )
+                visibleItemsInfo.add(
+                    centerItemInfo
+                )
+
+                newCenterItemIndex = centerItemInfo.index
+                newCenterItemScrollOffset = -centerItemInfo.offset
+
+                // Find the adjusted position of the central item in the coordinate system of the
+                // underlying LazyColumn by adjusting for any scaling
+                val centralItemAdjustedUnderlyingOffset =
+                    centralItem.offset + ((centerItemInfo.startOffset(anchorType.value!!) -
+                        centerItemInfo.unadjustedStartOffset(anchorType.value!!))).roundToInt()
+
+                // Go Up
+                // nextItemBottomNoPadding uses the coordinate system of the underlying LazyList. It
+                // keeps track of the top of the next potential list item that is a candidate to be
+                // drawn in the viewport as we walk up the list items from the center. Going up
+                // involved making offset smaller/negative as the coordinate system of the LazyList
+                // starts at the top of the viewport. Note that the start of the lazy list
+                // coordinates starts at '- start content padding in pixels' and goes beyond the
+                // last visible list items to include the end content padding in pixels.
+
+                // centralItem.offset is a startOffset in the coordinate system of the
+                // underlying lazy list.
+                var nextItemBottomNoPadding =
+                    centralItemAdjustedUnderlyingOffset - gapBetweenItemsPx.value!!
+
+                (centralItemArrayIndex - 1 downTo 0).forEach { ix ->
+                    if (nextItemBottomNoPadding >= verticalAdjustment) {
+                        val currentItem =
+                            lazyListState.layoutInfo.visibleItemsInfo[ix]
+                        if (!discardAutoCenteringListItem(currentItem)) {
+                            val itemInfo = calculateItemInfo(
+                                nextItemBottomNoPadding - currentItem.size,
+                                currentItem,
+                                verticalAdjustment,
+                                viewportHeightPx,
+                                viewportCenterLinePx(),
+                                scalingParams.value!!,
+                                beforeContentPaddingPx.value!!,
+                                anchorType.value!!,
+                                autoCentering.value,
+                                visible
+                            )
+                            visibleItemsInfo.add(0, itemInfo)
+                            nextItemBottomNoPadding =
+                                nextItemBottomNoPadding - itemInfo.size - gapBetweenItemsPx.value!!
+                        }
+                    } else {
+                        return@forEach
+                    }
+                }
+
+                // Go Down
+                // nextItemTopNoPadding uses the coordinate system of the underlying LazyList. It
+                // keeps track of the top of the next potential list item that is a candidate to be
+                // drawn in the viewport as we walk down the list items from the center.
+                var nextItemTopNoPadding =
+                    centralItemAdjustedUnderlyingOffset + centerItemInfo.size +
+                        gapBetweenItemsPx.value!!
+
+                (((centralItemArrayIndex + 1) until
+                    originalVisibleItemsInfo.size)).forEach { ix ->
+                    if ((nextItemTopNoPadding - viewportHeightPx) <= verticalAdjustment) {
+                        val currentItem =
+                            lazyListState.layoutInfo.visibleItemsInfo[ix]
+                        if (!discardAutoCenteringListItem(currentItem)) {
+                            val itemInfo = calculateItemInfo(
+                                nextItemTopNoPadding,
+                                currentItem,
+                                verticalAdjustment,
+                                viewportHeightPx,
+                                viewportCenterLinePx(),
+                                scalingParams.value!!,
+                                beforeContentPaddingPx.value!!,
+                                anchorType.value!!,
+                                autoCentering.value,
+                                visible
+                            )
+
+                            visibleItemsInfo.add(itemInfo)
+                            nextItemTopNoPadding += itemInfo.size + gapBetweenItemsPx.value!!
+                        }
+                    } else {
+                        return@forEach
+                    }
+                }
+            }
+            val totalItemsCount =
+                if (autoCentering.value != null) {
+                    (lazyListState.layoutInfo.totalItemsCount - 2).coerceAtLeast(0)
+                } else {
+                    lazyListState.layoutInfo.totalItemsCount
+                }
+
+            // Decide if we are ready for the 2nd stage of initialization
+            // 1. We are not yet initialized and
+
+            val readyForInitialScroll =
+                if (! initialized.value) {
+                    // 1. autoCentering is off or
+                    // 2. The list has no items or
+                    // 3. the before content autoCentering Spacer has been sized.
+                    // NOTE: It is possible, if the first real item in the list is large, that the size
+                    // of the Spacer is 0.
+                    autoCentering.value == null || (
+                        lazyListState.layoutInfo.visibleItemsInfo.size >= 2 && (
+                            // or Empty list (other than the 2 spacers)
+                            lazyListState.layoutInfo.visibleItemsInfo.size == 2 ||
+                                // or first item is correctly size
+                                topSpacerIsCorrectlySized(
+                                    lazyListState.layoutInfo.visibleItemsInfo,
+                                    lazyListState.layoutInfo.totalItemsCount
+                                )
+                            )
+                        )
+                } else {
+                    // We are already initialized and have an incomplete scroll to finish
+                    incompleteScrollItem.value != null
+                }
+
+            DefaultScalingLazyListLayoutInfo(
+                internalVisibleItemsInfo = visibleItemsInfo,
+                totalItemsCount = totalItemsCount,
+                viewportStartOffset = lazyListState.layoutInfo.viewportStartOffset +
+                    extraPaddingPx.value!!,
+                viewportEndOffset = lazyListState.layoutInfo.viewportEndOffset -
+                    extraPaddingPx.value!!,
+                centerItemIndex = if (initialized.value) newCenterItemIndex else 0,
+                centerItemScrollOffset = if (initialized.value) newCenterItemScrollOffset else 0,
+                reverseLayout = reverseLayout.value!!,
+                orientation = lazyListState.layoutInfo.orientation,
+                viewportSize = IntSize(
+                    width = lazyListState.layoutInfo.viewportSize.width,
+                    height = lazyListState.layoutInfo.viewportSize.height -
+                        extraPaddingPx.value!! * 2
+                ),
+                beforeContentPadding = beforeContentPaddingPx.value!!,
+                afterContentPadding = afterContentPaddingPx.value!!,
+                beforeAutoCenteringPadding = calculateTopAutoCenteringPaddingPx(visibleItemsInfo,
+                    totalItemsCount),
+                afterAutoCenteringPadding = calculateBottomAutoCenteringPaddingPx(visibleItemsInfo,
+                    totalItemsCount),
+                readyForInitialScroll = readyForInitialScroll,
+                initialized = initialized.value,
+                anchorType = anchorType.value!!,
+            )
+        }
+    }
+
+    private fun findItemNearestCenter(
+        verticalAdjustment: Int
+    ): Int? {
+        var result: Int? = null
+        // Find the item in the middle of the viewport
+        for (i in lazyListState.layoutInfo.visibleItemsInfo.indices) {
+            val item = lazyListState.layoutInfo.visibleItemsInfo[i]
+            if (! discardAutoCenteringListItem(item)) {
+                val rawItemStart = item.offset - verticalAdjustment
+                val rawItemEnd = rawItemStart + item.size + gapBetweenItemsPx.value!! / 2f
+                result = i
+                if (rawItemEnd > viewportCenterLinePx()) {
+                    break
+                }
+            }
+        }
+        return result
+    }
+
+    companion object {
+        /**
+         * The default [Saver] implementation for [ScalingLazyListState].
+         */
+        val Saver = listSaver<ScalingLazyListState, Int>(
+            save = {
+                listOf(
+                    it.centerItemIndex,
+                    it.centerItemScrollOffset,
+                )
+            },
+            restore = {
+                val scalingLazyColumnState = ScalingLazyListState(it[0], it[1])
+                scalingLazyColumnState
+            }
+        )
+    }
+
+    override val isScrollInProgress: Boolean
+        get() {
+            return lazyListState.isScrollInProgress
+        }
+
+    override fun dispatchRawDelta(delta: Float): Float {
+        return lazyListState.dispatchRawDelta(delta)
+    }
+
+    override suspend fun scroll(
+        scrollPriority: MutatePriority,
+        block: suspend ScrollScope.() -> Unit
+    ) {
+        lazyListState.scroll(scrollPriority = scrollPriority, block = block)
+    }
+
+    override val canScrollForward: Boolean
+        get() = lazyListState.canScrollForward
+
+    override val canScrollBackward: Boolean
+        get() = lazyListState.canScrollBackward
+    /**
+     * Instantly brings the item at [index] to the center of the viewport and positions it based on
+     * the [anchorType] and applies the [scrollOffset] pixels.
+     *
+     * @param index the index to which to scroll. Must be non-negative.
+     * @param scrollOffset the offset that the item should end up after the scroll. Note that
+     * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
+     * scroll the item further upward (taking it partly offscreen).
+     */
+    public suspend fun scrollToItem(
+        /*@IntRange(from = 0)*/
+        index: Int,
+        /*@IntRange(from = 0)*/
+        scrollOffset: Int = 0
+    ) {
+        return scrollToItem(false, index, scrollOffset)
+    }
+
+    /**
+     * Brings the item at [index] to the center of the viewport and positions it based on
+     * the [anchorType] and applies the [scrollOffset] pixels.
+     *
+     * @param animated whether to animate the scroll
+     * @param index the index to which to scroll. Must be non-negative.
+     * @param scrollOffset the offset that the item should end up after the scroll. Note that
+     * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
+     * scroll the item further upward (taking it partly offscreen).
+     */
+    internal suspend fun scrollToItem(
+        animated: Boolean,
+        /*@IntRange(from = 0)*/
+        index: Int,
+        /*@IntRange(from = 0)*/
+        scrollOffset: Int,
+    ) {
+        if (!initialized.value) {
+            // We can't scroll yet, save to do it when we can (on the first composition).
+            initialCenterItemIndex = index
+            initialCenterItemScrollOffset = scrollOffset
+            return
+        }
+
+        // Find the underlying LazyList index taking into account the Spacer added before
+        // the first ScalingLazyColumn list item when autoCentering
+        val targetIndex = index.coerceAtMost(layoutInfo.totalItemsCount)
+        val lazyListStateIndex = targetIndex + if (autoCentering.value != null) 1 else 0
+
+        val offsetToCenterOfViewport =
+            beforeContentPaddingPx.value!! - viewportCenterLinePx()
+        if (anchorType.value == ScalingLazyListAnchorType.ItemStart) {
+            val offset = offsetToCenterOfViewport + scrollOffset
+            return lazyListState.scrollToItem(animated, lazyListStateIndex, offset)
+        } else {
+            var item = lazyListState.layoutInfo.findItemInfoWithIndex(lazyListStateIndex)
+            if (item == null) {
+                val estimatedUnadjustedHeight = layoutInfo.averageUnadjustedItemSize()
+                val estimatedOffset =
+                    offsetToCenterOfViewport + (estimatedUnadjustedHeight / 2) + scrollOffset
+
+                // Scroll the item into the middle of the viewport so that we know it is visible
+                lazyListState.scrollToItem(animated, lazyListStateIndex, estimatedOffset)
+                // Now we know that the item is visible find it and fine tune our position
+                item = lazyListState.layoutInfo.findItemInfoWithIndex(lazyListStateIndex)
+            }
+            if (item != null) {
+                // Decide if the item is in the right place
+                if (centerItemIndex != index || centerItemScrollOffset != scrollOffset) {
+                    val offset = offsetToCenterOfViewport + (item.size / 2) + scrollOffset
+                    return lazyListState.scrollToItem(animated, lazyListStateIndex, offset)
+                }
+            } else {
+                // The scroll has failed - this should only happen if the list is not currently
+                // visible
+                incompleteScrollItem.value = targetIndex
+                incompleteScrollOffset.value = scrollOffset
+                incompleteScrollAnimated.value = animated
+            }
+        }
+        return
+    }
+
+    internal suspend fun scrollToInitialItem() {
+        // First time initialization
+        if (!initialized.value) {
+            initialized.value = true
+            scrollToItem(initialCenterItemIndex, initialCenterItemScrollOffset)
+        }
+        // Check whether we are becoming visible after an incomplete scrollTo/animatedScrollTo
+        if (incompleteScrollItem.value != null) {
+            val animated = incompleteScrollAnimated.value
+            val targetIndex = incompleteScrollItem.value!!
+            val targetOffset = incompleteScrollOffset.value!!
+            // Reset the incomplete scroll indicator
+            incompleteScrollItem.value = null
+            scrollToItem(animated, targetIndex, targetOffset)
+        }
+        return
+    }
+
+    /**
+     * Animate (smooth scroll) the given item at [index] to the center of the viewport and position
+     * it based on the [anchorType] and applies the [scrollOffset] pixels.
+     *
+     * @param index the index to which to scroll. Must be non-negative.
+     * @param scrollOffset the offset that the item should end up after the scroll (same as
+     * [scrollToItem]) - note that positive offset refers to forward scroll, so in a
+     * top-to-bottom list, positive offset will scroll the item further upward (taking it partly
+     * offscreen)
+     */
+    public suspend fun animateScrollToItem(
+        /*@IntRange(from = 0)*/
+        index: Int,
+        /*@IntRange(from = 0)*/
+        scrollOffset: Int = 0
+    ) {
+        return scrollToItem(true, index, scrollOffset)
+    }
+
+    private fun discardAutoCenteringListItem(item: LazyListItemInfo): Boolean =
+        autoCentering.value != null &&
+            (item.index == 0 || item.index == lazyListState.layoutInfo.totalItemsCount - 1)
+
+    /**
+     * Calculate the amount of top padding needed (if any) to make sure that the
+     * [AutoCenteringParams.itemIndex] item can be placed in the center of the viewport at
+     * [AutoCenteringParams.itemOffset]
+     */
+    private fun calculateTopAutoCenteringPaddingPx(
+        visibleItems: List<ScalingLazyListItemInfo>,
+        totalItemCount: Int
+    ): Int {
+        if (autoCentering.value == null ||
+            (visibleItems.isNotEmpty() && visibleItems.first().index != 0)) return 0
+
+        // Work out the index we want to find - if there are less items in the list than would be
+        // needed to make initialItemIndex be visible then use the last visible item
+        val itemIndexToFind = autoCentering.value!!.itemIndex.coerceAtMost(totalItemCount - 1)
+
+        // Find the initialCenterItem, if it is null that means it is not in view - therefore
+        // we have more than enough content before it to make sure it can be scrolled to the center
+        // of the viewport
+        val initialCenterItem =
+            visibleItems.find { it.index == itemIndexToFind }
+
+        // Determine how much space we actually need
+        var spaceNeeded = spaceNeeded(initialCenterItem)
+
+        if (spaceNeeded > 0f) {
+            // Now see how much content we already have
+            visibleItems.map {
+                if (it.index < itemIndexToFind) {
+                    // Reduce the space needed
+                    spaceNeeded = spaceNeeded - gapBetweenItemsPx.value!! - it.unadjustedSize
+                }
+            }
+        }
+        return (spaceNeeded + gapBetweenItemsPx.value!!).coerceAtLeast(0)
+    }
+
+    /**
+     * Determine if the top Spacer component in the underlying LazyColumn has the correct size. We
+     * need to be sure that it has the correct size before we do scrollToInitialItem in order to
+     * make sure that the initial scroll will be successful.
+     */
+    private fun topSpacerIsCorrectlySized(
+        visibleItems: List<LazyListItemInfo>,
+        totalItemCount: Int
+    ): Boolean {
+        // If the top items has a non-zero size we know that it has been correctly inflated.
+        if (lazyListState.layoutInfo.visibleItemsInfo.first().size > 0) return true
+
+        // Work out the index we want to find - if there are less items in the list than would be
+        // needed to make initialItemIndex be visible then use the last visible item. The -2 is to
+        // allow for the spacers, i.e. an underlying list of size 3 has 2 spacers in index 0 and 2
+        // and one real item in underlying lazy column index 1.
+        val itemIndexToFind = (autoCentering.value!!.itemIndex + 1).coerceAtMost(totalItemCount - 2)
+
+        // Find the initialCenterItem, if it is null that means it is not in view - therefore
+        // we have more than enough content before it to make sure it can be scrolled to the center
+        // of the viewport
+        val initialCenterItem =
+            visibleItems.find { it.index == itemIndexToFind }
+
+        // Determine how much space we actually need
+        var spaceNeeded = spaceNeeded(initialCenterItem)
+
+        if (spaceNeeded > 0f) {
+            // Now see how much content we already have
+            visibleItems.map {
+                if (it.index != 0 && it.index < itemIndexToFind) {
+                    // Reduce the space needed
+                    spaceNeeded = spaceNeeded - gapBetweenItemsPx.value!! - it.size
+                }
+            }
+        }
+        // Finally if the remaining space needed is less that the gap between items then we do not
+        // need to add any additional space so the spacer being size zero is correct. Otherwise we
+        // need to wait for it to be inflated.
+        return spaceNeeded < gapBetweenItemsPx.value!!
+    }
+
+    private fun spaceNeeded(item: ScalingLazyListItemInfo?) =
+        viewportCenterLinePx() - gapBetweenItemsPx.value!! - autoCentering.value!!.itemOffset -
+            (item?.unadjustedItemSizeAboveOffsetPoint() ?: 0)
+
+    private fun spaceNeeded(item: LazyListItemInfo?) =
+        viewportCenterLinePx() - gapBetweenItemsPx.value!! - autoCentering.value!!.itemOffset -
+            (item?.itemSizeAboveOffsetPoint() ?: 0)
+
+    private fun calculateBottomAutoCenteringPaddingPx(
+        visibleItemsInfo: List<ScalingLazyListItemInfo>,
+        totalItemsCount: Int
+    ) = if (autoCentering.value != null && visibleItemsInfo.isNotEmpty() &&
+        visibleItemsInfo.last().index == totalItemsCount - 1
+    ) {
+        // Round any fractional part up for the bottom spacer as we round down toward zero
+        // for the viewport center line and item heights working from the top of the
+        // viewport and then add 1 pixel if needed (for an odd height viewport) at the end
+        // spacer
+        viewportHeightPx.value!! - viewportCenterLinePx() -
+            visibleItemsInfo.last().unadjustedItemSizeBelowOffsetPoint()
+    } else {
+        0
+    }
+
+    /**
+     * Calculate the center line of the viewport. This is half of the viewport height rounded down
+     * to the nearest int. This means that for a viewport with an odd number of pixels in height we
+     * will have the area above the viewport being one pixel smaller, e.g. a 199 pixel high
+     * viewport will be treated as having 99 pixels above and 100 pixels below the center line.
+     */
+    private fun viewportCenterLinePx(): Int = viewportHeightPx.value!! / 2
+
+    /**
+     * How much of the items unscaled size would be above the point on the item that represents the
+     * offset point. For an edge anchored item the offset point is the top of the item. For a center
+     * anchored item the offset point is floor(height/2).
+     */
+    private fun ScalingLazyListItemInfo.unadjustedItemSizeAboveOffsetPoint() =
+        if (anchorType.value == ScalingLazyListAnchorType.ItemStart) {
+            0
+        } else {
+            this.unadjustedSize / 2
+        }
+
+    /**
+     * How much of the items size would be above the point on the item that represents the
+     * offset point. For an edge anchored item the offset point is the top of the item. For a center
+     * anchored item the offset point is floor(height/2).
+     */
+    private fun LazyListItemInfo.itemSizeAboveOffsetPoint() =
+        if (anchorType.value == ScalingLazyListAnchorType.ItemStart) {
+            0
+        } else {
+            this.size / 2
+        }
+
+    /**
+     * How much of the items size would be below the point on the item that represents the
+     * offset point. For an edge anchored item the offset point is the top of the item. For a center
+     * anchored item the offset point is floor(height/2).
+     */
+    private fun ScalingLazyListItemInfo.unadjustedItemSizeBelowOffsetPoint() =
+        this.unadjustedSize - unadjustedItemSizeAboveOffsetPoint()
+}
+
+private fun LazyListLayoutInfo.findItemInfoWithIndex(index: Int): LazyListItemInfo? {
+    return this.visibleItemsInfo.find { it.index == index }
+}
+
+private suspend fun LazyListState.scrollToItem(animated: Boolean, index: Int, offset: Int) {
+    if (animated) animateScrollToItem(index, offset) else scrollToItem(index, offset)
+}
+
+private fun ScalingLazyListLayoutInfo.averageUnadjustedItemSize(): Int {
+    var totalSize = 0
+    visibleItemsInfo.forEach { totalSize += it.unadjustedSize }
+    return if (visibleItemsInfo.isNotEmpty())
+        (totalSize.toFloat() / visibleItemsInfo.size).roundToInt()
+    else 0
+}
+
+private object EmptyScalingLazyListLayoutInfo : ScalingLazyListLayoutInfo {
+    override val visibleItemsInfo = emptyList<ScalingLazyListItemInfo>()
+    override val viewportStartOffset = 0
+    override val viewportEndOffset = 0
+    override val totalItemsCount = 0
+    override val viewportSize = IntSize.Zero
+    override val orientation = Orientation.Vertical
+    override val reverseLayout = false
+    override val beforeContentPadding = 0
+    override val afterContentPadding = 0
+    override val beforeAutoCenteringPadding = 0
+    override val afterAutoCenteringPadding = 0
+    override val anchorType: ScalingLazyListAnchorType = ScalingLazyListAnchorType.ItemCenter
+}
+
+internal fun ScalingLazyListLayoutInfo.internalVisibleItemInfo() =
+    (this as? DefaultScalingLazyListLayoutInfo)?.internalVisibleItemsInfo ?: emptyList()
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyScopeMarker.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyScopeMarker.kt
new file mode 100644
index 0000000..c0f85f7
--- /dev/null
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyScopeMarker.kt
@@ -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.
+ */
+
+package androidx.wear.compose.foundation.lazy
+
+/**
+ * DSL marker used to distinguish between lazy layout scope and the item scope.
+ */
+@DslMarker
+annotation class ScalingLazyScopeMarker
diff --git a/wear/compose/compose-material-core/api/current.txt b/wear/compose/compose-material-core/api/current.txt
index e6f50d0..d039d22 100644
--- a/wear/compose/compose-material-core/api/current.txt
+++ b/wear/compose/compose-material-core/api/current.txt
@@ -1 +1,8 @@
 // Signature format: 4.0
+package androidx.wear.compose.materialcore {
+
+  public final class ButtonKt {
+  }
+
+}
+
diff --git a/wear/compose/compose-material-core/api/public_plus_experimental_current.txt b/wear/compose/compose-material-core/api/public_plus_experimental_current.txt
index e6f50d0..d039d22 100644
--- a/wear/compose/compose-material-core/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material-core/api/public_plus_experimental_current.txt
@@ -1 +1,8 @@
 // Signature format: 4.0
+package androidx.wear.compose.materialcore {
+
+  public final class ButtonKt {
+  }
+
+}
+
diff --git a/wear/compose/compose-material-core/api/restricted_current.txt b/wear/compose/compose-material-core/api/restricted_current.txt
index e6f50d0..d039d22 100644
--- a/wear/compose/compose-material-core/api/restricted_current.txt
+++ b/wear/compose/compose-material-core/api/restricted_current.txt
@@ -1 +1,8 @@
 // Signature format: 4.0
+package androidx.wear.compose.materialcore {
+
+  public final class ButtonKt {
+  }
+
+}
+
diff --git a/wear/compose/compose-material-core/src/androidAndroidTest/kotlin/androidx/wear/compose/materialcore/ButtonTest.kt b/wear/compose/compose-material-core/src/androidAndroidTest/kotlin/androidx/wear/compose/materialcore/ButtonTest.kt
new file mode 100644
index 0000000..bcd470a
--- /dev/null
+++ b/wear/compose/compose-material-core/src/androidAndroidTest/kotlin/androidx/wear/compose/materialcore/ButtonTest.kt
@@ -0,0 +1,401 @@
+/*
+ * 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.wear.compose.materialcore
+
+import android.os.Build
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.CutCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ProvidedValue
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertTouchHeightIsEqualTo
+import androidx.compose.ui.test.assertTouchWidthIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.SdkSuppress
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+
+public class ButtonTest {
+    @get:Rule
+    public val rule = createComposeRule()
+
+    @Composable
+    internal fun ButtonWithDefaults(
+        modifier: Modifier = Modifier,
+        onClick: () -> Unit = {},
+        enabled: Boolean = true,
+        backgroundColor: @Composable (enabled: Boolean) -> State<Color> = {
+            remember { mutableStateOf(Color.Blue) }
+        },
+        interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+        shape: Shape = CircleShape,
+        border: @Composable (enabled: Boolean) -> State<BorderStroke?>? = { null },
+        contentProviderValues: Array<ProvidedValue<*>> = arrayOf(),
+        content: @Composable BoxScope.() -> Unit
+    ) = Button(
+        onClick = onClick,
+        modifier = modifier,
+        enabled = enabled,
+        backgroundColor = backgroundColor,
+        interactionSource = interactionSource,
+        shape = shape,
+        border = border,
+        minButtonSize = 52.dp,
+        contentProviderValues = contentProviderValues,
+        content = content
+    )
+
+    @Test
+    public fun supports_testtag_on_button() {
+        rule.setContent {
+            ButtonWithDefaults(
+                modifier = Modifier.testTag(TEST_TAG),
+            ) {
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    public fun has_clickaction_when_enabled() {
+        rule.setContent {
+            ButtonWithDefaults(
+                enabled = true,
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertHasClickAction()
+    }
+
+    @Test
+    public fun has_clickaction_when_disabled() {
+        rule.setContent {
+            ButtonWithDefaults(
+                enabled = false,
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertHasClickAction()
+    }
+
+    @Test
+    public fun is_correctly_enabled() {
+        rule.setContent {
+            ButtonWithDefaults(
+                enabled = true,
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertIsEnabled()
+    }
+
+    @Test
+    public fun is_correctly_disabled() {
+        rule.setContent {
+            ButtonWithDefaults(
+                enabled = false,
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertIsNotEnabled()
+    }
+
+    @Test
+    public fun button_responds_to_click_when_enabled() {
+        var clicked = false
+
+        rule.setContent {
+            ButtonWithDefaults(
+                onClick = { clicked = true },
+                enabled = true,
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performClick()
+
+        rule.runOnIdle {
+            assertEquals(true, clicked)
+        }
+    }
+
+    @Test
+    public fun button_does_not_respond_to_click_when_disabled() {
+        var clicked = false
+
+        rule.setContent {
+            ButtonWithDefaults(
+                onClick = { clicked = true },
+                enabled = false,
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performClick()
+
+        rule.runOnIdle {
+            assertEquals(false, clicked)
+        }
+    }
+
+    @Test
+    public fun has_role_button_for_button() {
+        rule.setContent {
+            ButtonWithDefaults(
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG)
+            .assert(
+                SemanticsMatcher.expectValue(
+                    SemanticsProperties.Role,
+                    Role.Button
+                )
+            )
+    }
+
+    @Test
+    public fun supports_circleshape_under_ltr_for_button() =
+        rule.isShape(CircleShape, LayoutDirection.Ltr) {
+            ButtonWithDefaults(
+                modifier = Modifier.testTag(TEST_TAG),
+            ) {
+            }
+        }
+
+    @Test
+    public fun supports_circleshape_under_rtl_for_button() =
+        rule.isShape(CircleShape, LayoutDirection.Rtl) {
+            ButtonWithDefaults(
+                modifier = Modifier.testTag(TEST_TAG),
+            ) {
+            }
+        }
+
+    @Test
+    public fun extra_small_button_meets_accessibility_tapsize() {
+        verifyTapSize(48.dp) {
+            ButtonWithDefaults(
+                modifier = Modifier.testTag(TEST_TAG).size(32.dp)
+            ) {
+            }
+        }
+    }
+
+    @Test
+    public fun extra_small_button_has_correct_visible_size() {
+        verifyVisibleSize(32.dp) {
+            ButtonWithDefaults(
+                modifier = Modifier
+                    .testTag(TEST_TAG)
+                    .requiredSize(32.dp)
+            ) {
+            }
+        }
+    }
+
+    @Test
+    public fun default_button_has_correct_tapsize() {
+        // Tap size for Button should be the min button size.
+        verifyTapSize(52.dp) {
+            ButtonWithDefaults(
+                modifier = Modifier.testTag(TEST_TAG).size(52.dp)
+            ) {
+            }
+        }
+    }
+
+    @Test
+    public fun default_button_has_correct_visible_size() {
+        // Tap size for Button should be the min button size.
+        verifyVisibleSize(52.dp) {
+            ButtonWithDefaults(
+                modifier = Modifier.testTag(TEST_TAG).size(52.dp)
+            ) {
+            }
+        }
+    }
+
+    @Test
+    public fun allows_custom_button_shape_override() {
+        val shape = CutCornerShape(4.dp)
+
+        rule.isShape(shape, LayoutDirection.Ltr) {
+            ButtonWithDefaults(
+                shape = shape,
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    public fun gives_enabled_button_correct_colors() =
+        verifyButtonColors(
+            Status.Enabled,
+            { enabled -> remember { mutableStateOf(if (enabled) Color.Green else Color.Red) } },
+            { enabled -> remember { mutableStateOf(if (enabled) Color.Blue else Color.Yellow) } },
+            Color.Green,
+            Color.Blue
+        )
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    public fun gives_disabled_button_correct_colors() =
+        verifyButtonColors(
+            Status.Disabled,
+            { enabled -> remember { mutableStateOf(if (enabled) Color.Green else Color.Red) } },
+            { enabled -> remember { mutableStateOf(if (enabled) Color.Blue else Color.Yellow) } },
+            Color.Red,
+            Color.Yellow,
+        )
+
+    @Test
+    fun button_obeys_content_provider_values() {
+        var data = -1
+
+        rule.setContent {
+            Box(modifier = Modifier.fillMaxSize()) {
+                ButtonWithDefaults(
+                    content = {
+                        data = LocalContentTestData.current
+                    },
+                    contentProviderValues = arrayOf(
+                        LocalContentTestData provides EXPECTED_LOCAL_TEST_DATA
+                    )
+                )
+            }
+        }
+
+        assertEquals(data, EXPECTED_LOCAL_TEST_DATA)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    internal fun verifyButtonColors(
+        status: Status,
+        backgroundColor: @Composable (Boolean) -> State<Color>,
+        borderColor: @Composable (Boolean) -> State<Color>,
+        expectedBackgroundColor: Color,
+        expectedBorderColor: Color,
+        backgroundThreshold: Float = 50.0f,
+        borderThreshold: Float = 1.0f,
+    ) {
+        val testBackground = Color.White
+        val expectedColor = { color: Color ->
+            if (color != Color.Transparent)
+                color.compositeOver(testBackground)
+            else
+                testBackground
+        }
+
+        rule.setContent {
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .background(testBackground)
+            ) {
+                val actualBorderColor = borderColor(status.enabled()).value
+                val border = remember { mutableStateOf(BorderStroke(2.dp, actualBorderColor)) }
+                ButtonWithDefaults(
+                    backgroundColor = backgroundColor,
+                    border = { return@ButtonWithDefaults border },
+                    enabled = status.enabled(),
+                    modifier = Modifier.testTag(TEST_TAG)
+                ) {
+                }
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG)
+            .captureToImage()
+            .assertContainsColor(expectedColor(expectedBackgroundColor), backgroundThreshold)
+        rule.onNodeWithTag(TEST_TAG)
+            .captureToImage()
+            .assertContainsColor(expectedColor(expectedBorderColor), borderThreshold)
+    }
+
+    private fun verifyTapSize(
+        expected: Dp,
+        content: @Composable () -> Unit
+    ) {
+        rule.setContent {
+            content()
+        }
+
+        rule.onNodeWithTag(TEST_TAG)
+            .assertTouchHeightIsEqualTo(expected)
+            .assertTouchWidthIsEqualTo(expected)
+    }
+
+    private fun verifyVisibleSize(
+        expected: Dp,
+        content: @Composable () -> Unit
+    ) {
+        rule.setContent {
+            content()
+        }
+
+        rule.onNodeWithTag(TEST_TAG)
+            .assertHeightIsEqualTo(expected)
+            .assertWidthIsEqualTo(expected)
+    }
+}
diff --git a/wear/compose/compose-material-core/src/androidAndroidTest/kotlin/androidx/wear/compose/materialcore/MaterialCoreTest.kt b/wear/compose/compose-material-core/src/androidAndroidTest/kotlin/androidx/wear/compose/materialcore/MaterialCoreTest.kt
new file mode 100644
index 0000000..4ff48c8
--- /dev/null
+++ b/wear/compose/compose-material-core/src/androidAndroidTest/kotlin/androidx/wear/compose/materialcore/MaterialCoreTest.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.wear.compose.materialcore
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.testutils.assertShape
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.toPixelMap
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.SdkSuppress
+import kotlin.math.round
+
+internal val LocalContentTestData: ProvidableCompositionLocal<Int> = compositionLocalOf { 1 }
+internal const val EXPECTED_LOCAL_TEST_DATA = 42
+
+internal const val TEST_TAG = "test-item"
+
+internal enum class Status {
+    Enabled,
+    Disabled;
+
+    public fun enabled() = this == Enabled
+}
+
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+internal fun ComposeContentTestRule.isShape(
+    shape: Shape,
+    layoutDirection: LayoutDirection,
+    padding: Dp = 0.dp,
+    backgroundColor: Color = Color.Red,
+    shapeColor: Color = Color.Blue,
+    content: @Composable () -> Unit
+) {
+    setContent {
+        CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+            Box(
+                Modifier
+                    .padding(padding)
+                    .background(backgroundColor)
+            ) {
+                content()
+            }
+        }
+    }
+
+    this.waitForIdle()
+    onNodeWithTag(TEST_TAG)
+        .captureToImage()
+        .assertShape(
+            density = density,
+            shape = shape,
+            horizontalPadding = padding,
+            verticalPadding = padding,
+            backgroundColor = backgroundColor,
+            shapeOverlapPixelCount = 2.0f,
+            shapeColor = shapeColor
+        )
+}
+
+/**
+ * assertContainsColor - uses a threshold on an ImageBitmap's color distribution
+ * to check that a UI element is predominantly the expected color.
+ */
+internal fun ImageBitmap.assertContainsColor(expectedColor: Color, minPercent: Float = 50.0f) {
+    val histogram = histogram()
+
+    val actualPercent = round(((histogram[expectedColor] ?: 0) * 100f) / (width * height))
+    if (actualPercent < minPercent) {
+        throw AssertionError(
+            "Expected color $expectedColor found $actualPercent%, below threshold $minPercent%"
+        )
+    }
+}
+
+private fun ImageBitmap.histogram(): MutableMap<Color, Long> {
+    val pixels = this.toPixelMap()
+    val histogram = mutableMapOf<Color, Long>()
+    for (x in 0 until width) {
+        for (y in 0 until height) {
+            val color = pixels[x, y]
+            histogram[color] = histogram.getOrDefault(color, 0) + 1
+        }
+    }
+    return histogram
+}
diff --git a/wear/compose/compose-material-core/src/commonMain/kotlin/androidx/wear/compose/materialcore/Button.kt b/wear/compose/compose-material-core/src/commonMain/kotlin/androidx/wear/compose/materialcore/Button.kt
new file mode 100644
index 0000000..7bcdfaf0
--- /dev/null
+++ b/wear/compose/compose-material-core/src/commonMain/kotlin/androidx/wear/compose/materialcore/Button.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.wear.compose.materialcore
+
+import androidx.annotation.RestrictTo
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.ProvidedValue
+import androidx.compose.runtime.State
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.unit.Dp
+
+/**
+ * Wear Material [Button] that offers a single slot to take any content (text, icon or image).
+ *
+ * [Button] can be enabled or disabled. A disabled button will not respond to click events.
+ *
+ * For more information, see the
+ * [Buttons](https://developer.android.com/training/wearables/components/buttons)
+ * guide.
+ *
+ * @param onClick Will be called when the user clicks the button.
+ * @param modifier Modifier to be applied to the button.
+ * @param enabled Controls the enabled state of the button. When `false`, this button will not
+ * be clickable.
+ * @param backgroundColor Resolves the background for this button in different states.
+ * @param interactionSource The [MutableInteractionSource] representing the stream of
+ * [Interaction]s for this Button. You can create and pass in your own remembered
+ * [MutableInteractionSource] if you want to observe [Interaction]s and customize the
+ * appearance / behavior of this Button in different [Interaction]s.
+ * @param shape Defines the button's shape.
+ * @param border Resolves the border for this button in different states.
+ * @param minButtonSize The default, minimum size of the button unless overridden by Modifier.size.
+ * @param contentProviderValues Values of CompositionLocal providers such as LocalContentColor,
+ * LocalContentAlpha, LocalTextStyle which are dependent on a specific material design version
+ * and are not part of this material-agnostic library.
+ * @param content The content displayed on the [Button] such as text, icon or image.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Composable
+public fun Button(
+    onClick: () -> Unit,
+    modifier: Modifier,
+    enabled: Boolean,
+    backgroundColor: @Composable (enabled: Boolean) -> State<Color>,
+    interactionSource: MutableInteractionSource,
+    shape: Shape,
+    border: @Composable (enabled: Boolean) -> State<BorderStroke?>?,
+    minButtonSize: Dp,
+    contentProviderValues: Array<ProvidedValue<*>>,
+    content: @Composable BoxScope.() -> Unit,
+) {
+    val borderStroke = border(enabled)?.value
+    Box(
+        contentAlignment = Alignment.Center,
+        modifier = Modifier
+            .clip(shape)
+            .clickable(
+                onClick = onClick,
+                enabled = enabled,
+                role = Role.Button,
+                interactionSource = interactionSource,
+                indication = rememberRipple(),
+            )
+            .then(
+                // Make sure modifier ordering is clip > clickable > padding > size,
+                // so that the ripple applies to the entire button shape and size.
+                modifier
+            )
+            .defaultMinSize(
+                minWidth = minButtonSize,
+                minHeight = minButtonSize
+            )
+            .then(
+                if (borderStroke != null) Modifier.border(border = borderStroke, shape = shape)
+                else Modifier
+            )
+            .background(
+                color = backgroundColor(enabled).value,
+                shape = shape
+            )
+    ) {
+        CompositionLocalProvider(
+            values = contentProviderValues
+        ) {
+            content()
+        }
+    }
+}
diff --git a/wear/compose/compose-material/api/current.ignore b/wear/compose/compose-material/api/current.ignore
index 82c32a0..05e58f9 100644
--- a/wear/compose/compose-material/api/current.ignore
+++ b/wear/compose/compose-material/api/current.ignore
@@ -1,7 +1,3 @@
 // Baseline format: 1.0
 ChangedValue: androidx.wear.compose.material.TimeTextDefaults#TimeFormat12Hours:
     Field androidx.wear.compose.material.TimeTextDefaults.TimeFormat12Hours has changed value from h:mm a to h:mm
-
-
-InvalidNullConversion: androidx.wear.compose.material.PickerKt#Picker(androidx.wear.compose.material.PickerState, String, androidx.compose.ui.Modifier, boolean, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit>, kotlin.jvm.functions.Function0<? extends kotlin.Unit>, androidx.wear.compose.material.ScalingParams, float, float, long, androidx.compose.foundation.gestures.FlingBehavior, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,? extends kotlin.Unit>) parameter #1:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter contentDescription in androidx.wear.compose.material.PickerKt.Picker(androidx.wear.compose.material.PickerState state, String contentDescription, androidx.compose.ui.Modifier modifier, boolean readOnly, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit> readOnlyLabel, kotlin.jvm.functions.Function0<? extends kotlin.Unit> onSelected, androidx.wear.compose.material.ScalingParams scalingParams, float separation, float gradientRatio, long gradientColor, androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,? extends kotlin.Unit> option)
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index f728567..445b1fc 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -4,8 +4,8 @@
   public final class AnimationKt {
   }
 
-  @androidx.compose.runtime.Immutable public final class AutoCenteringParams {
-    ctor public AutoCenteringParams(optional int itemIndex, optional int itemOffset);
+  @Deprecated @androidx.compose.runtime.Immutable public final class AutoCenteringParams {
+    ctor @Deprecated public AutoCenteringParams(optional int itemIndex, optional int itemOffset);
   }
 
   @androidx.compose.runtime.Stable public interface ButtonBorder {
@@ -262,17 +262,19 @@
   }
 
   public final class PickerDefaults {
+    method public androidx.wear.compose.foundation.lazy.ScalingParams defaultScalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior(androidx.wear.compose.material.PickerState state, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decay);
     method public float getDefaultGradientRatio();
-    method public androidx.wear.compose.material.ScalingParams scalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
+    method @Deprecated public androidx.wear.compose.material.ScalingParams scalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
     property public final float DefaultGradientRatio;
     field public static final androidx.wear.compose.material.PickerDefaults INSTANCE;
   }
 
   public final class PickerKt {
-    method @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material.PickerState state, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit> onSelected, optional androidx.wear.compose.material.ScalingParams scalingParams, optional float separation, optional float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,kotlin.Unit> option);
+    method @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material.PickerState state, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit> onSelected, optional androidx.wear.compose.foundation.lazy.ScalingParams scalingParams, optional float separation, optional float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,kotlin.Unit> option);
     method @Deprecated @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material.PickerState state, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel, optional androidx.wear.compose.material.ScalingParams scalingParams, optional float separation, optional float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,kotlin.Unit> option);
-    method @Deprecated @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material.PickerState state, String contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit> onSelected, optional androidx.wear.compose.material.ScalingParams scalingParams, optional float separation, optional float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,? extends kotlin.Unit> option);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material.PickerState state, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit> onSelected, optional androidx.wear.compose.material.ScalingParams scalingParams, optional float separation, optional float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,? extends kotlin.Unit> option);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material.PickerState state, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit> onSelected, optional androidx.wear.compose.material.ScalingParams scalingParams, optional float separation, optional float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,? extends kotlin.Unit> option);
     method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.PickerState rememberPickerState(int initialNumberOfOptions, optional int initiallySelectedOption, optional boolean repeatItems);
   }
 
@@ -325,7 +327,8 @@
 
   public final class PositionIndicatorKt {
     method @androidx.compose.runtime.Composable public static void PositionIndicator(androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.Modifier modifier, optional boolean reverseDirection);
-    method @androidx.compose.runtime.Composable public static void PositionIndicator(androidx.wear.compose.material.ScalingLazyListState scalingLazyListState, optional androidx.compose.ui.Modifier modifier, optional boolean reverseDirection);
+    method @androidx.compose.runtime.Composable public static void PositionIndicator(androidx.wear.compose.foundation.lazy.ScalingLazyListState scalingLazyListState, optional androidx.compose.ui.Modifier modifier, optional boolean reverseDirection);
+    method @Deprecated @androidx.compose.runtime.Composable public static void PositionIndicator(androidx.wear.compose.material.ScalingLazyListState scalingLazyListState, optional androidx.compose.ui.Modifier modifier, optional boolean reverseDirection);
     method @androidx.compose.runtime.Composable public static void PositionIndicator(androidx.compose.foundation.lazy.LazyListState lazyListState, optional androidx.compose.ui.Modifier modifier, optional boolean reverseDirection);
     method @androidx.compose.runtime.Composable public static void PositionIndicator(kotlin.jvm.functions.Function0<java.lang.Float> value, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> range, optional long color, optional boolean reverseDirection, optional int position);
     method @androidx.compose.runtime.Composable public static void PositionIndicator(androidx.wear.compose.material.PositionIndicatorState state, float indicatorHeight, float indicatorWidth, float paddingHorizontal, optional androidx.compose.ui.Modifier modifier, optional long background, optional long color, optional boolean reverseDirection, optional int position);
@@ -384,43 +387,43 @@
     method @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? vignette, optional kotlin.jvm.functions.Function0<kotlin.Unit>? positionIndicator, optional kotlin.jvm.functions.Function0<kotlin.Unit>? pageIndicator, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
-  public final class ScalingLazyColumnDefaults {
-    method public androidx.wear.compose.material.ScalingParams scalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
-    method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior snapFlingBehavior(androidx.wear.compose.material.ScalingLazyListState state, optional float snapOffset, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decay);
-    field public static final androidx.wear.compose.material.ScalingLazyColumnDefaults INSTANCE;
+  @Deprecated public final class ScalingLazyColumnDefaults {
+    method @Deprecated public androidx.wear.compose.material.ScalingParams scalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior snapFlingBehavior(androidx.wear.compose.material.ScalingLazyListState state, optional float snapOffset, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decay);
+    field @Deprecated public static final androidx.wear.compose.material.ScalingLazyColumnDefaults INSTANCE;
   }
 
   public final class ScalingLazyColumnKt {
-    method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.material.ScalingParams scalingParams, optional int anchorType, optional androidx.wear.compose.material.AutoCenteringParams? autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
-    method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
-    method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
-    method public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
-    method public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method @Deprecated @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.material.ScalingParams scalingParams, optional int anchorType, optional androidx.wear.compose.material.AutoCenteringParams? autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
+    method @Deprecated public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
+    method @Deprecated public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
+    method @Deprecated public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method @Deprecated public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
   public final class ScalingLazyColumnMeasureKt {
   }
 
-  @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class ScalingLazyListAnchorType {
-    field public static final androidx.wear.compose.material.ScalingLazyListAnchorType.Companion Companion;
+  @Deprecated @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class ScalingLazyListAnchorType {
+    field @Deprecated public static final androidx.wear.compose.material.ScalingLazyListAnchorType.Companion Companion;
   }
 
-  public static final class ScalingLazyListAnchorType.Companion {
-    method public int getItemCenter();
-    method public int getItemStart();
+  @Deprecated public static final class ScalingLazyListAnchorType.Companion {
+    method @Deprecated public int getItemCenter();
+    method @Deprecated public int getItemStart();
     property public final int ItemCenter;
     property public final int ItemStart;
   }
 
-  public sealed interface ScalingLazyListItemInfo {
-    method public float getAlpha();
-    method public int getIndex();
-    method public Object getKey();
-    method public int getOffset();
-    method public float getScale();
-    method public int getSize();
-    method public int getUnadjustedOffset();
-    method public int getUnadjustedSize();
+  @Deprecated public sealed interface ScalingLazyListItemInfo {
+    method @Deprecated public float getAlpha();
+    method @Deprecated public int getIndex();
+    method @Deprecated public Object getKey();
+    method @Deprecated public int getOffset();
+    method @Deprecated public float getScale();
+    method @Deprecated public int getSize();
+    method @Deprecated public int getUnadjustedOffset();
+    method @Deprecated public int getUnadjustedSize();
     property public abstract float alpha;
     property public abstract int index;
     property public abstract Object key;
@@ -431,24 +434,24 @@
     property public abstract int unadjustedSize;
   }
 
-  @androidx.compose.runtime.Stable @androidx.wear.compose.material.ScalingLazyScopeMarker public sealed interface ScalingLazyListItemScope {
-    method public androidx.compose.ui.Modifier fillParentMaxHeight(androidx.compose.ui.Modifier, optional float fraction);
-    method public androidx.compose.ui.Modifier fillParentMaxSize(androidx.compose.ui.Modifier, optional float fraction);
-    method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional float fraction);
+  @Deprecated @androidx.compose.runtime.Stable @androidx.wear.compose.material.ScalingLazyScopeMarker public sealed interface ScalingLazyListItemScope {
+    method @Deprecated public androidx.compose.ui.Modifier fillParentMaxHeight(androidx.compose.ui.Modifier, optional float fraction);
+    method @Deprecated public androidx.compose.ui.Modifier fillParentMaxSize(androidx.compose.ui.Modifier, optional float fraction);
+    method @Deprecated public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional float fraction);
   }
 
-  public sealed interface ScalingLazyListLayoutInfo {
-    method public int getAfterAutoCenteringPadding();
-    method public int getAfterContentPadding();
-    method public int getBeforeAutoCenteringPadding();
-    method public int getBeforeContentPadding();
-    method public androidx.compose.foundation.gestures.Orientation getOrientation();
-    method public boolean getReverseLayout();
-    method public int getTotalItemsCount();
-    method public int getViewportEndOffset();
-    method public long getViewportSize();
-    method public int getViewportStartOffset();
-    method public java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> getVisibleItemsInfo();
+  @Deprecated public sealed interface ScalingLazyListLayoutInfo {
+    method @Deprecated public int getAfterAutoCenteringPadding();
+    method @Deprecated public int getAfterContentPadding();
+    method @Deprecated public int getBeforeAutoCenteringPadding();
+    method @Deprecated public int getBeforeContentPadding();
+    method @Deprecated public androidx.compose.foundation.gestures.Orientation getOrientation();
+    method @Deprecated public boolean getReverseLayout();
+    method @Deprecated public int getTotalItemsCount();
+    method @Deprecated public int getViewportEndOffset();
+    method @Deprecated public long getViewportSize();
+    method @Deprecated public int getViewportStartOffset();
+    method @Deprecated public java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> getVisibleItemsInfo();
     property public abstract int afterAutoCenteringPadding;
     property public abstract int afterContentPadding;
     property public abstract int beforeAutoCenteringPadding;
@@ -462,51 +465,51 @@
     property public abstract java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> visibleItemsInfo;
   }
 
-  @androidx.wear.compose.material.ScalingLazyScopeMarker public sealed interface ScalingLazyListScope {
-    method public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListItemScope,kotlin.Unit> content);
-    method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+  @Deprecated @androidx.wear.compose.material.ScalingLazyScopeMarker public sealed interface ScalingLazyListScope {
+    method @Deprecated public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListItemScope,kotlin.Unit> content);
+    method @Deprecated public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
   }
 
-  @androidx.compose.runtime.Stable public final class ScalingLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
-    ctor public ScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
-    method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public float dispatchRawDelta(float delta);
-    method public int getCenterItemIndex();
-    method public int getCenterItemScrollOffset();
-    method public androidx.wear.compose.material.ScalingLazyListLayoutInfo getLayoutInfo();
-    method public boolean isScrollInProgress();
-    method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+  @Deprecated @androidx.compose.runtime.Stable public final class ScalingLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
+    ctor @Deprecated public ScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
+    method @Deprecated public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @Deprecated public float dispatchRawDelta(float delta);
+    method @Deprecated public int getCenterItemIndex();
+    method @Deprecated public int getCenterItemScrollOffset();
+    method @Deprecated public androidx.wear.compose.material.ScalingLazyListLayoutInfo getLayoutInfo();
+    method @Deprecated public boolean isScrollInProgress();
+    method @Deprecated public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @Deprecated public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
     property public boolean canScrollBackward;
     property public boolean canScrollForward;
     property public final int centerItemIndex;
     property public final int centerItemScrollOffset;
     property public boolean isScrollInProgress;
     property public final androidx.wear.compose.material.ScalingLazyListLayoutInfo layoutInfo;
-    field public static final androidx.wear.compose.material.ScalingLazyListState.Companion Companion;
+    field @Deprecated public static final androidx.wear.compose.material.ScalingLazyListState.Companion Companion;
   }
 
-  public static final class ScalingLazyListState.Companion {
-    method public androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyListState,java.lang.Object> getSaver();
+  @Deprecated public static final class ScalingLazyListState.Companion {
+    method @Deprecated public androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyListState,java.lang.Object> getSaver();
     property public final androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyListState,java.lang.Object> Saver;
   }
 
   public final class ScalingLazyListStateKt {
-    method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyListState rememberScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
+    method @Deprecated @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyListState rememberScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
   }
 
-  @kotlin.DslMarker public @interface ScalingLazyScopeMarker {
+  @Deprecated @kotlin.DslMarker public @interface ScalingLazyScopeMarker {
   }
 
-  @androidx.compose.runtime.Stable public interface ScalingParams {
-    method public float getEdgeAlpha();
-    method public float getEdgeScale();
-    method public float getMaxElementHeight();
-    method public float getMaxTransitionArea();
-    method public float getMinElementHeight();
-    method public float getMinTransitionArea();
-    method public androidx.compose.animation.core.Easing getScaleInterpolator();
-    method public int resolveViewportVerticalOffset(long viewportConstraints);
+  @Deprecated @androidx.compose.runtime.Stable public interface ScalingParams {
+    method @Deprecated public float getEdgeAlpha();
+    method @Deprecated public float getEdgeScale();
+    method @Deprecated public float getMaxElementHeight();
+    method @Deprecated public float getMaxTransitionArea();
+    method @Deprecated public float getMinElementHeight();
+    method @Deprecated public float getMinTransitionArea();
+    method @Deprecated public androidx.compose.animation.core.Easing getScaleInterpolator();
+    method @Deprecated public int resolveViewportVerticalOffset(long viewportConstraints);
     property public abstract float edgeAlpha;
     property public abstract float edgeScale;
     property public abstract float maxElementHeight;
@@ -519,7 +522,8 @@
   public final class ScrollAwayKt {
     method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.ScrollState scrollState, optional float offset);
     method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.lazy.LazyListState scrollState, optional int itemIndex, optional float offset);
-    method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.material.ScalingLazyListState scrollState, optional int itemIndex, optional float offset);
+    method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollState, optional int itemIndex, optional float offset);
+    method @Deprecated public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.material.ScalingLazyListState scrollState, optional int itemIndex, optional float offset);
   }
 
   @androidx.compose.runtime.Immutable public final class Shapes {
@@ -558,8 +562,10 @@
   }
 
   public final class StepperKt {
-    method @androidx.compose.runtime.Composable public static void Stepper(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, int steps, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Stepper(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, int steps, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional long backgroundColor, optional long contentColor, optional long iconColor, optional boolean enableRangeSemantics, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, optional boolean enableRangeSemantics, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Stepper(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends kotlin.Unit> onValueChange, int steps, kotlin.jvm.functions.Function0<? extends kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<? extends kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<? extends kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<? extends kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit> content);
   }
 
   public final class SwipeToDismissBoxDefaults {
@@ -783,13 +789,17 @@
   }
 
   public final class DialogKt {
-    method @androidx.compose.runtime.Composable public static void Alert(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit> negativeButton, kotlin.jvm.functions.Function0<kotlin.Unit> positiveButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? icon, optional androidx.wear.compose.material.ScalingLazyListState scrollState, optional long backgroundColor, optional long contentColor, optional long titleColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? content);
-    method @androidx.compose.runtime.Composable public static void Alert(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? icon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? message, optional androidx.wear.compose.material.ScalingLazyListState scrollState, optional long backgroundColor, optional long titleColor, optional long messageColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void Confirmation(kotlin.jvm.functions.Function0<kotlin.Unit> onTimeout, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? icon, optional androidx.wear.compose.material.ScalingLazyListState scrollState, optional long durationMillis, optional long backgroundColor, optional long contentColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Alert(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit> negativeButton, kotlin.jvm.functions.Function0<kotlin.Unit> positiveButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? icon, optional androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollState, optional long backgroundColor, optional long contentColor, optional long titleColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? content);
+    method @androidx.compose.runtime.Composable public static void Alert(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? icon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? message, optional androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollState, optional long backgroundColor, optional long titleColor, optional long messageColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Alert(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> title, kotlin.jvm.functions.Function0<? extends kotlin.Unit> negativeButton, kotlin.jvm.functions.Function0<? extends kotlin.Unit> positiveButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit>? icon, optional androidx.wear.compose.material.ScalingLazyListState scrollState, optional long backgroundColor, optional long contentColor, optional long titleColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit>? content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Alert(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit>? icon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit>? message, optional androidx.wear.compose.material.ScalingLazyListState scrollState, optional long backgroundColor, optional long titleColor, optional long messageColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,? extends kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Confirmation(kotlin.jvm.functions.Function0<kotlin.Unit> onTimeout, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? icon, optional androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollState, optional long durationMillis, optional long backgroundColor, optional long contentColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Confirmation(kotlin.jvm.functions.Function0<? extends kotlin.Unit> onTimeout, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit>? icon, optional androidx.wear.compose.material.ScalingLazyListState scrollState, optional long durationMillis, optional long backgroundColor, optional long contentColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> content);
   }
 
   public final class Dialog_androidKt {
-    method @androidx.compose.runtime.Composable public static void Dialog(boolean showDialog, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState? scrollState, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Dialog(boolean showDialog, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.lazy.ScalingLazyListState? scrollState, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Dialog(boolean showDialog, kotlin.jvm.functions.Function0<? extends kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState? scrollState, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function0<? extends kotlin.Unit> content);
   }
 
 }
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index 09bb7fd..d734539 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -4,8 +4,8 @@
   public final class AnimationKt {
   }
 
-  @androidx.compose.runtime.Immutable public final class AutoCenteringParams {
-    ctor public AutoCenteringParams(optional int itemIndex, optional int itemOffset);
+  @Deprecated @androidx.compose.runtime.Immutable public final class AutoCenteringParams {
+    ctor @Deprecated public AutoCenteringParams(optional int itemIndex, optional int itemOffset);
   }
 
   @androidx.compose.runtime.Stable public interface ButtonBorder {
@@ -278,17 +278,19 @@
   }
 
   public final class PickerDefaults {
+    method public androidx.wear.compose.foundation.lazy.ScalingParams defaultScalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior(androidx.wear.compose.material.PickerState state, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decay);
     method public float getDefaultGradientRatio();
-    method public androidx.wear.compose.material.ScalingParams scalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
+    method @Deprecated public androidx.wear.compose.material.ScalingParams scalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
     property public final float DefaultGradientRatio;
     field public static final androidx.wear.compose.material.PickerDefaults INSTANCE;
   }
 
   public final class PickerKt {
-    method @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material.PickerState state, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit> onSelected, optional androidx.wear.compose.material.ScalingParams scalingParams, optional float separation, optional float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,kotlin.Unit> option);
+    method @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material.PickerState state, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit> onSelected, optional androidx.wear.compose.foundation.lazy.ScalingParams scalingParams, optional float separation, optional float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,kotlin.Unit> option);
     method @Deprecated @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material.PickerState state, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel, optional androidx.wear.compose.material.ScalingParams scalingParams, optional float separation, optional float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,kotlin.Unit> option);
-    method @Deprecated @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material.PickerState state, String contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit> onSelected, optional androidx.wear.compose.material.ScalingParams scalingParams, optional float separation, optional float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,? extends kotlin.Unit> option);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material.PickerState state, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit> onSelected, optional androidx.wear.compose.material.ScalingParams scalingParams, optional float separation, optional float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,? extends kotlin.Unit> option);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material.PickerState state, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit> onSelected, optional androidx.wear.compose.material.ScalingParams scalingParams, optional float separation, optional float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,? extends kotlin.Unit> option);
     method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.PickerState rememberPickerState(int initialNumberOfOptions, optional int initiallySelectedOption, optional boolean repeatItems);
   }
 
@@ -362,7 +364,8 @@
 
   public final class PositionIndicatorKt {
     method @androidx.compose.runtime.Composable public static void PositionIndicator(androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.Modifier modifier, optional boolean reverseDirection);
-    method @androidx.compose.runtime.Composable public static void PositionIndicator(androidx.wear.compose.material.ScalingLazyListState scalingLazyListState, optional androidx.compose.ui.Modifier modifier, optional boolean reverseDirection);
+    method @androidx.compose.runtime.Composable public static void PositionIndicator(androidx.wear.compose.foundation.lazy.ScalingLazyListState scalingLazyListState, optional androidx.compose.ui.Modifier modifier, optional boolean reverseDirection);
+    method @Deprecated @androidx.compose.runtime.Composable public static void PositionIndicator(androidx.wear.compose.material.ScalingLazyListState scalingLazyListState, optional androidx.compose.ui.Modifier modifier, optional boolean reverseDirection);
     method @androidx.compose.runtime.Composable public static void PositionIndicator(androidx.compose.foundation.lazy.LazyListState lazyListState, optional androidx.compose.ui.Modifier modifier, optional boolean reverseDirection);
     method @androidx.compose.runtime.Composable public static void PositionIndicator(kotlin.jvm.functions.Function0<java.lang.Float> value, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> range, optional long color, optional boolean reverseDirection, optional int position);
     method @androidx.compose.runtime.Composable public static void PositionIndicator(androidx.wear.compose.material.PositionIndicatorState state, float indicatorHeight, float indicatorWidth, float paddingHorizontal, optional androidx.compose.ui.Modifier modifier, optional long background, optional long color, optional boolean reverseDirection, optional int position);
@@ -432,43 +435,43 @@
     method @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? vignette, optional kotlin.jvm.functions.Function0<kotlin.Unit>? positionIndicator, optional kotlin.jvm.functions.Function0<kotlin.Unit>? pageIndicator, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
-  public final class ScalingLazyColumnDefaults {
-    method public androidx.wear.compose.material.ScalingParams scalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
-    method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior snapFlingBehavior(androidx.wear.compose.material.ScalingLazyListState state, optional float snapOffset, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decay);
-    field public static final androidx.wear.compose.material.ScalingLazyColumnDefaults INSTANCE;
+  @Deprecated public final class ScalingLazyColumnDefaults {
+    method @Deprecated public androidx.wear.compose.material.ScalingParams scalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior snapFlingBehavior(androidx.wear.compose.material.ScalingLazyListState state, optional float snapOffset, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decay);
+    field @Deprecated public static final androidx.wear.compose.material.ScalingLazyColumnDefaults INSTANCE;
   }
 
   public final class ScalingLazyColumnKt {
-    method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.material.ScalingParams scalingParams, optional int anchorType, optional androidx.wear.compose.material.AutoCenteringParams? autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
-    method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
-    method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
-    method public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
-    method public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method @Deprecated @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.material.ScalingParams scalingParams, optional int anchorType, optional androidx.wear.compose.material.AutoCenteringParams? autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
+    method @Deprecated public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
+    method @Deprecated public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
+    method @Deprecated public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method @Deprecated public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
   public final class ScalingLazyColumnMeasureKt {
   }
 
-  @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class ScalingLazyListAnchorType {
-    field public static final androidx.wear.compose.material.ScalingLazyListAnchorType.Companion Companion;
+  @Deprecated @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class ScalingLazyListAnchorType {
+    field @Deprecated public static final androidx.wear.compose.material.ScalingLazyListAnchorType.Companion Companion;
   }
 
-  public static final class ScalingLazyListAnchorType.Companion {
-    method public int getItemCenter();
-    method public int getItemStart();
+  @Deprecated public static final class ScalingLazyListAnchorType.Companion {
+    method @Deprecated public int getItemCenter();
+    method @Deprecated public int getItemStart();
     property public final int ItemCenter;
     property public final int ItemStart;
   }
 
-  public sealed interface ScalingLazyListItemInfo {
-    method public float getAlpha();
-    method public int getIndex();
-    method public Object getKey();
-    method public int getOffset();
-    method public float getScale();
-    method public int getSize();
-    method public int getUnadjustedOffset();
-    method public int getUnadjustedSize();
+  @Deprecated public sealed interface ScalingLazyListItemInfo {
+    method @Deprecated public float getAlpha();
+    method @Deprecated public int getIndex();
+    method @Deprecated public Object getKey();
+    method @Deprecated public int getOffset();
+    method @Deprecated public float getScale();
+    method @Deprecated public int getSize();
+    method @Deprecated public int getUnadjustedOffset();
+    method @Deprecated public int getUnadjustedSize();
     property public abstract float alpha;
     property public abstract int index;
     property public abstract Object key;
@@ -479,24 +482,24 @@
     property public abstract int unadjustedSize;
   }
 
-  @androidx.compose.runtime.Stable @androidx.wear.compose.material.ScalingLazyScopeMarker public sealed interface ScalingLazyListItemScope {
-    method public androidx.compose.ui.Modifier fillParentMaxHeight(androidx.compose.ui.Modifier, optional float fraction);
-    method public androidx.compose.ui.Modifier fillParentMaxSize(androidx.compose.ui.Modifier, optional float fraction);
-    method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional float fraction);
+  @Deprecated @androidx.compose.runtime.Stable @androidx.wear.compose.material.ScalingLazyScopeMarker public sealed interface ScalingLazyListItemScope {
+    method @Deprecated public androidx.compose.ui.Modifier fillParentMaxHeight(androidx.compose.ui.Modifier, optional float fraction);
+    method @Deprecated public androidx.compose.ui.Modifier fillParentMaxSize(androidx.compose.ui.Modifier, optional float fraction);
+    method @Deprecated public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional float fraction);
   }
 
-  public sealed interface ScalingLazyListLayoutInfo {
-    method public int getAfterAutoCenteringPadding();
-    method public int getAfterContentPadding();
-    method public int getBeforeAutoCenteringPadding();
-    method public int getBeforeContentPadding();
-    method public androidx.compose.foundation.gestures.Orientation getOrientation();
-    method public boolean getReverseLayout();
-    method public int getTotalItemsCount();
-    method public int getViewportEndOffset();
-    method public long getViewportSize();
-    method public int getViewportStartOffset();
-    method public java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> getVisibleItemsInfo();
+  @Deprecated public sealed interface ScalingLazyListLayoutInfo {
+    method @Deprecated public int getAfterAutoCenteringPadding();
+    method @Deprecated public int getAfterContentPadding();
+    method @Deprecated public int getBeforeAutoCenteringPadding();
+    method @Deprecated public int getBeforeContentPadding();
+    method @Deprecated public androidx.compose.foundation.gestures.Orientation getOrientation();
+    method @Deprecated public boolean getReverseLayout();
+    method @Deprecated public int getTotalItemsCount();
+    method @Deprecated public int getViewportEndOffset();
+    method @Deprecated public long getViewportSize();
+    method @Deprecated public int getViewportStartOffset();
+    method @Deprecated public java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> getVisibleItemsInfo();
     property public abstract int afterAutoCenteringPadding;
     property public abstract int afterContentPadding;
     property public abstract int beforeAutoCenteringPadding;
@@ -510,51 +513,51 @@
     property public abstract java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> visibleItemsInfo;
   }
 
-  @androidx.wear.compose.material.ScalingLazyScopeMarker public sealed interface ScalingLazyListScope {
-    method public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListItemScope,kotlin.Unit> content);
-    method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+  @Deprecated @androidx.wear.compose.material.ScalingLazyScopeMarker public sealed interface ScalingLazyListScope {
+    method @Deprecated public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListItemScope,kotlin.Unit> content);
+    method @Deprecated public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
   }
 
-  @androidx.compose.runtime.Stable public final class ScalingLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
-    ctor public ScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
-    method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public float dispatchRawDelta(float delta);
-    method public int getCenterItemIndex();
-    method public int getCenterItemScrollOffset();
-    method public androidx.wear.compose.material.ScalingLazyListLayoutInfo getLayoutInfo();
-    method public boolean isScrollInProgress();
-    method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+  @Deprecated @androidx.compose.runtime.Stable public final class ScalingLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
+    ctor @Deprecated public ScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
+    method @Deprecated public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @Deprecated public float dispatchRawDelta(float delta);
+    method @Deprecated public int getCenterItemIndex();
+    method @Deprecated public int getCenterItemScrollOffset();
+    method @Deprecated public androidx.wear.compose.material.ScalingLazyListLayoutInfo getLayoutInfo();
+    method @Deprecated public boolean isScrollInProgress();
+    method @Deprecated public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @Deprecated public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
     property public boolean canScrollBackward;
     property public boolean canScrollForward;
     property public final int centerItemIndex;
     property public final int centerItemScrollOffset;
     property public boolean isScrollInProgress;
     property public final androidx.wear.compose.material.ScalingLazyListLayoutInfo layoutInfo;
-    field public static final androidx.wear.compose.material.ScalingLazyListState.Companion Companion;
+    field @Deprecated public static final androidx.wear.compose.material.ScalingLazyListState.Companion Companion;
   }
 
-  public static final class ScalingLazyListState.Companion {
-    method public androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyListState,java.lang.Object> getSaver();
+  @Deprecated public static final class ScalingLazyListState.Companion {
+    method @Deprecated public androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyListState,java.lang.Object> getSaver();
     property public final androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyListState,java.lang.Object> Saver;
   }
 
   public final class ScalingLazyListStateKt {
-    method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyListState rememberScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
+    method @Deprecated @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyListState rememberScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
   }
 
-  @kotlin.DslMarker public @interface ScalingLazyScopeMarker {
+  @Deprecated @kotlin.DslMarker public @interface ScalingLazyScopeMarker {
   }
 
-  @androidx.compose.runtime.Stable public interface ScalingParams {
-    method public float getEdgeAlpha();
-    method public float getEdgeScale();
-    method public float getMaxElementHeight();
-    method public float getMaxTransitionArea();
-    method public float getMinElementHeight();
-    method public float getMinTransitionArea();
-    method public androidx.compose.animation.core.Easing getScaleInterpolator();
-    method public int resolveViewportVerticalOffset(long viewportConstraints);
+  @Deprecated @androidx.compose.runtime.Stable public interface ScalingParams {
+    method @Deprecated public float getEdgeAlpha();
+    method @Deprecated public float getEdgeScale();
+    method @Deprecated public float getMaxElementHeight();
+    method @Deprecated public float getMaxTransitionArea();
+    method @Deprecated public float getMinElementHeight();
+    method @Deprecated public float getMinTransitionArea();
+    method @Deprecated public androidx.compose.animation.core.Easing getScaleInterpolator();
+    method @Deprecated public int resolveViewportVerticalOffset(long viewportConstraints);
     property public abstract float edgeAlpha;
     property public abstract float edgeScale;
     property public abstract float maxElementHeight;
@@ -567,7 +570,8 @@
   public final class ScrollAwayKt {
     method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.ScrollState scrollState, optional float offset);
     method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.lazy.LazyListState scrollState, optional int itemIndex, optional float offset);
-    method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.material.ScalingLazyListState scrollState, optional int itemIndex, optional float offset);
+    method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollState, optional int itemIndex, optional float offset);
+    method @Deprecated public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.material.ScalingLazyListState scrollState, optional int itemIndex, optional float offset);
   }
 
   @androidx.compose.runtime.Immutable public final class Shapes {
@@ -606,8 +610,10 @@
   }
 
   public final class StepperKt {
-    method @androidx.compose.runtime.Composable public static void Stepper(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, int steps, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Stepper(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, int steps, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional long backgroundColor, optional long contentColor, optional long iconColor, optional boolean enableRangeSemantics, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, optional boolean enableRangeSemantics, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Stepper(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends kotlin.Unit> onValueChange, int steps, kotlin.jvm.functions.Function0<? extends kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<? extends kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<? extends kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<? extends kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit> content);
   }
 
   @androidx.compose.runtime.Immutable @androidx.wear.compose.material.ExperimentalWearMaterialApi public final class SwipeProgress<T> {
@@ -884,13 +890,17 @@
   }
 
   public final class DialogKt {
-    method @androidx.compose.runtime.Composable public static void Alert(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit> negativeButton, kotlin.jvm.functions.Function0<kotlin.Unit> positiveButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? icon, optional androidx.wear.compose.material.ScalingLazyListState scrollState, optional long backgroundColor, optional long contentColor, optional long titleColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? content);
-    method @androidx.compose.runtime.Composable public static void Alert(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? icon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? message, optional androidx.wear.compose.material.ScalingLazyListState scrollState, optional long backgroundColor, optional long titleColor, optional long messageColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void Confirmation(kotlin.jvm.functions.Function0<kotlin.Unit> onTimeout, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? icon, optional androidx.wear.compose.material.ScalingLazyListState scrollState, optional long durationMillis, optional long backgroundColor, optional long contentColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Alert(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit> negativeButton, kotlin.jvm.functions.Function0<kotlin.Unit> positiveButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? icon, optional androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollState, optional long backgroundColor, optional long contentColor, optional long titleColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? content);
+    method @androidx.compose.runtime.Composable public static void Alert(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? icon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? message, optional androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollState, optional long backgroundColor, optional long titleColor, optional long messageColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Alert(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> title, kotlin.jvm.functions.Function0<? extends kotlin.Unit> negativeButton, kotlin.jvm.functions.Function0<? extends kotlin.Unit> positiveButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit>? icon, optional androidx.wear.compose.material.ScalingLazyListState scrollState, optional long backgroundColor, optional long contentColor, optional long titleColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit>? content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Alert(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit>? icon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit>? message, optional androidx.wear.compose.material.ScalingLazyListState scrollState, optional long backgroundColor, optional long titleColor, optional long messageColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,? extends kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Confirmation(kotlin.jvm.functions.Function0<kotlin.Unit> onTimeout, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? icon, optional androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollState, optional long durationMillis, optional long backgroundColor, optional long contentColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Confirmation(kotlin.jvm.functions.Function0<? extends kotlin.Unit> onTimeout, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit>? icon, optional androidx.wear.compose.material.ScalingLazyListState scrollState, optional long durationMillis, optional long backgroundColor, optional long contentColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> content);
   }
 
   public final class Dialog_androidKt {
-    method @androidx.compose.runtime.Composable public static void Dialog(boolean showDialog, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState? scrollState, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Dialog(boolean showDialog, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.lazy.ScalingLazyListState? scrollState, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Dialog(boolean showDialog, kotlin.jvm.functions.Function0<? extends kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState? scrollState, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function0<? extends kotlin.Unit> content);
   }
 
 }
diff --git a/wear/compose/compose-material/api/restricted_current.ignore b/wear/compose/compose-material/api/restricted_current.ignore
index 82c32a0..05e58f9 100644
--- a/wear/compose/compose-material/api/restricted_current.ignore
+++ b/wear/compose/compose-material/api/restricted_current.ignore
@@ -1,7 +1,3 @@
 // Baseline format: 1.0
 ChangedValue: androidx.wear.compose.material.TimeTextDefaults#TimeFormat12Hours:
     Field androidx.wear.compose.material.TimeTextDefaults.TimeFormat12Hours has changed value from h:mm a to h:mm
-
-
-InvalidNullConversion: androidx.wear.compose.material.PickerKt#Picker(androidx.wear.compose.material.PickerState, String, androidx.compose.ui.Modifier, boolean, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit>, kotlin.jvm.functions.Function0<? extends kotlin.Unit>, androidx.wear.compose.material.ScalingParams, float, float, long, androidx.compose.foundation.gestures.FlingBehavior, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,? extends kotlin.Unit>) parameter #1:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter contentDescription in androidx.wear.compose.material.PickerKt.Picker(androidx.wear.compose.material.PickerState state, String contentDescription, androidx.compose.ui.Modifier modifier, boolean readOnly, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit> readOnlyLabel, kotlin.jvm.functions.Function0<? extends kotlin.Unit> onSelected, androidx.wear.compose.material.ScalingParams scalingParams, float separation, float gradientRatio, long gradientColor, androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,? extends kotlin.Unit> option)
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index f728567..445b1fc 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -4,8 +4,8 @@
   public final class AnimationKt {
   }
 
-  @androidx.compose.runtime.Immutable public final class AutoCenteringParams {
-    ctor public AutoCenteringParams(optional int itemIndex, optional int itemOffset);
+  @Deprecated @androidx.compose.runtime.Immutable public final class AutoCenteringParams {
+    ctor @Deprecated public AutoCenteringParams(optional int itemIndex, optional int itemOffset);
   }
 
   @androidx.compose.runtime.Stable public interface ButtonBorder {
@@ -262,17 +262,19 @@
   }
 
   public final class PickerDefaults {
+    method public androidx.wear.compose.foundation.lazy.ScalingParams defaultScalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior(androidx.wear.compose.material.PickerState state, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decay);
     method public float getDefaultGradientRatio();
-    method public androidx.wear.compose.material.ScalingParams scalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
+    method @Deprecated public androidx.wear.compose.material.ScalingParams scalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
     property public final float DefaultGradientRatio;
     field public static final androidx.wear.compose.material.PickerDefaults INSTANCE;
   }
 
   public final class PickerKt {
-    method @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material.PickerState state, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit> onSelected, optional androidx.wear.compose.material.ScalingParams scalingParams, optional float separation, optional float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,kotlin.Unit> option);
+    method @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material.PickerState state, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit> onSelected, optional androidx.wear.compose.foundation.lazy.ScalingParams scalingParams, optional float separation, optional float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,kotlin.Unit> option);
     method @Deprecated @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material.PickerState state, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? readOnlyLabel, optional androidx.wear.compose.material.ScalingParams scalingParams, optional float separation, optional float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,kotlin.Unit> option);
-    method @Deprecated @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material.PickerState state, String contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit> onSelected, optional androidx.wear.compose.material.ScalingParams scalingParams, optional float separation, optional float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,? extends kotlin.Unit> option);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material.PickerState state, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit> onSelected, optional androidx.wear.compose.material.ScalingParams scalingParams, optional float separation, optional float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,? extends kotlin.Unit> option);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Picker(androidx.wear.compose.material.PickerState state, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit>? readOnlyLabel, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit> onSelected, optional androidx.wear.compose.material.ScalingParams scalingParams, optional float separation, optional float gradientRatio, optional long gradientColor, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.PickerScope,? super java.lang.Integer,? extends kotlin.Unit> option);
     method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.PickerState rememberPickerState(int initialNumberOfOptions, optional int initiallySelectedOption, optional boolean repeatItems);
   }
 
@@ -325,7 +327,8 @@
 
   public final class PositionIndicatorKt {
     method @androidx.compose.runtime.Composable public static void PositionIndicator(androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.Modifier modifier, optional boolean reverseDirection);
-    method @androidx.compose.runtime.Composable public static void PositionIndicator(androidx.wear.compose.material.ScalingLazyListState scalingLazyListState, optional androidx.compose.ui.Modifier modifier, optional boolean reverseDirection);
+    method @androidx.compose.runtime.Composable public static void PositionIndicator(androidx.wear.compose.foundation.lazy.ScalingLazyListState scalingLazyListState, optional androidx.compose.ui.Modifier modifier, optional boolean reverseDirection);
+    method @Deprecated @androidx.compose.runtime.Composable public static void PositionIndicator(androidx.wear.compose.material.ScalingLazyListState scalingLazyListState, optional androidx.compose.ui.Modifier modifier, optional boolean reverseDirection);
     method @androidx.compose.runtime.Composable public static void PositionIndicator(androidx.compose.foundation.lazy.LazyListState lazyListState, optional androidx.compose.ui.Modifier modifier, optional boolean reverseDirection);
     method @androidx.compose.runtime.Composable public static void PositionIndicator(kotlin.jvm.functions.Function0<java.lang.Float> value, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> range, optional long color, optional boolean reverseDirection, optional int position);
     method @androidx.compose.runtime.Composable public static void PositionIndicator(androidx.wear.compose.material.PositionIndicatorState state, float indicatorHeight, float indicatorWidth, float paddingHorizontal, optional androidx.compose.ui.Modifier modifier, optional long background, optional long color, optional boolean reverseDirection, optional int position);
@@ -384,43 +387,43 @@
     method @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? vignette, optional kotlin.jvm.functions.Function0<kotlin.Unit>? positionIndicator, optional kotlin.jvm.functions.Function0<kotlin.Unit>? pageIndicator, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
-  public final class ScalingLazyColumnDefaults {
-    method public androidx.wear.compose.material.ScalingParams scalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
-    method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior snapFlingBehavior(androidx.wear.compose.material.ScalingLazyListState state, optional float snapOffset, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decay);
-    field public static final androidx.wear.compose.material.ScalingLazyColumnDefaults INSTANCE;
+  @Deprecated public final class ScalingLazyColumnDefaults {
+    method @Deprecated public androidx.wear.compose.material.ScalingParams scalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior snapFlingBehavior(androidx.wear.compose.material.ScalingLazyListState state, optional float snapOffset, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decay);
+    field @Deprecated public static final androidx.wear.compose.material.ScalingLazyColumnDefaults INSTANCE;
   }
 
   public final class ScalingLazyColumnKt {
-    method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.material.ScalingParams scalingParams, optional int anchorType, optional androidx.wear.compose.material.AutoCenteringParams? autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
-    method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
-    method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
-    method public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
-    method public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method @Deprecated @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.material.ScalingParams scalingParams, optional int anchorType, optional androidx.wear.compose.material.AutoCenteringParams? autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
+    method @Deprecated public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
+    method @Deprecated public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
+    method @Deprecated public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method @Deprecated public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
   public final class ScalingLazyColumnMeasureKt {
   }
 
-  @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class ScalingLazyListAnchorType {
-    field public static final androidx.wear.compose.material.ScalingLazyListAnchorType.Companion Companion;
+  @Deprecated @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class ScalingLazyListAnchorType {
+    field @Deprecated public static final androidx.wear.compose.material.ScalingLazyListAnchorType.Companion Companion;
   }
 
-  public static final class ScalingLazyListAnchorType.Companion {
-    method public int getItemCenter();
-    method public int getItemStart();
+  @Deprecated public static final class ScalingLazyListAnchorType.Companion {
+    method @Deprecated public int getItemCenter();
+    method @Deprecated public int getItemStart();
     property public final int ItemCenter;
     property public final int ItemStart;
   }
 
-  public sealed interface ScalingLazyListItemInfo {
-    method public float getAlpha();
-    method public int getIndex();
-    method public Object getKey();
-    method public int getOffset();
-    method public float getScale();
-    method public int getSize();
-    method public int getUnadjustedOffset();
-    method public int getUnadjustedSize();
+  @Deprecated public sealed interface ScalingLazyListItemInfo {
+    method @Deprecated public float getAlpha();
+    method @Deprecated public int getIndex();
+    method @Deprecated public Object getKey();
+    method @Deprecated public int getOffset();
+    method @Deprecated public float getScale();
+    method @Deprecated public int getSize();
+    method @Deprecated public int getUnadjustedOffset();
+    method @Deprecated public int getUnadjustedSize();
     property public abstract float alpha;
     property public abstract int index;
     property public abstract Object key;
@@ -431,24 +434,24 @@
     property public abstract int unadjustedSize;
   }
 
-  @androidx.compose.runtime.Stable @androidx.wear.compose.material.ScalingLazyScopeMarker public sealed interface ScalingLazyListItemScope {
-    method public androidx.compose.ui.Modifier fillParentMaxHeight(androidx.compose.ui.Modifier, optional float fraction);
-    method public androidx.compose.ui.Modifier fillParentMaxSize(androidx.compose.ui.Modifier, optional float fraction);
-    method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional float fraction);
+  @Deprecated @androidx.compose.runtime.Stable @androidx.wear.compose.material.ScalingLazyScopeMarker public sealed interface ScalingLazyListItemScope {
+    method @Deprecated public androidx.compose.ui.Modifier fillParentMaxHeight(androidx.compose.ui.Modifier, optional float fraction);
+    method @Deprecated public androidx.compose.ui.Modifier fillParentMaxSize(androidx.compose.ui.Modifier, optional float fraction);
+    method @Deprecated public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional float fraction);
   }
 
-  public sealed interface ScalingLazyListLayoutInfo {
-    method public int getAfterAutoCenteringPadding();
-    method public int getAfterContentPadding();
-    method public int getBeforeAutoCenteringPadding();
-    method public int getBeforeContentPadding();
-    method public androidx.compose.foundation.gestures.Orientation getOrientation();
-    method public boolean getReverseLayout();
-    method public int getTotalItemsCount();
-    method public int getViewportEndOffset();
-    method public long getViewportSize();
-    method public int getViewportStartOffset();
-    method public java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> getVisibleItemsInfo();
+  @Deprecated public sealed interface ScalingLazyListLayoutInfo {
+    method @Deprecated public int getAfterAutoCenteringPadding();
+    method @Deprecated public int getAfterContentPadding();
+    method @Deprecated public int getBeforeAutoCenteringPadding();
+    method @Deprecated public int getBeforeContentPadding();
+    method @Deprecated public androidx.compose.foundation.gestures.Orientation getOrientation();
+    method @Deprecated public boolean getReverseLayout();
+    method @Deprecated public int getTotalItemsCount();
+    method @Deprecated public int getViewportEndOffset();
+    method @Deprecated public long getViewportSize();
+    method @Deprecated public int getViewportStartOffset();
+    method @Deprecated public java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> getVisibleItemsInfo();
     property public abstract int afterAutoCenteringPadding;
     property public abstract int afterContentPadding;
     property public abstract int beforeAutoCenteringPadding;
@@ -462,51 +465,51 @@
     property public abstract java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> visibleItemsInfo;
   }
 
-  @androidx.wear.compose.material.ScalingLazyScopeMarker public sealed interface ScalingLazyListScope {
-    method public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListItemScope,kotlin.Unit> content);
-    method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+  @Deprecated @androidx.wear.compose.material.ScalingLazyScopeMarker public sealed interface ScalingLazyListScope {
+    method @Deprecated public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListItemScope,kotlin.Unit> content);
+    method @Deprecated public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
   }
 
-  @androidx.compose.runtime.Stable public final class ScalingLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
-    ctor public ScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
-    method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public float dispatchRawDelta(float delta);
-    method public int getCenterItemIndex();
-    method public int getCenterItemScrollOffset();
-    method public androidx.wear.compose.material.ScalingLazyListLayoutInfo getLayoutInfo();
-    method public boolean isScrollInProgress();
-    method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+  @Deprecated @androidx.compose.runtime.Stable public final class ScalingLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
+    ctor @Deprecated public ScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
+    method @Deprecated public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @Deprecated public float dispatchRawDelta(float delta);
+    method @Deprecated public int getCenterItemIndex();
+    method @Deprecated public int getCenterItemScrollOffset();
+    method @Deprecated public androidx.wear.compose.material.ScalingLazyListLayoutInfo getLayoutInfo();
+    method @Deprecated public boolean isScrollInProgress();
+    method @Deprecated public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @Deprecated public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
     property public boolean canScrollBackward;
     property public boolean canScrollForward;
     property public final int centerItemIndex;
     property public final int centerItemScrollOffset;
     property public boolean isScrollInProgress;
     property public final androidx.wear.compose.material.ScalingLazyListLayoutInfo layoutInfo;
-    field public static final androidx.wear.compose.material.ScalingLazyListState.Companion Companion;
+    field @Deprecated public static final androidx.wear.compose.material.ScalingLazyListState.Companion Companion;
   }
 
-  public static final class ScalingLazyListState.Companion {
-    method public androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyListState,java.lang.Object> getSaver();
+  @Deprecated public static final class ScalingLazyListState.Companion {
+    method @Deprecated public androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyListState,java.lang.Object> getSaver();
     property public final androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyListState,java.lang.Object> Saver;
   }
 
   public final class ScalingLazyListStateKt {
-    method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyListState rememberScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
+    method @Deprecated @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyListState rememberScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
   }
 
-  @kotlin.DslMarker public @interface ScalingLazyScopeMarker {
+  @Deprecated @kotlin.DslMarker public @interface ScalingLazyScopeMarker {
   }
 
-  @androidx.compose.runtime.Stable public interface ScalingParams {
-    method public float getEdgeAlpha();
-    method public float getEdgeScale();
-    method public float getMaxElementHeight();
-    method public float getMaxTransitionArea();
-    method public float getMinElementHeight();
-    method public float getMinTransitionArea();
-    method public androidx.compose.animation.core.Easing getScaleInterpolator();
-    method public int resolveViewportVerticalOffset(long viewportConstraints);
+  @Deprecated @androidx.compose.runtime.Stable public interface ScalingParams {
+    method @Deprecated public float getEdgeAlpha();
+    method @Deprecated public float getEdgeScale();
+    method @Deprecated public float getMaxElementHeight();
+    method @Deprecated public float getMaxTransitionArea();
+    method @Deprecated public float getMinElementHeight();
+    method @Deprecated public float getMinTransitionArea();
+    method @Deprecated public androidx.compose.animation.core.Easing getScaleInterpolator();
+    method @Deprecated public int resolveViewportVerticalOffset(long viewportConstraints);
     property public abstract float edgeAlpha;
     property public abstract float edgeScale;
     property public abstract float maxElementHeight;
@@ -519,7 +522,8 @@
   public final class ScrollAwayKt {
     method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.ScrollState scrollState, optional float offset);
     method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.lazy.LazyListState scrollState, optional int itemIndex, optional float offset);
-    method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.material.ScalingLazyListState scrollState, optional int itemIndex, optional float offset);
+    method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollState, optional int itemIndex, optional float offset);
+    method @Deprecated public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.material.ScalingLazyListState scrollState, optional int itemIndex, optional float offset);
   }
 
   @androidx.compose.runtime.Immutable public final class Shapes {
@@ -558,8 +562,10 @@
   }
 
   public final class StepperKt {
-    method @androidx.compose.runtime.Composable public static void Stepper(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, int steps, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Stepper(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, int steps, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional long backgroundColor, optional long contentColor, optional long iconColor, optional boolean enableRangeSemantics, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, optional boolean enableRangeSemantics, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Stepper(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends kotlin.Unit> onValueChange, int steps, kotlin.jvm.functions.Function0<? extends kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<? extends kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<? extends kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<? extends kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit> content);
   }
 
   public final class SwipeToDismissBoxDefaults {
@@ -783,13 +789,17 @@
   }
 
   public final class DialogKt {
-    method @androidx.compose.runtime.Composable public static void Alert(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit> negativeButton, kotlin.jvm.functions.Function0<kotlin.Unit> positiveButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? icon, optional androidx.wear.compose.material.ScalingLazyListState scrollState, optional long backgroundColor, optional long contentColor, optional long titleColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? content);
-    method @androidx.compose.runtime.Composable public static void Alert(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? icon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? message, optional androidx.wear.compose.material.ScalingLazyListState scrollState, optional long backgroundColor, optional long titleColor, optional long messageColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void Confirmation(kotlin.jvm.functions.Function0<kotlin.Unit> onTimeout, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? icon, optional androidx.wear.compose.material.ScalingLazyListState scrollState, optional long durationMillis, optional long backgroundColor, optional long contentColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Alert(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit> negativeButton, kotlin.jvm.functions.Function0<kotlin.Unit> positiveButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? icon, optional androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollState, optional long backgroundColor, optional long contentColor, optional long titleColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? content);
+    method @androidx.compose.runtime.Composable public static void Alert(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? icon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? message, optional androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollState, optional long backgroundColor, optional long titleColor, optional long messageColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Alert(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> title, kotlin.jvm.functions.Function0<? extends kotlin.Unit> negativeButton, kotlin.jvm.functions.Function0<? extends kotlin.Unit> positiveButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit>? icon, optional androidx.wear.compose.material.ScalingLazyListState scrollState, optional long backgroundColor, optional long contentColor, optional long titleColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit>? content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Alert(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit>? icon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit>? message, optional androidx.wear.compose.material.ScalingLazyListState scrollState, optional long backgroundColor, optional long titleColor, optional long messageColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,? extends kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Confirmation(kotlin.jvm.functions.Function0<kotlin.Unit> onTimeout, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? icon, optional androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollState, optional long durationMillis, optional long backgroundColor, optional long contentColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Confirmation(kotlin.jvm.functions.Function0<? extends kotlin.Unit> onTimeout, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit>? icon, optional androidx.wear.compose.material.ScalingLazyListState scrollState, optional long durationMillis, optional long backgroundColor, optional long contentColor, optional long iconColor, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> content);
   }
 
   public final class Dialog_androidKt {
-    method @androidx.compose.runtime.Composable public static void Dialog(boolean showDialog, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState? scrollState, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Dialog(boolean showDialog, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.lazy.ScalingLazyListState? scrollState, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Dialog(boolean showDialog, kotlin.jvm.functions.Function0<? extends kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState? scrollState, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function0<? extends kotlin.Unit> content);
   }
 
 }
diff --git a/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ScalingLazyColumnBenchmark.kt b/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ScalingLazyColumnBenchmark.kt
index dbb84ac..b352e1c 100644
--- a/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ScalingLazyColumnBenchmark.kt
+++ b/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ScalingLazyColumnBenchmark.kt
@@ -88,6 +88,7 @@
     }
 }
 
+@Suppress("DEPRECATION")
 internal class ScalingLazyColumnTestCase : LayeredComposeTestCase() {
     private var itemSizeDp: Dp = 10.dp
     private var defaultItemSpacingDp: Dp = 4.dp
diff --git a/wear/compose/compose-material/samples/build.gradle b/wear/compose/compose-material/samples/build.gradle
index 0e71692..fc8c8f3 100644
--- a/wear/compose/compose-material/samples/build.gradle
+++ b/wear/compose/compose-material/samples/build.gradle
@@ -33,6 +33,7 @@
     implementation(project(":compose:foundation:foundation-layout"))
     implementation(project(":compose:runtime:runtime"))
     implementation(project(":compose:ui:ui"))
+    implementation(project(":compose:ui:ui-util"))
     implementation(project(":compose:ui:ui-text"))
     implementation(project(":compose:material:material-icons-core"))
     implementation(project(":wear:compose:compose-material"))
diff --git a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/DialogSample.kt b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/DialogSample.kt
index 7d233f6..e402617db 100644
--- a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/DialogSample.kt
+++ b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/DialogSample.kt
@@ -41,6 +41,7 @@
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
 import androidx.wear.compose.material.Button
 import androidx.wear.compose.material.ButtonDefaults
 import androidx.wear.compose.material.Chip
@@ -51,7 +52,6 @@
 import androidx.wear.compose.material.dialog.Alert
 import androidx.wear.compose.material.dialog.Confirmation
 import androidx.wear.compose.material.dialog.Dialog
-import androidx.wear.compose.material.rememberScalingLazyListState
 
 @Sampled
 @Composable
diff --git a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScaffoldSample.kt b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScaffoldSample.kt
index 31f3bd0..9aa2006 100644
--- a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScaffoldSample.kt
+++ b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScaffoldSample.kt
@@ -28,12 +28,12 @@
 import androidx.wear.compose.material.ChipDefaults
 import androidx.wear.compose.material.PositionIndicator
 import androidx.wear.compose.material.Scaffold
-import androidx.wear.compose.material.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material.Text
 import androidx.wear.compose.material.TimeText
 import androidx.wear.compose.material.Vignette
 import androidx.wear.compose.material.VignettePosition
-import androidx.wear.compose.material.rememberScalingLazyListState
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
 
 @SuppressLint("UnrememberedMutableState")
 @Sampled
diff --git a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScalingLazyColumnSample.kt b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScalingLazyColumnSample.kt
index c877c44..1826758 100644
--- a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScalingLazyColumnSample.kt
+++ b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScalingLazyColumnSample.kt
@@ -25,15 +25,15 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.dp
-import androidx.wear.compose.material.AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults
+import androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
 import androidx.wear.compose.material.Chip
 import androidx.wear.compose.material.ChipDefaults
 import androidx.wear.compose.material.ListHeader
-import androidx.wear.compose.material.ScalingLazyColumn
-import androidx.wear.compose.material.ScalingLazyColumnDefaults
-import androidx.wear.compose.material.ScalingLazyListAnchorType
 import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.rememberScalingLazyListState
 import kotlinx.coroutines.launch
 
 @Sampled
diff --git a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/StepperSample.kt b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/StepperSample.kt
index b87f558..ca2344c 100644
--- a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/StepperSample.kt
+++ b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/StepperSample.kt
@@ -17,15 +17,22 @@
 package androidx.wear.compose.material.samples
 
 import androidx.annotation.Sampled
+import androidx.compose.foundation.progressSemantics
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.disabled
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.setProgress
+import androidx.compose.ui.util.lerp
 import androidx.wear.compose.material.Icon
 import androidx.wear.compose.material.Stepper
 import androidx.wear.compose.material.StepperDefaults
 import androidx.wear.compose.material.Text
+import kotlin.math.roundToInt
 
 @Sampled
 @Composable
@@ -53,3 +60,70 @@
         valueProgression = 1..10
     ) { Text("Value: $value") }
 }
+
+@Sampled
+@Composable
+fun StepperWithoutRangeSemanticsSample() {
+    var value by remember { mutableStateOf(2f) }
+    Stepper(
+        value = value,
+        onValueChange = { value = it },
+        valueRange = 1f..4f,
+        increaseIcon = { Icon(StepperDefaults.Increase, "Increase") },
+        decreaseIcon = { Icon(StepperDefaults.Decrease, "Decrease") },
+        steps = 7,
+        enableRangeSemantics = false
+    ) { Text("Value: $value") }
+}
+
+@Sampled
+@Composable
+fun StepperWithCustomSemanticsSample() {
+    var value by remember { mutableStateOf(2f) }
+    val valueRange = 1f..4f
+    val onValueChange = { i: Float -> value = i }
+    val steps = 7
+
+    Stepper(
+        value = value,
+        onValueChange = onValueChange,
+        valueRange = valueRange,
+        modifier = Modifier.customSemantics(value, true, onValueChange, valueRange, steps),
+        increaseIcon = { Icon(StepperDefaults.Increase, "Increase") },
+        decreaseIcon = { Icon(StepperDefaults.Decrease, "Decrease") },
+        steps = steps,
+        enableRangeSemantics = false
+    ) { Text("Value: $value") }
+}
+
+// Declaring the custom semantics for StepperWithCustomSemanticsSample
+private fun Modifier.customSemantics(
+    value: Float,
+    enabled: Boolean,
+    onValueChange: (Float) -> Unit,
+    valueRange: ClosedFloatingPointRange<Float>,
+    steps: Int
+): Modifier = semantics(mergeDescendants = true) {
+
+    if (!enabled) disabled()
+    setProgress(
+        action = { targetValue ->
+            val newStepIndex = ((value - valueRange.start) /
+                (valueRange.endInclusive - valueRange.start) * (steps + 1))
+                .roundToInt().coerceIn(0, steps + 1)
+
+            if (value.toInt() == newStepIndex) {
+                false
+            } else {
+                onValueChange(targetValue)
+                true
+            }
+        }
+    )
+}.progressSemantics(
+    lerp(
+        valueRange.start, valueRange.endInclusive,
+        value / (steps + 1).toFloat()
+    ).coerceIn(valueRange),
+    valueRange, steps
+)
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ButtonTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ButtonTest.kt
index b537b76..91b3c27 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ButtonTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ButtonTest.kt
@@ -178,7 +178,7 @@
     }
 
     @Test
-    public fun is_correctly_enabled_when_enabled_equals_true() {
+    public fun is_correctly_enabled() {
         rule.setContentWithTheme {
             Button(
                 onClick = {},
@@ -193,7 +193,7 @@
     }
 
     @Test
-    public fun is_correctly_disabled_when_enabled_equals_false() {
+    public fun is_correctly_disabled() {
         rule.setContentWithTheme {
             Button(
                 onClick = {},
@@ -208,7 +208,7 @@
     }
 
     @Test
-    public fun responds_to_click_when_enabled_on_compact_button() {
+    public fun compact_button_responds_to_click_when_enabled() {
         var clicked = false
 
         rule.setContentWithTheme {
@@ -229,7 +229,7 @@
     }
 
     @Test
-    public fun responds_to_click_when_enabled_on_button() {
+    public fun button_responds_to_click_when_enabled() {
         var clicked = false
 
         rule.setContentWithTheme {
@@ -250,7 +250,7 @@
     }
 
     @Test
-    public fun does_not_respond_to_click_when_disabled_on_compact_button() {
+    public fun compact_button_does_not_respond_to_click_when_disabled() {
         var clicked = false
 
         rule.setContentWithTheme {
@@ -271,7 +271,7 @@
     }
 
     @Test
-    public fun does_not_respond_to_click_when_disabled_on_button() {
+    public fun button_does_not_respond_to_click_when_disabled() {
         var clicked = false
 
         rule.setContentWithTheme {
@@ -1021,12 +1021,14 @@
 
     setContentWithTheme {
         background = MaterialTheme.colors.surface
-        buttonColor = MaterialTheme.colors.primary
-        content(
-            Modifier
-                .testTag(TEST_TAG)
-                .padding(padding)
-                .background(background))
+        Box(Modifier.background(background)) {
+            buttonColor = MaterialTheme.colors.primary
+            content(
+                Modifier
+                    .testTag(TEST_TAG)
+                    .padding(padding)
+            )
+        }
     }
 
     onNodeWithTag(TEST_TAG)
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
index ac201f8..7829b10 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
@@ -43,7 +43,6 @@
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
@@ -180,23 +179,44 @@
         assertThat(state.selectedOption).isEqualTo(numberOfOptions - 1)
     }
 
-    @SdkSuppress(minSdkVersion = 29) // b/260234023
     @Test
     fun uses_positive_separation_correctly() =
-        uses_separation_correctly(1)
+        scroll_with_separation(1)
 
     @Test
     fun uses_negative_separation_correctly() =
-        uses_separation_correctly(-1)
+        scroll_with_separation(-1)
 
-    private fun uses_separation_correctly(separationSign: Int) {
+    /**
+     * Test that picker is properly scrolled with scrollOffset, which equals to a half of the
+     * (itemSizePx + separationPx) and minus 1 pixel
+     * for making it definitely less than a half of an item
+     */
+    @Test
+    fun scroll_with_positive_separation_and_offset() =
+        scroll_with_separation(1, scrollOffset = (itemSizePx + separationPx) / 2 - 1)
+
+    /**
+     * Test that picker is properly scrolled with scrollOffset, which equals to a half of the
+     * (itemSizePx - separationPx) and minus 1 pixel
+     * for making it definitely less than a half of an item
+     */
+    @Test
+    fun scroll_with_negative_separation_and_offset() =
+        scroll_with_separation(-1, scrollOffset = (itemSizePx - separationPx) / 2 - 1)
+
+    private fun scroll_with_separation(
+        separationSign: Int,
+        scrollOffset: Int = 0
+    ) {
         lateinit var state: PickerState
         rule.setContent {
             WithTouchSlop(0f) {
                 Picker(
                     state = rememberPickerState(20).also { state = it },
                     contentDescription = CONTENT_DESCRIPTION,
-                    modifier = Modifier.testTag(TEST_TAG)
+                    modifier = Modifier
+                        .testTag(TEST_TAG)
                         .requiredSize(itemSizeDp * 11 + separationDp * 10 * separationSign),
                     separation = separationDp * separationSign
                 ) {
@@ -209,12 +229,14 @@
         val itemsToScroll = 4
 
         rule.onNodeWithTag(TEST_TAG).performTouchInput {
-            // Start at bottom - 2 to allow for 1.dp padding around the Picker
+            // Start at bottom - 5 to allow for around 2.dp padding around the Picker
             // (which was added to prevent jitter around the start of the gradient).
             swipeWithVelocity(
-                start = Offset(centerX, bottom - 2),
-                end = Offset(centerX, bottom - 2 -
-                    (itemSizePx + separationPx * separationSign) * itemsToScroll),
+                start = Offset(centerX, bottom - 5),
+                end = Offset(
+                    centerX, bottom - 5 - scrollOffset -
+                        (itemSizePx + separationPx * separationSign) * itemsToScroll
+                ),
                 endVelocity = NOT_A_FLING_SPEED
             )
         }
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
index bb27e59..246fd6a 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
@@ -16,6 +16,13 @@
 
 package androidx.wear.compose.material
 
+import androidx.wear.compose.foundation.lazy.ScalingLazyListLayoutInfo as ScalingLazyListLayoutInfo
+import androidx.wear.compose.foundation.lazy.ScalingLazyListState as ScalingLazyListState
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState as rememberScalingLazyListState
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn as ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults as ScalingLazyColumnDefaults
+import androidx.wear.compose.foundation.lazy.AutoCenteringParams as AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.ScalingLazyListScope as ScalingLazyListScope
 import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.background
@@ -24,16 +31,19 @@
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.requiredHeight
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyListLayoutInfo
+import androidx.compose.foundation.lazy.LazyListScope
 import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -73,38 +83,9 @@
 
     @Test
     fun emptyScalingLazyColumnGivesCorrectPositionAndSize() {
-        lateinit var state: ScalingLazyListState
-        lateinit var positionIndicatorState: PositionIndicatorState
-        var viewPortHeight = 0
-        rule.setContent {
-            state = rememberScalingLazyListState()
-            positionIndicatorState = ScalingLazyColumnStateAdapter(state)
-            ScalingLazyColumn(
-                state = state,
-                verticalArrangement = Arrangement.spacedBy(itemSpacingDp),
-                modifier = Modifier
-                    .onSizeChanged { viewPortHeight = it.height }
-                    .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f)
-            ) {
-            }
-            PositionIndicator(
-                state = positionIndicatorState,
-                indicatorHeight = 50.dp,
-                indicatorWidth = 4.dp,
-                paddingHorizontal = 5.dp,
-            )
-        }
-
-        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
-        rule.waitUntil { state.initialized.value }
-        rule.runOnIdle {
-            assertThat(
-                positionIndicatorState.positionFraction
-            ).isEqualTo(0)
-            assertThat(
-                positionIndicatorState.sizeFraction(viewPortHeight.toFloat())
-            ).isEqualTo(1)
-        }
+        scalingLazyColumnNotLargeEnoughToScroll(
+            Arrangement.spacedBy(itemSpacingDp)
+        ) {}
     }
 
     @Test
@@ -118,46 +99,36 @@
     }
 
     private fun scalingLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSize: Dp) {
-        lateinit var state: ScalingLazyListState
-        lateinit var positionIndicatorState: PositionIndicatorState
-        var viewPortHeight = 0
-        rule.setContent {
-            state = rememberScalingLazyListState()
-            positionIndicatorState = ScalingLazyColumnStateAdapter(state)
-            ScalingLazyColumn(
-                state = state,
-                verticalArrangement = Arrangement.spacedBy(itemSpacingDp),
-                modifier = Modifier
-                    .onSizeChanged { viewPortHeight = it.height }
-                    .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f),
-                autoCentering = null
-            ) {
-                items(3) {
-                    Box(Modifier.requiredSize(itemSize))
-                }
+        scalingLazyColumnNotLargeEnoughToScroll(
+            Arrangement.spacedBy(itemSpacingDp),
+            autoCentering = null
+        ) {
+            items(3) {
+                Box(Modifier.requiredSize(itemSize))
             }
-            PositionIndicator(
-                state = positionIndicatorState,
-                indicatorHeight = 50.dp,
-                indicatorWidth = 4.dp,
-                paddingHorizontal = 5.dp,
-            )
-        }
-
-        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
-        rule.waitUntil { state.initialized.value }
-        rule.runOnIdle {
-            assertThat(
-                positionIndicatorState.positionFraction
-            ).isEqualTo(0f)
-            assertThat(
-                positionIndicatorState.sizeFraction(viewPortHeight.toFloat())
-            ).isEqualTo(1f)
         }
     }
 
     @Test
     fun scalingLazyColumnNotLargeEnoughToScrollSwapVerticalAlignmentGivesCorrectPositionAndSize() {
+        scalingLazyColumnNotLargeEnoughToScroll(
+            Arrangement.spacedBy(
+                space = itemSpacingDp,
+                alignment = Alignment.Bottom
+            )
+        ) {
+            items(3) {
+                Box(Modifier.requiredSize(itemSizeDp))
+            }
+        }
+    }
+
+    private fun scalingLazyColumnNotLargeEnoughToScroll(
+        verticalArrangement: Arrangement.Vertical,
+        reverseLayout: Boolean = false,
+        autoCentering: AutoCenteringParams? = AutoCenteringParams(),
+        content: ScalingLazyListScope.() -> Unit
+    ) {
         lateinit var state: ScalingLazyListState
         lateinit var positionIndicatorState: PositionIndicatorState
         var viewPortHeight = 0
@@ -166,17 +137,14 @@
             positionIndicatorState = ScalingLazyColumnStateAdapter(state)
             ScalingLazyColumn(
                 state = state,
-                verticalArrangement = Arrangement.spacedBy(
-                    space = itemSpacingDp,
-                    alignment = Alignment.Bottom
-                ),
+                verticalArrangement = verticalArrangement,
+                reverseLayout = reverseLayout,
                 modifier = Modifier
                     .onSizeChanged { viewPortHeight = it.height }
-                    .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f)
+                    .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f),
+                autoCentering = autoCentering
             ) {
-                items(3) {
-                    Box(Modifier.requiredSize(itemSizeDp))
-                }
+                content(this)
             }
             PositionIndicator(
                 state = positionIndicatorState,
@@ -186,8 +154,6 @@
             )
         }
 
-        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
-        rule.waitUntil { state.initialized.value }
         rule.runOnIdle {
             assertThat(
                 positionIndicatorState.positionFraction
@@ -250,7 +216,11 @@
                 }
             ) {
                 items(5) {
-                    Box(Modifier.requiredSize(itemSizeDp).border(BorderStroke(1.dp, Color.Green)))
+                    Box(
+                        Modifier
+                            .requiredSize(itemSizeDp)
+                            .border(BorderStroke(1.dp, Color.Green))
+                    )
                 }
             }
             PositionIndicator(
@@ -261,8 +231,6 @@
             )
         }
 
-        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
-        rule.waitUntil { state.initialized.value }
         rule.runOnIdle {
             // Scroll forwards so that item with index 2 is in the center of the viewport
             runBlocking {
@@ -286,84 +254,25 @@
 
     @Test
     fun emptyReverseLayoutScalingLazyColumnGivesCorrectPositionAndSize() {
-        lateinit var state: ScalingLazyListState
-        lateinit var positionIndicatorState: PositionIndicatorState
-        var viewPortHeight = 0
-        rule.setContent {
-            state = rememberScalingLazyListState()
-            positionIndicatorState = ScalingLazyColumnStateAdapter(state)
-            ScalingLazyColumn(
-                state = state,
-                verticalArrangement = Arrangement.spacedBy(itemSpacingDp),
-                reverseLayout = true,
-                modifier = Modifier
-                    .onSizeChanged { viewPortHeight = it.height }
-                    .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f)
-            ) {
-            }
-            PositionIndicator(
-                state = positionIndicatorState,
-                indicatorHeight = 50.dp,
-                indicatorWidth = 4.dp,
-                paddingHorizontal = 5.dp,
-            )
-        }
-
-        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
-        rule.waitUntil { state.initialized.value }
-        rule.runOnIdle {
-            assertThat(
-                positionIndicatorState.positionFraction
-            ).isEqualTo(0)
-            assertThat(
-                positionIndicatorState.sizeFraction(viewPortHeight.toFloat())
-            ).isEqualTo(1)
-        }
+        scalingLazyColumnNotLargeEnoughToScroll(
+            Arrangement.spacedBy(itemSpacingDp),
+            reverseLayout = true
+        ) {}
     }
 
     @Test
     fun reverseLayoutScalingLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize() {
-        lateinit var state: ScalingLazyListState
-        lateinit var positionIndicatorState: PositionIndicatorState
-        var viewPortHeight = 0
-        rule.setContent {
-            state = rememberScalingLazyListState()
-            positionIndicatorState = ScalingLazyColumnStateAdapter(state)
-            ScalingLazyColumn(
-                state = state,
-                verticalArrangement = Arrangement.spacedBy(
-                    space = itemSpacingDp,
-                    alignment = Alignment.Bottom
-                ),
-                reverseLayout = true,
-                modifier = Modifier
-                    .onSizeChanged { viewPortHeight = it.height }
-                    .fillMaxWidth()
-                    .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f)
-                    .background(Color.DarkGray),
-                autoCentering = null
-            ) {
-                items(3) {
-                    Box(Modifier.requiredSize(itemSizeDp))
-                }
+        scalingLazyColumnNotLargeEnoughToScroll(
+            Arrangement.spacedBy(
+                space = itemSpacingDp,
+                alignment = Alignment.Bottom
+            ),
+            autoCentering = null,
+            reverseLayout = true
+        ) {
+            items(3) {
+                Box(Modifier.requiredSize(itemSizeDp))
             }
-            PositionIndicator(
-                state = positionIndicatorState,
-                indicatorHeight = 50.dp,
-                indicatorWidth = 4.dp,
-                paddingHorizontal = 5.dp,
-            )
-        }
-
-        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
-        rule.waitUntil { state.initialized.value }
-        rule.runOnIdle {
-            assertThat(
-                positionIndicatorState.positionFraction
-            ).isEqualTo(0f)
-            assertThat(
-                positionIndicatorState.sizeFraction(viewPortHeight.toFloat())
-            ).isEqualTo(1f)
         }
     }
 
@@ -401,8 +310,6 @@
             )
         }
 
-        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
-        rule.waitUntil { state.initialized.value }
         rule.runOnIdle {
             runBlocking {
                 state.scrollBy(itemSizePx.toFloat() + itemSpacingPx.toFloat())
@@ -425,86 +332,52 @@
 
     @Test
     fun emptyLazyColumnGivesCorrectPositionAndSize() {
-        lateinit var state: LazyListState
-        lateinit var positionIndicatorState: PositionIndicatorState
-        var viewPortHeight = 0
-        rule.setContent {
-            state = rememberLazyListState()
-            positionIndicatorState = LazyColumnStateAdapter(state)
-            LazyColumn(
-                state = state,
-                verticalArrangement = Arrangement.spacedBy(itemSpacingDp),
-                modifier = Modifier
-                    .onSizeChanged { viewPortHeight = it.height }
-                    .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f)
-            ) {
-            }
-            PositionIndicator(
-                state = positionIndicatorState,
-                indicatorHeight = 50.dp,
-                indicatorWidth = 4.dp,
-                paddingHorizontal = 5.dp,
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(
-                positionIndicatorState.positionFraction
-            ).isEqualTo(0)
-            assertThat(
-                positionIndicatorState.sizeFraction(viewPortHeight.toFloat())
-            ).isEqualTo(1)
-        }
+        lazyColumnNotLargeEnoughToScroll(
+            Arrangement.spacedBy(itemSpacingDp)
+        ) {}
     }
 
     @Test
     fun lazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize() {
-        lazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSizeDp)
+        lazyColumnNotLargeEnoughToScroll(
+            Arrangement.spacedBy(itemSpacingDp)
+        ) {
+            items(3) {
+                Box(Modifier.requiredSize(itemSizeDp))
+            }
+        }
     }
 
     @Test
     fun lazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSizeForZeroSizeItems() {
-        lazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(0.dp)
-    }
-
-    private fun lazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSize: Dp) {
-        lateinit var state: LazyListState
-        lateinit var positionIndicatorState: PositionIndicatorState
-        var viewPortHeight = 0
-        rule.setContent {
-            state = rememberLazyListState()
-            positionIndicatorState = LazyColumnStateAdapter(state)
-            LazyColumn(
-                state = state,
-                verticalArrangement = Arrangement.spacedBy(itemSpacingDp),
-                modifier = Modifier
-                    .onSizeChanged { viewPortHeight = it.height }
-                    .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f)
-            ) {
-                items(3) {
-                    Box(Modifier.requiredSize(itemSize))
-                }
+        lazyColumnNotLargeEnoughToScroll(
+            Arrangement.spacedBy(itemSpacingDp)
+        ) {
+            items(3) {
+                Box(Modifier.requiredSize(0.dp))
             }
-            PositionIndicator(
-                state = positionIndicatorState,
-                indicatorHeight = 50.dp,
-                indicatorWidth = 4.dp,
-                paddingHorizontal = 5.dp,
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(
-                positionIndicatorState.positionFraction
-            ).isEqualTo(0f)
-            assertThat(
-                positionIndicatorState.sizeFraction(viewPortHeight.toFloat())
-            ).isEqualTo(1f)
         }
     }
 
     @Test
     fun lazyColumnNotLargeEnoughToScrollSwapVerticalAlignmentGivesCorrectPositionAndSize() {
+        lazyColumnNotLargeEnoughToScroll(
+            Arrangement.spacedBy(
+                space = itemSpacingDp,
+                alignment = Alignment.Bottom
+            )
+        ) {
+            items(3) {
+                Box(Modifier.requiredSize(itemSizeDp))
+            }
+        }
+    }
+
+    private fun lazyColumnNotLargeEnoughToScroll(
+        verticalArrangement: Arrangement.Vertical,
+        reverseLayout: Boolean = false,
+        content: LazyListScope.() -> Unit
+    ) {
         lateinit var state: LazyListState
         lateinit var positionIndicatorState: PositionIndicatorState
         var viewPortHeight = 0
@@ -513,17 +386,13 @@
             positionIndicatorState = LazyColumnStateAdapter(state)
             LazyColumn(
                 state = state,
-                verticalArrangement = Arrangement.spacedBy(
-                    space = itemSpacingDp,
-                    alignment = Alignment.Bottom
-                ),
+                verticalArrangement = verticalArrangement,
+                reverseLayout = reverseLayout,
                 modifier = Modifier
                     .onSizeChanged { viewPortHeight = it.height }
                     .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f)
             ) {
-                items(3) {
-                    Box(Modifier.requiredSize(itemSizeDp))
-                }
+                content()
             }
             PositionIndicator(
                 state = positionIndicatorState,
@@ -591,79 +460,24 @@
 
     @Test
     fun emptyReverseLayoutLazyColumnGivesCorrectPositionAndSize() {
-        lateinit var state: LazyListState
-        lateinit var positionIndicatorState: PositionIndicatorState
-        var viewPortHeight = 0
-        rule.setContent {
-            state = rememberLazyListState()
-            positionIndicatorState = LazyColumnStateAdapter(state)
-            LazyColumn(
-                state = state,
-                verticalArrangement = Arrangement.spacedBy(itemSpacingDp),
-                reverseLayout = true,
-                modifier = Modifier
-                    .onSizeChanged { viewPortHeight = it.height }
-                    .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f)
-            ) {
-            }
-            PositionIndicator(
-                state = positionIndicatorState,
-                indicatorHeight = 50.dp,
-                indicatorWidth = 4.dp,
-                paddingHorizontal = 5.dp,
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(
-                positionIndicatorState.positionFraction
-            ).isEqualTo(0)
-            assertThat(
-                positionIndicatorState.sizeFraction(viewPortHeight.toFloat())
-            ).isEqualTo(1)
-        }
+        lazyColumnNotLargeEnoughToScroll(
+            Arrangement.spacedBy(itemSpacingDp),
+            reverseLayout = true
+        ) {}
     }
 
     @Test
     fun reverseLayoutLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize() {
-        lateinit var state: LazyListState
-        lateinit var positionIndicatorState: PositionIndicatorState
-        var viewPortHeight = 0
-        rule.setContent {
-            state = rememberLazyListState()
-            positionIndicatorState = LazyColumnStateAdapter(state)
-            LazyColumn(
-                state = state,
-                verticalArrangement = Arrangement.spacedBy(
-                    space = itemSpacingDp,
-                    alignment = Alignment.Bottom
-                ),
-                reverseLayout = true,
-                modifier = Modifier
-                    .onSizeChanged { viewPortHeight = it.height }
-                    .fillMaxWidth()
-                    .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f)
-                    .background(Color.DarkGray),
-            ) {
-                items(3) {
-                    Box(Modifier.requiredSize(itemSizeDp))
-                }
+        lazyColumnNotLargeEnoughToScroll(
+            Arrangement.spacedBy(
+                space = itemSpacingDp,
+                alignment = Alignment.Bottom
+            ),
+            reverseLayout = true
+        ) {
+            items(3) {
+                Box(Modifier.requiredSize(itemSizeDp))
             }
-            PositionIndicator(
-                state = positionIndicatorState,
-                indicatorHeight = 50.dp,
-                indicatorWidth = 4.dp,
-                paddingHorizontal = 5.dp,
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(
-                positionIndicatorState.positionFraction
-            ).isEqualTo(0f)
-            assertThat(
-                positionIndicatorState.sizeFraction(viewPortHeight.toFloat())
-            ).isEqualTo(1f)
         }
     }
 
@@ -716,82 +530,56 @@
 
     @Test
     fun emptyScrollableColumnGivesCorrectPositionAndSize() {
-        lateinit var state: ScrollState
-        lateinit var positionIndicatorState: PositionIndicatorState
-        var viewPortHeight = 0
-        rule.setContent {
-            state = rememberScrollState()
-            positionIndicatorState = ScrollStateAdapter(scrollState = state)
-            Column(
-                modifier = Modifier
-                    .onSizeChanged { viewPortHeight = it.height }
-                    .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f)
-                    .verticalScroll(state = state)
-            ) {
-            }
-            PositionIndicator(
-                state = positionIndicatorState,
-                indicatorHeight = 50.dp,
-                indicatorWidth = 4.dp,
-                paddingHorizontal = 5.dp,
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(
-                positionIndicatorState.positionFraction
-            ).isEqualTo(0)
-            assertThat(
-                positionIndicatorState.sizeFraction(viewPortHeight.toFloat())
-            ).isEqualTo(1)
-        }
+        scrollableColumnNotLargeEnoughToScroll({})
     }
 
     @Test
     fun emptyReversedScrollableColumnGivesCorrectPositionAndSize() {
-        lateinit var state: ScrollState
-        lateinit var positionIndicatorState: PositionIndicatorState
-        var viewPortHeight = 0
-        rule.setContent {
-            state = rememberScrollState()
-            positionIndicatorState = ScrollStateAdapter(scrollState = state)
-            Column(
-                modifier = Modifier
-                    .onSizeChanged { viewPortHeight = it.height }
-                    .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f)
-                    .verticalScroll(state = state, reverseScrolling = true)
-            ) {
-            }
-            PositionIndicator(
-                state = positionIndicatorState,
-                indicatorHeight = 50.dp,
-                indicatorWidth = 4.dp,
-                paddingHorizontal = 5.dp,
-                reverseDirection = true,
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(
-                positionIndicatorState.positionFraction
-            ).isEqualTo(0)
-            assertThat(
-                positionIndicatorState.sizeFraction(viewPortHeight.toFloat())
-            ).isEqualTo(1)
-        }
+        scrollableColumnNotLargeEnoughToScroll({}, reverseScrolling = true)
     }
 
     @Test
     fun scrollableColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize() {
-        scrollableColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSizeDp)
+        scrollableColumnNotLargeEnoughToScroll(
+            {
+                Box(Modifier.requiredSize(itemSizeDp))
+                Box(Modifier.requiredSize(itemSizeDp))
+                Box(Modifier.requiredSize(itemSizeDp))
+            },
+            Arrangement.spacedBy(itemSpacingDp),
+        )
     }
 
     @Test
     fun scrollableColumnNotLargeEnoughToScrollGivesCorrectPositionAndSizeForZeroSizeItems() {
-        scrollableColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(0.dp)
+        scrollableColumnNotLargeEnoughToScroll(
+            {
+                Box(Modifier.requiredSize(0.dp))
+                Box(Modifier.requiredSize(0.dp))
+                Box(Modifier.requiredSize(0.dp))
+            },
+            Arrangement.spacedBy(itemSpacingDp),
+        )
     }
 
-    private fun scrollableColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSize: Dp) {
+    @Test
+    fun reversedScrollableColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize() {
+        scrollableColumnNotLargeEnoughToScroll(
+            {
+                Box(Modifier.requiredSize(itemSizeDp))
+                Box(Modifier.requiredSize(itemSizeDp))
+                Box(Modifier.requiredSize(itemSizeDp))
+            },
+            Arrangement.spacedBy(itemSpacingDp),
+            reverseScrolling = true
+        )
+    }
+
+    private fun scrollableColumnNotLargeEnoughToScroll(
+        columnContent: @Composable ColumnScope.() -> Unit,
+        verticalArrangement: Arrangement.Vertical = Arrangement.Top,
+        reverseScrolling: Boolean = false
+    ) {
         lateinit var state: ScrollState
         lateinit var positionIndicatorState: PositionIndicatorState
         var viewPortHeight = 0
@@ -802,66 +590,27 @@
                 modifier = Modifier
                     .onSizeChanged { viewPortHeight = it.height }
                     .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f)
-                    .verticalScroll(state = state),
-                verticalArrangement = Arrangement.spacedBy(itemSpacingDp)
+                    .verticalScroll(state = state, reverseScrolling = reverseScrolling),
+                verticalArrangement = verticalArrangement
             ) {
-                Box(Modifier.requiredSize(itemSize))
-                Box(Modifier.requiredSize(itemSize))
-                Box(Modifier.requiredSize(itemSize))
+                columnContent()
             }
             PositionIndicator(
                 state = positionIndicatorState,
                 indicatorHeight = 50.dp,
                 indicatorWidth = 4.dp,
                 paddingHorizontal = 5.dp,
+                reverseDirection = reverseScrolling
             )
         }
 
         rule.runOnIdle {
             assertThat(
                 positionIndicatorState.positionFraction
-            ).isEqualTo(0)
+            ).isEqualTo(0f)
             assertThat(
                 positionIndicatorState.sizeFraction(viewPortHeight.toFloat())
-            ).isEqualTo(1)
-        }
-    }
-
-    @Test
-    fun reversedScrollableColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize() {
-        lateinit var state: ScrollState
-        lateinit var positionIndicatorState: PositionIndicatorState
-        var viewPortHeight = 0
-        rule.setContent {
-            state = rememberScrollState()
-            positionIndicatorState = ScrollStateAdapter(scrollState = state)
-            Column(
-                modifier = Modifier
-                    .onSizeChanged { viewPortHeight = it.height }
-                    .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f)
-                    .verticalScroll(state = state, reverseScrolling = true),
-                verticalArrangement = Arrangement.spacedBy(itemSpacingDp)
-            ) {
-                Box(Modifier.requiredSize(itemSizeDp))
-                Box(Modifier.requiredSize(itemSizeDp))
-                Box(Modifier.requiredSize(itemSizeDp))
-            }
-            PositionIndicator(
-                state = positionIndicatorState,
-                indicatorHeight = 50.dp,
-                indicatorWidth = 4.dp,
-                paddingHorizontal = 5.dp,
-                reverseDirection = true,
-            )
-        }
-
-        rule.runOnIdle {
-            assertThat(
-                positionIndicatorState.positionFraction
-            ).isEqualTo(0)
-            assertThat(
-                positionIndicatorState.sizeFraction(viewPortHeight.toFloat())
-            ).isEqualTo(1)
+            ).isEqualTo(1f)
         }
     }
 
@@ -1011,7 +760,303 @@
     ) {
         assertThat(visibleItemsInfo.first().index).isEqualTo(firstItemIndex)
         assertThat(visibleItemsInfo.last().index).isEqualTo(lastItemIndex)
-        assertThat((viewPortHeight / 2f) >=
-            (visibleItemsInfo.last().offset + (visibleItemsInfo.last().size / 2)))
+        assertThat(
+            (viewPortHeight / 2f) >=
+                (visibleItemsInfo.last().offset + (visibleItemsInfo.last().size / 2))
+        )
     }
 }
+
+/**
+ * Tests for PositionIndicator api which uses deprecated ScalingLazyColumn
+ * from androidx.wear.compose.material package.
+ */
+@MediumTest
+@Suppress("DEPRECATION")
+@RunWith(AndroidJUnit4::class)
+public class PositionIndicatorWithMaterialSLCTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    private var itemSizePx: Int = 50
+    private var itemSizeDp: Dp = Dp.Infinity
+    private var itemSpacingPx = 6
+    private var itemSpacingDp: Dp = Dp.Infinity
+
+    @Before
+    fun before() {
+        with(rule.density) {
+            itemSizeDp = itemSizePx.toDp()
+            itemSpacingDp = itemSpacingPx.toDp()
+        }
+    }
+
+    @Test
+    fun emptyScalingLazyColumnGivesCorrectPositionAndSize() {
+        scalingLazyColumnNotLargeEnoughToScroll(
+            Arrangement.spacedBy(itemSpacingDp)
+        ) {}
+    }
+
+    @Test
+    fun scalingLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize() {
+        scalingLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSizeDp)
+    }
+
+    @Test
+    fun scalingLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSizeForZeroSizeItems() {
+        scalingLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(0.dp)
+    }
+
+    private fun scalingLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSize: Dp) {
+        scalingLazyColumnNotLargeEnoughToScroll(
+            Arrangement.spacedBy(itemSpacingDp),
+            autoCentering = null
+        ) {
+            items(3) {
+                Box(Modifier.requiredSize(itemSize))
+            }
+        }
+    }
+
+    @Test
+    fun scalingLazyColumnNotLargeEnoughToScrollSwapVerticalAlignmentGivesCorrectPositionAndSize() {
+        scalingLazyColumnNotLargeEnoughToScroll(
+            Arrangement.spacedBy(
+                space = itemSpacingDp,
+                alignment = Alignment.Bottom
+            )
+        ) {
+            items(3) {
+                Box(Modifier.requiredSize(itemSizeDp))
+            }
+        }
+    }
+
+    fun scrollableScalingLazyColumnGivesCorrectPositionAndSize() {
+        scrollableScalingLazyColumnPositionAndSize(
+            enableAutoCentering = true,
+            contentPaddingPx = 0
+        )
+    }
+
+    @Test
+    fun scrollableScalingLazyColumnGivesCorrectPositionAndSizeWithContentPadding() {
+        scrollableScalingLazyColumnPositionAndSize(
+            enableAutoCentering = true,
+            contentPaddingPx = itemSizePx + itemSpacingPx
+        )
+    }
+
+    @Test
+    fun scrollableScalingLazyColumnGivesCorrectPositionAndSizeWithContentPaddingNoAutoCenter() {
+        scrollableScalingLazyColumnPositionAndSize(
+            enableAutoCentering = false,
+            contentPaddingPx = itemSizePx + itemSpacingPx
+        )
+    }
+
+    private fun scrollableScalingLazyColumnPositionAndSize(
+        enableAutoCentering: Boolean,
+        contentPaddingPx: Int
+    ) {
+        lateinit var state: androidx.wear.compose.material.ScalingLazyListState
+        lateinit var positionIndicatorState: PositionIndicatorState
+        var viewPortHeight = 0
+        rule.setContent {
+            state =
+                androidx.wear.compose.material.rememberScalingLazyListState(
+                    initialCenterItemIndex = 0
+                )
+            positionIndicatorState = MaterialScalingLazyColumnStateAdapter(state)
+            ScalingLazyColumn(
+                state = state,
+                verticalArrangement = Arrangement.spacedBy(itemSpacingDp),
+                modifier = Modifier
+                    .onSizeChanged { viewPortHeight = it.height }
+                    .requiredHeight(
+                        // Exactly the right size to hold 3 items with spacing
+                        itemSizeDp * 3f + itemSpacingDp * 2f
+                    )
+                    .background(Color.Black),
+                scalingParams =
+                androidx.wear.compose.material.ScalingLazyColumnDefaults.scalingParams(
+                    edgeScale = 1.0f
+                ),
+                autoCentering = if (enableAutoCentering)
+                    androidx.wear.compose.material.AutoCenteringParams(itemIndex = 0) else null,
+                contentPadding = with(LocalDensity.current) {
+                    PaddingValues(contentPaddingPx.toDp())
+                }
+            ) {
+                items(5) {
+                    Box(
+                        Modifier
+                            .requiredSize(itemSizeDp)
+                            .border(BorderStroke(1.dp, Color.Green))
+                    )
+                }
+            }
+            PositionIndicator(
+                state = positionIndicatorState,
+                indicatorHeight = 50.dp,
+                indicatorWidth = 4.dp,
+                paddingHorizontal = 5.dp,
+            )
+        }
+
+        rule.runOnIdle {
+            // Scroll forwards so that item with index 2 is in the center of the viewport
+            runBlocking {
+                state.scrollBy((itemSizePx.toFloat() + itemSpacingPx.toFloat()) * 2f)
+            }
+
+            state.layoutInfo.assertWhollyVisibleItems(
+                firstItemIndex = 1, lastItemIndex = 3,
+                viewPortHeight = viewPortHeight
+            )
+
+            // And that the indicator is at position 0.5 and of expected size
+            assertThat(
+                positionIndicatorState.positionFraction
+            ).isWithin(0.05f).of(0.5f)
+            assertThat(
+                positionIndicatorState.sizeFraction(viewPortHeight.toFloat())
+            ).isWithin(0.05f).of(0.6f)
+        }
+    }
+
+    @Test
+    fun emptyReverseLayoutScalingLazyColumnGivesCorrectPositionAndSize() {
+        scalingLazyColumnNotLargeEnoughToScroll(
+            Arrangement.spacedBy(itemSpacingDp),
+            reverseLayout = true
+        ) {}
+    }
+
+    @Test
+    fun reverseLayoutScalingLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize() {
+        scalingLazyColumnNotLargeEnoughToScroll(
+            Arrangement.spacedBy(
+                space = itemSpacingDp,
+                alignment = Alignment.Bottom
+            ),
+            autoCentering = null,
+            reverseLayout = true
+        ) {
+            items(3) {
+                Box(Modifier.requiredSize(itemSizeDp))
+            }
+        }
+    }
+
+    private fun scalingLazyColumnNotLargeEnoughToScroll(
+        verticalArrangement: Arrangement.Vertical,
+        reverseLayout: Boolean = false,
+        autoCentering: androidx.wear.compose.material.AutoCenteringParams? =
+            androidx.wear.compose.material.AutoCenteringParams(),
+        slcContent: androidx.wear.compose.material.ScalingLazyListScope.() -> Unit
+    ) {
+        lateinit var state: androidx.wear.compose.material.ScalingLazyListState
+        lateinit var positionIndicatorState: PositionIndicatorState
+        var viewPortHeight = 0
+        rule.setContent {
+            state = androidx.wear.compose.material.rememberScalingLazyListState()
+            positionIndicatorState = MaterialScalingLazyColumnStateAdapter(state)
+            ScalingLazyColumn(
+                state = state,
+                verticalArrangement = verticalArrangement,
+                reverseLayout = reverseLayout,
+                modifier = Modifier
+                    .onSizeChanged { viewPortHeight = it.height }
+                    .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f),
+                autoCentering = autoCentering
+            ) {
+                slcContent(this)
+            }
+            PositionIndicator(
+                state = positionIndicatorState,
+                indicatorHeight = 50.dp,
+                indicatorWidth = 4.dp,
+                paddingHorizontal = 5.dp,
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(
+                positionIndicatorState.positionFraction
+            ).isEqualTo(0f)
+            assertThat(
+                positionIndicatorState.sizeFraction(viewPortHeight.toFloat())
+            ).isEqualTo(1f)
+        }
+    }
+
+    @Test
+    fun reverseLayoutScrollableScalingLazyColumnGivesCorrectPositionAndSize() {
+        lateinit var state: androidx.wear.compose.material.ScalingLazyListState
+        lateinit var positionIndicatorState: PositionIndicatorState
+        var viewPortHeight = 0
+        rule.setContent {
+            state = androidx.wear.compose.material.rememberScalingLazyListState()
+            positionIndicatorState = MaterialScalingLazyColumnStateAdapter(state)
+            ScalingLazyColumn(
+                state = state,
+                verticalArrangement = Arrangement.spacedBy(itemSpacingDp),
+                reverseLayout = true,
+                modifier = Modifier
+                    .onSizeChanged { viewPortHeight = it.height }
+                    .requiredHeight(
+                        // Exactly the right size to hold 3 items with spacing
+                        itemSizeDp * 3f + itemSpacingDp * 2f
+                    )
+                    .background(Color.DarkGray),
+                scalingParams = androidx.wear.compose.material.ScalingLazyColumnDefaults
+                    .scalingParams(edgeScale = 1.0f),
+                autoCentering = null
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+            PositionIndicator(
+                state = positionIndicatorState,
+                indicatorHeight = 50.dp,
+                indicatorWidth = 4.dp,
+                paddingHorizontal = 5.dp,
+            )
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(itemSizePx.toFloat() + itemSpacingPx.toFloat())
+            }
+
+            state.layoutInfo.assertWhollyVisibleItems(
+                firstItemIndex = 1, lastItemIndex = 3,
+                viewPortHeight = viewPortHeight
+            )
+
+            // And that the indicator is at position 0.5 and of expected size
+            assertThat(
+                positionIndicatorState.positionFraction
+            ).isWithin(0.05f).of(0.5f)
+            assertThat(
+                positionIndicatorState.sizeFraction(viewPortHeight.toFloat())
+            ).isWithin(0.05f).of(0.6f)
+        }
+    }
+
+    private fun androidx.wear.compose.material.ScalingLazyListLayoutInfo.assertWhollyVisibleItems(
+        firstItemIndex: Int,
+        lastItemIndex: Int,
+        viewPortHeight: Int
+    ) {
+        assertThat(visibleItemsInfo.first().index).isEqualTo(firstItemIndex)
+        assertThat(visibleItemsInfo.last().index).isEqualTo(lastItemIndex)
+        assertThat(
+            (viewPortHeight / 2f) >=
+                (visibleItemsInfo.last().offset + (visibleItemsInfo.last().size / 2))
+        )
+    }
+}
\ No newline at end of file
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScaffoldTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScaffoldTest.kt
index e4261bf..c6c1ed6 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScaffoldTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScaffoldTest.kt
@@ -26,6 +26,8 @@
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
 import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
@@ -107,7 +109,9 @@
             val scrollState = rememberScalingLazyListState()
 
             Scaffold(
-                modifier = Modifier.testTag(TEST_TAG).background(Color.Black),
+                modifier = Modifier
+                    .testTag(TEST_TAG)
+                    .background(Color.Black),
                 timeText = { Text(TIME_TEXT_MESSAGE) },
                 vignette = {
                     if (showVignette.value) {
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnIndexedTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnIndexedTest.kt
index 1a3cbe7..5eec897 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnIndexedTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnIndexedTest.kt
@@ -34,6 +34,7 @@
 import org.junit.Rule
 import org.junit.Test
 
+@Suppress("DEPRECATION")
 class ScalingLazyColumnIndexedTest {
 
     @get:Rule
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt
index b6f60a4..a0ef0e1 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt
@@ -57,6 +57,7 @@
 import kotlin.math.roundToInt
 import kotlinx.coroutines.runBlocking
 
+@Suppress("DEPRECATION")
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 // These tests are in addition to ScalingLazyListLayoutInfoTest which handles scroll events at an
@@ -963,6 +964,42 @@
         assertThat(state.centerItemIndex).isEqualTo(24)
         assertThat(state.centerItemScrollOffset).isEqualTo(0)
     }
+
+    @Test
+    fun centerItemIndexPublishesUpdatesOnChangeOnly() {
+        lateinit var state: ScalingLazyListState
+        var recompositionCount = 0
+
+        rule.setContent {
+            state = rememberScalingLazyListState(initialCenterItemIndex = 0)
+
+            WithTouchSlop(0f) {
+                state.centerItemIndex
+                recompositionCount++
+
+                ScalingLazyColumn(
+                    state = state,
+                    modifier = Modifier.testTag(TEST_TAG).requiredSize(
+                        itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                    ),
+                    autoCentering = AutoCenteringParams(itemIndex = 0)
+                ) {
+                    items(5) {
+                        Box(Modifier.requiredSize(itemSizeDp))
+                    }
+                }
+            }
+        }
+        // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+        rule.waitUntil { state.initialized.value }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput {
+            swipeUp(endY = bottom - (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()))
+        }
+
+        rule.waitForIdle()
+        assertThat(recompositionCount).isEqualTo(2)
+    }
 }
 
 internal const val TestTouchSlop = 18f
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
index a9d571a..82dcfc3 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
@@ -50,6 +50,7 @@
 import kotlin.math.roundToInt
 import org.junit.Ignore
 
+@Suppress("DEPRECATION")
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 public class ScalingLazyListLayoutInfoTest {
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScrollAwayTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScrollAwayTest.kt
index 86a4f4b..edbe7fc 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScrollAwayTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScrollAwayTest.kt
@@ -36,6 +36,10 @@
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import androidx.wear.compose.foundation.lazy.AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.ScalingLazyListState
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -83,6 +87,49 @@
         rule.onNodeWithTag(TIME_TEXT_TAG).assertIsDisplayed()
     }
 
+    @Suppress("DEPRECATION")
+    @Test
+    fun hidesTimeTextWithMaterialScalingLazyColumn() {
+        lateinit var scrollState: androidx.wear.compose.material.ScalingLazyListState
+        rule.setContentWithTheme {
+            scrollState =
+                androidx.wear.compose.material.rememberScalingLazyListState(
+                    initialCenterItemIndex = 1,
+                    initialCenterItemScrollOffset = 0
+                )
+            MaterialScalingLazyColumnTest(itemIndex = 1, offset = 0.dp, scrollState)
+        }
+
+        rule.onNodeWithTag(SCROLL_TAG).performTouchInput { swipeUp() }
+
+        rule.onNodeWithTag(TIME_TEXT_TAG).assertIsNotDisplayed()
+    }
+
+    @Suppress("DEPRECATION")
+    @Test
+    fun showsTimeTextWithMaterialScalingLazyColumnIfItemIndexInvalid() {
+        val scrollAwayItemIndex = 10
+        lateinit var scrollState: androidx.wear.compose.material.ScalingLazyListState
+        rule.setContentWithTheme {
+            scrollState =
+                androidx.wear.compose.material.rememberScalingLazyListState(
+                    initialCenterItemIndex = 1,
+                    initialCenterItemScrollOffset = 0
+                )
+            MaterialScalingLazyColumnTest(
+                itemIndex = scrollAwayItemIndex,
+                offset = 0.dp,
+                scrollState
+            )
+        }
+
+        rule.onNodeWithTag(SCROLL_TAG).performTouchInput { swipeUp() }
+
+        // b/256166359 - itemIndex > number of items in the list.
+        // ScrollAway should default to always showing TimeText
+        rule.onNodeWithTag(TIME_TEXT_TAG).assertIsDisplayed()
+    }
+
     @Composable
     private fun ScalingLazyColumnTest(
         itemIndex: Int,
@@ -91,7 +138,9 @@
     ) {
         WithTouchSlop(0f) {
             Scaffold(
-                modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background),
+                modifier = Modifier
+                    .fillMaxSize()
+                    .background(MaterialTheme.colors.background),
                 timeText = {
                     TimeText(
                         modifier = Modifier
@@ -122,6 +171,50 @@
         }
     }
 
+    @Suppress("DEPRECATION")
+    @Composable
+    private fun MaterialScalingLazyColumnTest(
+        itemIndex: Int,
+        offset: Dp,
+        scrollState: androidx.wear.compose.material.ScalingLazyListState
+    ) {
+        WithTouchSlop(0f) {
+            Scaffold(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .background(MaterialTheme.colors.background),
+                timeText = {
+                    TimeText(
+                        modifier = Modifier
+                            .scrollAway(
+                                scrollState = scrollState,
+                                itemIndex = itemIndex,
+                                offset = offset,
+                            )
+                            .testTag(TIME_TEXT_TAG)
+                    )
+                },
+            ) {
+                ScalingLazyColumn(
+                    contentPadding = PaddingValues(10.dp),
+                    state = scrollState,
+                    autoCentering = androidx.wear.compose.material.AutoCenteringParams(
+                        itemIndex = 1, itemOffset = 0
+                    ),
+                    modifier = Modifier.testTag(SCROLL_TAG)
+                ) {
+                    item {
+                        ListHeader { Text("Chips") }
+                    }
+
+                    items(5) { i ->
+                        ChipTest(Modifier.fillParentMaxHeight(0.5f), i)
+                    }
+                }
+            }
+        }
+    }
+
     @Test
     fun hidesTimeTextWithLazyColumn() {
         lateinit var scrollState: LazyListState
@@ -149,7 +242,8 @@
                     initialFirstVisibleItemIndex = 1,
                 )
             LazyColumnTest(
-                itemIndex = scrollAwayItemIndex, offset = 0.dp, scrollState)
+                itemIndex = scrollAwayItemIndex, offset = 0.dp, scrollState
+            )
         }
 
         rule.onNodeWithTag(SCROLL_TAG).performTouchInput { swipeUp() }
@@ -167,7 +261,9 @@
     ) {
         WithTouchSlop(0f) {
             Scaffold(
-                modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background),
+                modifier = Modifier
+                    .fillMaxSize()
+                    .background(MaterialTheme.colors.background),
                 timeText = {
                     TimeText(
                         modifier = Modifier
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/StepperTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/StepperTest.kt
index 10ae04b..d8a4b41 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/StepperTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/StepperTest.kt
@@ -74,7 +74,7 @@
         steps = 4,
         initialValue = 4f,
         newValue = 20f,
-        expectedValue = 10f
+        expectedFinalValue = 10f
     )
 
     @Test
@@ -83,35 +83,9 @@
         steps = 4,
         initialValue = 4f,
         newValue = -20f,
-        expectedValue = 0f
+        expectedFinalValue = 0f
     )
 
-    @Test
-    public fun coerces_value_top_limit_and_doesnt_change_state() {
-        val state = mutableStateOf(4f)
-        val valueRange = 0f..10f
-
-        rule.initDefaultStepper(state, valueRange, 4)
-
-        rule.runOnIdle {
-            state.value = 20f
-        }
-        assertEquals(20f, state.value)
-    }
-
-    @Test
-    public fun coerces_value_lower_limit_and_doesnt_change_state() {
-        val state = mutableStateOf(4f)
-        val valueRange = 0f..10f
-
-        rule.initDefaultStepper(state, valueRange, 4)
-
-        rule.runOnIdle {
-            state.value = -20f
-        }
-        assertEquals(-20f, state.value)
-    }
-
     @Test(expected = IllegalArgumentException::class)
     public fun throws_when_steps_negative() {
         rule.setContent {
@@ -126,49 +100,36 @@
     }
 
     @Test
-    public fun snaps_value_exactly() = rule.setNewValueAndCheck(
+    public fun coerce_value_exactly() = rule.setNewValueAndCheck(
         range = 0f..1f,
         steps = 4,
         initialValue = 0f,
         // Allowed values are only 0, 0.2, 0.4, 0.6, 0.8, 1
         newValue = 0.6f,
-        expectedValue = 0.6f
+        expectedFinalValue = 0.6f
     )
 
     @Test
-    public fun snaps_value_to_previous() = rule.setNewValueAndCheck(
+    public fun coerce_value_to_previous() = rule.setNewValueAndCheck(
         range = 0f..1f,
         steps = 4,
         initialValue = 0f,
         // Allowed values are only 0, 0.2, 0.4, 0.6, 0.8, 1
         newValue = 0.65f,
-        expectedValue = 0.6f
+        expectedFinalValue = 0.6f
     )
 
     @Test
-    public fun snaps_value_to_next() = rule.setNewValueAndCheck(
+    public fun coerce_value_to_next() = rule.setNewValueAndCheck(
         range = 0f..1f,
         steps = 4,
         initialValue = 0f,
         // Allowed values are only 0, 0.2, 0.4, 0.6, 0.8, 1
         newValue = 0.55f,
-        expectedValue = 0.6f
+        expectedFinalValue = 0.6f
     )
 
     @Test
-    public fun snaps_value_to_next_and_does_not_change_state() {
-        val state = mutableStateOf(0f)
-        val valueRange = 0f..1f
-
-        rule.initDefaultStepper(state, valueRange, 4)
-
-        rule.runOnIdle {
-            state.value = 0.55f
-        }
-        assertEquals(0.55f, state.value)
-    }
-
-    @Test
     public fun decreases_value_by_clicking_bottom() {
         val state = mutableStateOf(2f)
         val range = 1f..4f
@@ -390,6 +351,57 @@
             .assertContentDescriptionContains(testContentDescription)
     }
 
+    @Test
+    fun supports_stepper_range_semantics_by_default() {
+        val value = 1f
+        val steps = 5
+        val valueRange = 0f..(steps + 1).toFloat()
+
+        val modifier = Modifier.testTag(TEST_TAG)
+
+        rule.setContentWithTheme {
+            Stepper(
+                modifier = modifier,
+                value = value,
+                steps = steps,
+                valueRange = valueRange,
+                onValueChange = { },
+                increaseIcon = { Icon(StepperDefaults.Increase, "Increase") },
+                decreaseIcon = { Icon(StepperDefaults.Decrease, "Decrease") },
+            ) {}
+        }
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(TEST_TAG, true)
+            .assertExists()
+            .assertRangeInfoEquals(ProgressBarRangeInfo(value, valueRange, steps))
+    }
+
+    @Test(expected = java.lang.AssertionError::class)
+    fun disable_stepper_semantics_with_enableDefaultSemantics_false() {
+        val value = 1f
+        val steps = 5
+        val valueRange = 0f..(steps + 1).toFloat()
+
+        rule.setContentWithTheme {
+            Stepper(
+                modifier = Modifier.testTag(TEST_TAG),
+                value = value,
+                steps = steps,
+                valueRange = valueRange,
+                onValueChange = { },
+                increaseIcon = { Icon(StepperDefaults.Increase, "Increase") },
+                decreaseIcon = { Icon(StepperDefaults.Decrease, "Decrease") },
+                enableRangeSemantics = false
+            ) {}
+        }
+        rule.waitForIdle()
+        // Should throw assertion error for assertRangeInfoEquals
+        rule.onNodeWithTag(TEST_TAG, true)
+            .assertExists()
+            .assertRangeInfoEquals(ProgressBarRangeInfo(value, valueRange, steps))
+    }
+
     private fun verifyDisabledColors(increase: Boolean, value: Float) {
         val state = mutableStateOf(value)
         var expectedIconColor = Color.Transparent
@@ -474,7 +486,7 @@
         progression = 0..10,
         initialValue = 4,
         newValue = 20,
-        expectedValue = 10
+        expectedFinalValue = 10
     )
 
     @Test
@@ -482,58 +494,89 @@
         progression = 0..10,
         initialValue = 4,
         newValue = -20,
-        expectedValue = 0
+        expectedFinalValue = 0
     )
 
     @Test
-    public fun coerces_value_top_limit_and_doesnt_change_state() {
-        val state = mutableStateOf(4)
-        val valueProgression = 0..10
-
-        rule.initDefaultStepper(state, valueProgression)
-
-        rule.runOnIdle {
-            state.value = 20
-        }
-        assertEquals(20, state.value)
-    }
-
-    @Test
-    public fun coerces_value_lower_limit_and_doesnt_change_state() {
-        val state = mutableStateOf(4)
-        val valueProgression = 0..10
-
-        rule.initDefaultStepper(state, valueProgression)
-
-        rule.runOnIdle {
-            state.value = -20
-        }
-        assertEquals(-20, state.value)
-    }
-
-    @Test
-    public fun snaps_value_exactly() = rule.setNewValueAndCheck(
+    public fun coerce_value_exactly() = rule.setNewValueAndCheck(
         progression = IntProgression.fromClosedRange(0, 12, 3),
         initialValue = 0,
         newValue = 3,
-        expectedValue = 3
+        expectedFinalValue = 3
     )
 
     @Test
-    public fun snaps_value_to_previous() = rule.setNewValueAndCheck(
+    public fun coerce_value_to_previous() = rule.setNewValueAndCheck(
         progression = IntProgression.fromClosedRange(0, 12, 3),
         initialValue = 0,
         newValue = 4,
-        expectedValue = 3
+        expectedFinalValue = 3
     )
 
     @Test
-    public fun snaps_value_to_next() = rule.setNewValueAndCheck(
+    public fun coerce_value_to_next() = rule.setNewValueAndCheck(
         progression = IntProgression.fromClosedRange(0, 12, 3),
         initialValue = 0,
         newValue = 5,
-        expectedValue = 6
+        expectedFinalValue = 6
     )
+
+    @Test
+    fun supports_stepper_range_semantics_by_default() {
+        val value = 1
+        val valueProgression = 0..10
+
+        rule.setContentWithTheme {
+            Stepper(
+                value = value,
+                onValueChange = {},
+                valueProgression = valueProgression,
+                increaseIcon = { Icon(StepperDefaults.Increase, "Increase") },
+                decreaseIcon = { Icon(StepperDefaults.Decrease, "Decrease") },
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {}
+        }
+        rule.waitForIdle()
+        // Should throw assertion error for assertRangeInfoEquals
+        rule.onNodeWithTag(TEST_TAG, true)
+            .assertExists()
+            .assertRangeInfoEquals(
+                ProgressBarRangeInfo(
+                    value.toFloat(),
+                    valueProgression.first.toFloat()..valueProgression.last.toFloat(),
+                    valueProgression.stepsNumber()
+                )
+            )
+    }
+
+    @Test(expected = java.lang.AssertionError::class)
+    fun disable_stepper_semantics_with_enableDefaultSemantics_false() {
+        val value = 1
+        val valueProgression = 0..10
+
+        rule.setContentWithTheme {
+            Stepper(
+                value = value,
+                onValueChange = {},
+                valueProgression = valueProgression,
+                increaseIcon = { Icon(StepperDefaults.Increase, "Increase") },
+                decreaseIcon = { Icon(StepperDefaults.Decrease, "Decrease") },
+                modifier = Modifier.testTag(TEST_TAG),
+                enableRangeSemantics = false
+            ) {}
+        }
+        rule.waitForIdle()
+        // Should throw assertion error for assertRangeInfoEquals
+        rule.onNodeWithTag(TEST_TAG, true)
+            .assertExists()
+            .assertRangeInfoEquals(
+                ProgressBarRangeInfo(
+                    value.toFloat(),
+                    valueProgression.first.toFloat()..valueProgression.last.toFloat(),
+                    valueProgression.stepsNumber()
+                )
+            )
+    }
 }
 
 private fun ComposeContentTestRule.setNewValueAndCheck(
@@ -541,7 +584,7 @@
     steps: Int,
     initialValue: Float,
     newValue: Float,
-    expectedValue: Float
+    expectedFinalValue: Float
 ) {
     val state = mutableStateOf(initialValue)
 
@@ -549,7 +592,12 @@
 
     runOnIdle { state.value = newValue }
     onNodeWithTag(TEST_TAG)
-        .assertRangeInfoEquals(ProgressBarRangeInfo(expectedValue, range, steps))
+        .assertRangeInfoEquals(ProgressBarRangeInfo(expectedFinalValue, range, steps))
+
+    // State value is not coerced to expectedValue - thus we expect it to be equal to
+    // the last set value, which is newValue
+    waitForIdle()
+    assertEquals(newValue, state.value)
 }
 
 private fun ComposeContentTestRule.initDefaultStepper(
@@ -574,7 +622,7 @@
     progression: IntProgression,
     initialValue: Int,
     newValue: Int,
-    expectedValue: Int
+    expectedFinalValue: Int
 ) {
     val state = mutableStateOf(initialValue)
 
@@ -584,11 +632,16 @@
     onNodeWithTag(TEST_TAG)
         .assertRangeInfoEquals(
             ProgressBarRangeInfo(
-                expectedValue.toFloat(),
+                expectedFinalValue.toFloat(),
                 progression.first.toFloat()..progression.last.toFloat(),
                 progression.stepsNumber()
             )
         )
+
+    // State value is not coerced to expectedValue - thus we expect it to be equal to
+    // the last set value, which is newValue
+    waitForIdle()
+    assertEquals(newValue, state.value)
 }
 
 private fun ComposeContentTestRule.initDefaultStepper(
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/dialog/DialogWithMaterialSlcTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/dialog/DialogWithMaterialSlcTest.kt
new file mode 100644
index 0000000..1eddea4
--- /dev/null
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/dialog/DialogWithMaterialSlcTest.kt
@@ -0,0 +1,1131 @@
+/*
+ * 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.wear.compose.material.dialog
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertIsEqualTo
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.hasClickAction
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeRight
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.SdkSuppress
+import androidx.wear.compose.material.Button
+import androidx.wear.compose.material.Chip
+import androidx.wear.compose.material.LocalContentColor
+import androidx.wear.compose.material.LocalTextStyle
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.TEST_TAG
+import androidx.wear.compose.material.TestImage
+import androidx.wear.compose.material.Text
+import androidx.wear.compose.material.assertContainsColor
+import androidx.wear.compose.material.setContentWithTheme
+import androidx.wear.compose.material.setContentWithThemeForSizeAssertions
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * These tests were copied from DialogTest.kt for support of deprecated Dialogs
+ */
+
+@Suppress("DEPRECATION")
+class DialogWithMaterialSlcBehaviourTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun supports_testtag_on_alert_with_buttons() {
+        rule.setContentWithTheme {
+            AlertWithMaterialSlc(
+                title = {},
+                negativeButton = {},
+                positiveButton = {},
+                modifier = Modifier.testTag(TEST_TAG),
+            )
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun supports_testtag_on_alert_with_chips() {
+        rule.setContentWithTheme {
+            AlertWithMaterialSlc(
+                title = {},
+                message = {},
+                content = {},
+                modifier = Modifier.testTag(TEST_TAG),
+            )
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun supports_testtag_on_ConfirmationWithMaterialSlc() {
+        rule.setContentWithTheme {
+            ConfirmationWithMaterialSlc(
+                onTimeout = {},
+                icon = {},
+                content = {},
+                modifier = Modifier.testTag(TEST_TAG),
+            )
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun displays_icon_on_alert_with_buttons() {
+        rule.setContentWithTheme {
+            AlertWithMaterialSlc(
+                icon = { TestImage(TEST_TAG) },
+                title = {},
+                negativeButton = {},
+                positiveButton = {},
+            )
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun displays_icon_on_alert_with_chips() {
+        rule.setContentWithTheme {
+            AlertWithMaterialSlc(
+                icon = { TestImage(TEST_TAG) },
+                title = {},
+                message = {},
+                content = {},
+            )
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun displays_icon_on_ConfirmationWithMaterialSlc() {
+        rule.setContentWithTheme {
+            ConfirmationWithMaterialSlc(
+                onTimeout = {},
+                icon = { TestImage(TEST_TAG) },
+                content = {},
+            )
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun displays_title_on_alert_with_buttons() {
+        rule.setContentWithTheme {
+            AlertWithMaterialSlc(
+                title = { Text("Text", modifier = Modifier.testTag(TEST_TAG)) },
+                negativeButton = {},
+                positiveButton = {},
+            )
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun displays_title_on_alert_with_chips() {
+        rule.setContentWithTheme {
+            AlertWithMaterialSlc(
+                icon = {},
+                title = { Text("Text", modifier = Modifier.testTag(TEST_TAG)) },
+                message = {},
+                content = {},
+            )
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun displays_title_on_ConfirmationWithMaterialSlc() {
+        rule.setContentWithTheme {
+            ConfirmationWithMaterialSlc(
+                onTimeout = {},
+                icon = {},
+                content = { Text("Text", modifier = Modifier.testTag(TEST_TAG)) },
+            )
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun displays_bodymessage_on_alert_with_buttons() {
+        rule.setContentWithTheme {
+            AlertWithMaterialSlc(
+                title = {},
+                negativeButton = {},
+                positiveButton = {},
+                content = { Text("Text", modifier = Modifier.testTag(TEST_TAG)) },
+            )
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun displays_bodymessage_on_alert_with_chips() {
+        rule.setContentWithTheme {
+            AlertWithMaterialSlc(
+                icon = {},
+                title = {},
+                message = { Text("Text", modifier = Modifier.testTag(TEST_TAG)) },
+                content = {},
+            )
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun displays_buttons_on_alert_with_buttons() {
+        val buttonTag1 = "Button1"
+        val buttonTag2 = "Button2"
+
+        rule.setContentWithTheme {
+            AlertWithMaterialSlc(
+                title = {},
+                negativeButton = {
+                    Button(onClick = {}, modifier = Modifier.testTag(buttonTag1), content = {})
+                },
+                positiveButton = {
+                    Button(onClick = {}, modifier = Modifier.testTag(buttonTag2), content = {})
+                },
+                content = {},
+            )
+        }
+
+        rule.onNodeWithTag(buttonTag1).assertExists()
+        rule.onNodeWithTag(buttonTag2).assertExists()
+    }
+
+    @Test
+    fun supports_swipetodismiss_on_wrapped_alertdialog_with_buttons() {
+        rule.setContentWithTheme {
+            Box {
+                var showDialog by remember { mutableStateOf(true) }
+                Column(
+                    modifier = Modifier.fillMaxSize(),
+                    verticalArrangement = Arrangement.Center,
+                    horizontalAlignment = Alignment.CenterHorizontally
+                ) {
+                    Text("Start Screen")
+                }
+                Dialog(
+                    showDialog = showDialog,
+                    onDismissRequest = { showDialog = false },
+                ) {
+                    AlertWithMaterialSlc(
+                        title = {},
+                        negativeButton = {
+                            Button(onClick = {}, content = {})
+                        },
+                        positiveButton = {
+                            Button(onClick = {}, content = {})
+                        },
+                        content = { Text("Dialog", modifier = Modifier.testTag(TEST_TAG)) },
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput({ swipeRight() })
+        rule.onNodeWithTag(TEST_TAG).assertDoesNotExist()
+    }
+
+    @Test
+    fun supports_swipetodismiss_on_wrapped_alertdialog_with_chips() {
+        rule.setContentWithTheme {
+            Box {
+                var showDialog by remember { mutableStateOf(true) }
+                Column(
+                    modifier = Modifier.fillMaxSize(),
+                    verticalArrangement = Arrangement.Center,
+                    horizontalAlignment = Alignment.CenterHorizontally
+                ) {
+                    Text("Label")
+                }
+                Dialog(
+                    showDialog = showDialog,
+                    onDismissRequest = { showDialog = false },
+                ) {
+                    AlertWithMaterialSlc(
+                        icon = {},
+                        title = {},
+                        message = { Text("Text", modifier = Modifier.testTag(TEST_TAG)) },
+                        content = {},
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput({ swipeRight() })
+        rule.onNodeWithTag(TEST_TAG).assertDoesNotExist()
+    }
+
+    @Test
+    fun supports_swipetodismiss_on_wrapped_confirmationdialog() {
+        rule.setContentWithTheme {
+            Box {
+                var showDialog by remember { mutableStateOf(true) }
+                Column(
+                    modifier = Modifier.fillMaxSize(),
+                    verticalArrangement = Arrangement.Center,
+                    horizontalAlignment = Alignment.CenterHorizontally
+                ) {
+                    Text("Label")
+                }
+                Dialog(
+                    showDialog = showDialog,
+                    onDismissRequest = { showDialog = false },
+                ) {
+                    ConfirmationWithMaterialSlc(
+                        onTimeout = { showDialog = false },
+                        icon = {},
+                        content = { Text("Dialog", modifier = Modifier.testTag(TEST_TAG)) },
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput({ swipeRight() })
+        rule.onNodeWithTag(TEST_TAG).assertDoesNotExist()
+    }
+
+    @Test
+    fun shows_dialog_when_showdialog_equals_true() {
+        rule.setContentWithTheme {
+            Box {
+                var showDialog by remember { mutableStateOf(false) }
+                Column(
+                    modifier = Modifier.fillMaxSize(),
+                    verticalArrangement = Arrangement.Center,
+                    horizontalAlignment = Alignment.CenterHorizontally
+                ) {
+                    Chip(onClick = { showDialog = true }, label = { Text("Show") })
+                }
+                Dialog(
+                    showDialog = showDialog,
+                    onDismissRequest = { showDialog = false },
+                ) {
+                    AlertWithMaterialSlc(
+                        icon = {},
+                        title = {},
+                        message = { Text("Text", modifier = Modifier.testTag(TEST_TAG)) },
+                        content = {},
+                    )
+                }
+            }
+        }
+
+        rule.onNode(hasClickAction()).performClick()
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun calls_ondismissrequest_when_dialog_is_swiped() {
+        val dismissedText = "Dismissed"
+        rule.setContentWithTheme {
+            Box {
+                var dismissed by remember { mutableStateOf(false) }
+                Column(
+                    modifier = Modifier.fillMaxSize(),
+                    verticalArrangement = Arrangement.Center,
+                    horizontalAlignment = Alignment.CenterHorizontally
+                ) {
+                    Text(if (dismissed) dismissedText else "Label")
+                }
+                Dialog(
+                    showDialog = !dismissed,
+                    onDismissRequest = { dismissed = true },
+                ) {
+                    AlertWithMaterialSlc(
+                        icon = {},
+                        title = {},
+                        message = { Text("Text", modifier = Modifier.testTag(TEST_TAG)) },
+                        content = {},
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput({ swipeRight() })
+        rule.onNodeWithText(dismissedText).assertExists()
+    }
+}
+
+@Suppress("DEPRECATION")
+class DialogWithMaterialSlcContentSizeAndPositionTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun spaces_icon_and_title_correctly_on_alert_with_buttons() {
+        rule
+            .setContentWithThemeForSizeAssertions(useUnmergedTree = true) {
+                AlertWithMaterialSlc(
+                    icon = { TestImage(ICON_TAG) },
+                    title = { Text("Title", modifier = Modifier.testTag(TITLE_TAG)) },
+                    negativeButton = {
+                        Button(onClick = {}, modifier = Modifier.testTag(BUTTON_TAG)) {}
+                    },
+                    positiveButton = { Button(onClick = {}) {} },
+                    verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.CenterVertically),
+                    modifier = Modifier.testTag(TEST_TAG),
+                )
+            }
+
+        val iconBottom = rule.onNodeWithTag(ICON_TAG).getUnclippedBoundsInRoot().bottom
+        val titleTop = rule.onNodeWithTag(TITLE_TAG).getUnclippedBoundsInRoot().top
+        titleTop.assertIsEqualTo(iconBottom + DialogDefaults.IconSpacing)
+    }
+
+    @Test
+    fun spaces_title_and_buttons_correctly_on_alert_with_buttons() {
+        var titlePadding = 0.dp
+
+        rule
+            .setContentWithThemeForSizeAssertions(useUnmergedTree = true) {
+                titlePadding = DialogDefaults.TitlePadding.calculateBottomPadding()
+                AlertWithMaterialSlc(
+                    icon = { TestImage(ICON_TAG) },
+                    title = { Text("Title", modifier = Modifier.testTag(TITLE_TAG)) },
+                    negativeButton = {
+                        Button(onClick = {}, modifier = Modifier.testTag(BUTTON_TAG)) {}
+                    },
+                    positiveButton = { Button(onClick = {}) {} },
+                    verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.CenterVertically),
+                    modifier = Modifier.testTag(TEST_TAG),
+                )
+            }
+
+        val titleBottom = rule.onNodeWithTag(TITLE_TAG).getUnclippedBoundsInRoot().bottom
+        val buttonTop = rule.onNodeWithTag(BUTTON_TAG).getUnclippedBoundsInRoot().top
+        buttonTop.assertIsEqualTo(titleBottom + titlePadding)
+    }
+
+    @Test
+    fun spaces_icon_and_title_correctly_on_alert_with_chips() {
+        rule
+            .setContentWithThemeForSizeAssertions(useUnmergedTree = true) {
+                AlertWithMaterialSlc(
+                    icon = { TestImage(ICON_TAG) },
+                    title = { Text("Title", modifier = Modifier.testTag(TITLE_TAG)) },
+                    content = {
+                        item {
+                            Chip(
+                                label = { Text("Chip") },
+                                onClick = {},
+                                modifier = Modifier.testTag(CHIP_TAG)
+                            )
+                        }
+                    },
+                    verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.CenterVertically),
+                    modifier = Modifier.testTag(TEST_TAG),
+                )
+            }
+
+        val iconBottom = rule.onNodeWithTag(ICON_TAG).getUnclippedBoundsInRoot().bottom
+        val titleTop = rule.onNodeWithTag(TITLE_TAG).getUnclippedBoundsInRoot().top
+        titleTop.assertIsEqualTo(iconBottom + DialogDefaults.IconSpacing)
+    }
+
+    @Test
+    fun spaces_title_and_chips_correctly_on_alert_with_chips() {
+        var titlePadding = 0.dp
+
+        rule
+            .setContentWithThemeForSizeAssertions(useUnmergedTree = true) {
+                titlePadding = DialogDefaults.TitlePadding.calculateBottomPadding()
+                AlertWithMaterialSlc(
+                    icon = { TestImage(ICON_TAG) },
+                    title = { Text("Title", modifier = Modifier.testTag(TITLE_TAG)) },
+                    content = {
+                        item {
+                            Chip(
+                                label = { Text("Chip") },
+                                onClick = {},
+                                modifier = Modifier.testTag(CHIP_TAG)
+                            )
+                        }
+                    },
+                    verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.CenterVertically),
+                    modifier = Modifier.testTag(TEST_TAG),
+                )
+            }
+
+        val titleBottom = rule.onNodeWithTag(TITLE_TAG).getUnclippedBoundsInRoot().bottom
+        val chipTop = rule.onNodeWithTag(CHIP_TAG).getUnclippedBoundsInRoot().top
+        chipTop.assertIsEqualTo(titleBottom + titlePadding)
+    }
+
+    @Test
+    fun spaces_icon_and_title_correctly_on_ConfirmationWithMaterialSlc() {
+        rule
+            .setContentWithThemeForSizeAssertions(useUnmergedTree = true) {
+                ConfirmationWithMaterialSlc(
+                    onTimeout = {},
+                    icon = { TestImage(ICON_TAG) },
+                    content = { Text("Title", modifier = Modifier.testTag(TITLE_TAG)) },
+                    verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.CenterVertically),
+                    modifier = Modifier.testTag(TEST_TAG),
+                )
+            }
+
+        val iconBottom = rule.onNodeWithTag(ICON_TAG).getUnclippedBoundsInRoot().bottom
+        val titleTop = rule.onNodeWithTag(TITLE_TAG).getUnclippedBoundsInRoot().top
+        titleTop.assertIsEqualTo(iconBottom + DialogDefaults.IconSpacing)
+    }
+
+    @Test
+    fun spaces_title_and_body_correctly_on_alert_with_buttons() {
+        var titleSpacing = 0.dp
+
+        rule
+            .setContentWithThemeForSizeAssertions(useUnmergedTree = true) {
+                titleSpacing = DialogDefaults.TitlePadding.calculateBottomPadding()
+                AlertWithMaterialSlc(
+                    title = { Text("Title", modifier = Modifier.testTag(TITLE_TAG)) },
+                    negativeButton = {
+                        Button(onClick = {}, modifier = Modifier.testTag(BUTTON_TAG)) {}
+                    },
+                    positiveButton = { Button(onClick = {}) {} },
+                    content = { Text("Body", modifier = Modifier.testTag(BODY_TAG)) },
+                    verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.CenterVertically),
+                    modifier = Modifier.testTag(TEST_TAG),
+                )
+            }
+
+        val titleBottom = rule.onNodeWithTag(TITLE_TAG).getUnclippedBoundsInRoot().bottom
+        val bodyTop = rule.onNodeWithTag(BODY_TAG).getUnclippedBoundsInRoot().top
+        bodyTop.assertIsEqualTo(titleBottom + titleSpacing)
+    }
+
+    @Test
+    fun spaces_title_and_body_correctly_on_alert_with_chips() {
+        var titleSpacing = 0.dp
+
+        rule
+            .setContentWithThemeForSizeAssertions(useUnmergedTree = true) {
+                titleSpacing = DialogDefaults.TitlePadding.calculateBottomPadding()
+                AlertWithMaterialSlc(
+                    icon = { TestImage(ICON_TAG) },
+                    title = { Text("Title", modifier = Modifier.testTag(TITLE_TAG)) },
+                    message = { Text("Message", modifier = Modifier.testTag(BODY_TAG)) },
+                    content = {
+                        item {
+                            Chip(
+                                label = { Text("Chip") },
+                                onClick = {},
+                                modifier = Modifier.testTag(CHIP_TAG)
+                            )
+                        }
+                    },
+                    verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.CenterVertically),
+                    modifier = Modifier.testTag(TEST_TAG),
+                )
+            }
+
+        val titleBottom = rule.onNodeWithTag(TITLE_TAG).getUnclippedBoundsInRoot().bottom
+        val bodyTop = rule.onNodeWithTag(BODY_TAG).getUnclippedBoundsInRoot().top
+        bodyTop.assertIsEqualTo(titleBottom + titleSpacing)
+    }
+
+    @Test
+    fun spaces_body_and_buttons_correctly_on_alert_with_buttons() {
+        var bodyPadding = 0.dp
+
+        rule
+            .setContentWithThemeForSizeAssertions(useUnmergedTree = true) {
+                bodyPadding = DialogDefaults.BodyPadding.calculateBottomPadding()
+                AlertWithMaterialSlc(
+                    icon = {},
+                    title = {},
+                    negativeButton = {
+                        Button(onClick = {}, modifier = Modifier.testTag(BUTTON_TAG)) {}
+                    },
+                    positiveButton = {
+                        Button(onClick = {}) {}
+                    },
+                    content = { Text("Body", modifier = Modifier.testTag(BODY_TAG)) },
+                    verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.CenterVertically),
+                    modifier = Modifier.testTag(TEST_TAG),
+                )
+            }
+
+        val bodyBottom = rule.onNodeWithTag(BODY_TAG).getUnclippedBoundsInRoot().bottom
+        val buttonTop = rule.onNodeWithTag(BUTTON_TAG).getUnclippedBoundsInRoot().top
+        buttonTop.assertIsEqualTo(bodyBottom + bodyPadding)
+    }
+
+    @Test
+    fun spaces_body_and_chips_correctly_on_alert_with_chips() {
+        var bodyPadding = 0.dp
+
+        rule
+            .setContentWithThemeForSizeAssertions(useUnmergedTree = true) {
+                bodyPadding = DialogDefaults.BodyPadding.calculateBottomPadding()
+                AlertWithMaterialSlc(
+                    icon = { TestImage(ICON_TAG) },
+                    title = { Text("Title", modifier = Modifier.testTag(TITLE_TAG)) },
+                    message = { Text("Message", modifier = Modifier.testTag(BODY_TAG)) },
+                    content = {
+                        item {
+                            Chip(
+                                label = { Text("Chip") },
+                                onClick = {},
+                                modifier = Modifier.testTag(CHIP_TAG)
+                            )
+                        }
+                    },
+                    verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.CenterVertically),
+                    modifier = Modifier.testTag(TEST_TAG),
+                )
+            }
+
+        val bodyBottom = rule.onNodeWithTag(BODY_TAG).getUnclippedBoundsInRoot().bottom
+        val chipTop = rule.onNodeWithTag(CHIP_TAG).getUnclippedBoundsInRoot().top
+        chipTop.assertIsEqualTo(bodyBottom + bodyPadding)
+    }
+}
+
+@Suppress("DEPRECATION")
+class DialogWithMaterialSlcContentColorTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun gives_icon_onbackground_on_alert_for_buttons() {
+        var expectedColor = Color.Transparent
+        var actualColor = Color.Transparent
+
+        rule.setContentWithTheme {
+            expectedColor = MaterialTheme.colors.onBackground
+            AlertWithMaterialSlc(
+                icon = { actualColor = LocalContentColor.current },
+                title = {},
+                negativeButton = {},
+                positiveButton = {},
+                content = {},
+            )
+        }
+
+        Assert.assertEquals(expectedColor, actualColor)
+    }
+
+    @Test
+    fun gives_icon_onbackground_on_alert_for_chips() {
+        var expectedColor = Color.Transparent
+        var actualColor = Color.Transparent
+
+        rule.setContentWithTheme {
+            expectedColor = MaterialTheme.colors.onBackground
+            AlertWithMaterialSlc(
+                icon = { actualColor = LocalContentColor.current },
+                title = {},
+                message = {},
+                content = {},
+            )
+        }
+
+        Assert.assertEquals(expectedColor, actualColor)
+    }
+
+    @Test
+    fun gives_icon_onbackground_on_ConfirmationWithMaterialSlc() {
+        var expectedColor = Color.Transparent
+        var actualColor = Color.Transparent
+
+        rule.setContentWithTheme {
+            expectedColor = MaterialTheme.colors.onBackground
+            ConfirmationWithMaterialSlc(
+                onTimeout = {},
+                icon = { actualColor = LocalContentColor.current },
+                content = {},
+            )
+        }
+
+        Assert.assertEquals(expectedColor, actualColor)
+    }
+
+    @Test
+    fun gives_custom_icon_on_alert_for_buttons() {
+        val overrideColor = Color.Yellow
+        var actualColor = Color.Transparent
+
+        rule.setContentWithTheme {
+            AlertWithMaterialSlc(
+                iconColor = overrideColor,
+                icon = { actualColor = LocalContentColor.current },
+                title = {},
+                negativeButton = {},
+                positiveButton = {},
+                content = {},
+            )
+        }
+
+        Assert.assertEquals(overrideColor, actualColor)
+    }
+
+    @Test
+    fun gives_custom_icon_on_alert_for_chips() {
+        val overrideColor = Color.Yellow
+        var actualColor = Color.Transparent
+
+        rule.setContentWithTheme {
+            AlertWithMaterialSlc(
+                iconColor = overrideColor,
+                icon = { actualColor = LocalContentColor.current },
+                title = {},
+                message = {},
+                content = {},
+            )
+        }
+
+        Assert.assertEquals(overrideColor, actualColor)
+    }
+
+    @Test
+    fun gives_custom_icon_on_ConfirmationWithMaterialSlc() {
+        val overrideColor = Color.Yellow
+        var actualColor = Color.Transparent
+
+        rule.setContentWithTheme {
+            ConfirmationWithMaterialSlc(
+                onTimeout = {},
+                iconColor = overrideColor,
+                icon = { actualColor = LocalContentColor.current },
+                content = {},
+            )
+        }
+
+        Assert.assertEquals(overrideColor, actualColor)
+    }
+
+    @Test
+    fun gives_title_onbackground_on_alert_for_buttons() {
+        var expectedColor = Color.Transparent
+        var actualColor = Color.Transparent
+
+        rule.setContentWithTheme {
+            expectedColor = MaterialTheme.colors.onBackground
+            AlertWithMaterialSlc(
+                title = { actualColor = LocalContentColor.current },
+                negativeButton = {},
+                positiveButton = {},
+                content = {},
+            )
+        }
+
+        Assert.assertEquals(expectedColor, actualColor)
+    }
+
+    @Test
+    fun gives_title_onbackground_on_alert_for_chips() {
+        var expectedColor = Color.Transparent
+        var actualColor = Color.Transparent
+
+        rule.setContentWithTheme {
+            expectedColor = MaterialTheme.colors.onBackground
+            AlertWithMaterialSlc(
+                title = { actualColor = LocalContentColor.current },
+                message = {},
+                content = {},
+            )
+        }
+
+        Assert.assertEquals(expectedColor, actualColor)
+    }
+
+    @Test
+    fun gives_title_onbackground_on_ConfirmationWithMaterialSlc() {
+        var expectedColor = Color.Transparent
+        var actualColor = Color.Transparent
+
+        rule.setContentWithTheme {
+            expectedColor = MaterialTheme.colors.onBackground
+            ConfirmationWithMaterialSlc(
+                onTimeout = {},
+                content = { actualColor = LocalContentColor.current },
+            )
+        }
+
+        Assert.assertEquals(expectedColor, actualColor)
+    }
+
+    @Test
+    fun gives_custom_title_on_alert_for_buttons() {
+        val overrideColor = Color.Yellow
+        var actualColor = Color.Transparent
+
+        rule.setContentWithTheme {
+            AlertWithMaterialSlc(
+                titleColor = overrideColor,
+                title = { actualColor = LocalContentColor.current },
+                negativeButton = {},
+                positiveButton = {},
+                content = {},
+            )
+        }
+
+        Assert.assertEquals(overrideColor, actualColor)
+    }
+
+    @Test
+    fun gives_custom_title_on_alert_for_chips() {
+        val overrideColor = Color.Yellow
+        var actualColor = Color.Transparent
+
+        rule.setContentWithTheme {
+            AlertWithMaterialSlc(
+                titleColor = overrideColor,
+                title = { actualColor = LocalContentColor.current },
+                message = {},
+                content = {},
+            )
+        }
+
+        Assert.assertEquals(overrideColor, actualColor)
+    }
+
+    @Test
+    fun gives_custom_title_on_ConfirmationWithMaterialSlc() {
+        val overrideColor = Color.Yellow
+        var actualColor = Color.Transparent
+
+        rule.setContentWithTheme {
+            ConfirmationWithMaterialSlc(
+                onTimeout = {},
+                contentColor = overrideColor,
+                content = { actualColor = LocalContentColor.current },
+            )
+        }
+
+        Assert.assertEquals(overrideColor, actualColor)
+    }
+
+    @Test
+    fun gives_bodymessage_onbackground_on_alert_for_buttons() {
+        var expectedContentColor = Color.Transparent
+        var actualContentColor = Color.Transparent
+
+        rule.setContentWithTheme {
+            expectedContentColor = MaterialTheme.colors.onBackground
+            AlertWithMaterialSlc(
+                title = {},
+                negativeButton = {},
+                positiveButton = {},
+                content = { actualContentColor = LocalContentColor.current },
+            )
+        }
+
+        Assert.assertEquals(expectedContentColor, actualContentColor)
+    }
+
+    @Test
+    fun gives_bodymessage_onbackground_on_alert_for_chips() {
+        var expectedContentColor = Color.Transparent
+        var actualContentColor = Color.Transparent
+
+        rule.setContentWithTheme {
+            expectedContentColor = MaterialTheme.colors.onBackground
+            AlertWithMaterialSlc(
+                title = {},
+                message = { actualContentColor = LocalContentColor.current },
+                content = {},
+            )
+        }
+
+        Assert.assertEquals(expectedContentColor, actualContentColor)
+    }
+
+    @Test
+    fun gives_custom_bodymessage_on_alert_for_buttons() {
+        val overrideColor = Color.Yellow
+        var actualColor = Color.Transparent
+
+        rule.setContentWithTheme {
+            AlertWithMaterialSlc(
+                title = {},
+                negativeButton = {},
+                positiveButton = {},
+                contentColor = overrideColor,
+                content = { actualColor = LocalContentColor.current },
+            )
+        }
+
+        Assert.assertEquals(overrideColor, actualColor)
+    }
+
+    @Test
+    fun gives_custom_bodymessage_on_alert_for_chips() {
+        val overrideColor = Color.Yellow
+        var actualColor = Color.Transparent
+
+        rule.setContentWithTheme {
+            AlertWithMaterialSlc(
+                title = {},
+                messageColor = overrideColor,
+                message = { actualColor = LocalContentColor.current },
+                content = {},
+            )
+        }
+
+        Assert.assertEquals(overrideColor, actualColor)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun gives_correct_background_color_on_alert_for_buttons() {
+        verifyBackgroundColor(expected = { MaterialTheme.colors.background }) {
+            AlertWithMaterialSlc(
+                title = {},
+                negativeButton = {},
+                positiveButton = {},
+                content = {},
+                modifier = Modifier.testTag(TEST_TAG)
+            )
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun gives_correct_background_color_on_alert_for_chips() {
+        verifyBackgroundColor(expected = { MaterialTheme.colors.background }) {
+            AlertWithMaterialSlc(
+                title = {},
+                message = {},
+                content = {},
+                modifier = Modifier.testTag(TEST_TAG)
+            )
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun gives_correct_background_color_on_ConfirmationWithMaterialSlc() {
+        verifyBackgroundColor(expected = { MaterialTheme.colors.background }) {
+            ConfirmationWithMaterialSlc(
+                onTimeout = {},
+                content = {},
+                modifier = Modifier.testTag(TEST_TAG),
+            )
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun gives_custom_background_color_on_alert_for_buttons() {
+        val overrideColor = Color.Yellow
+
+        rule.setContentWithTheme {
+            AlertWithMaterialSlc(
+                title = {},
+                negativeButton = {},
+                positiveButton = {},
+                content = {},
+                backgroundColor = overrideColor,
+                modifier = Modifier.testTag(TEST_TAG),
+            )
+        }
+
+        rule.onNodeWithTag(TEST_TAG)
+            .captureToImage()
+            .assertContainsColor(overrideColor, 100.0f)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun gives_custom_background_color_on_alert_for_chips() {
+        val overrideColor = Color.Yellow
+
+        rule.setContentWithTheme {
+            AlertWithMaterialSlc(
+                title = {},
+                message = {},
+                content = {},
+                backgroundColor = overrideColor,
+                modifier = Modifier.testTag(TEST_TAG),
+            )
+        }
+
+        rule.onNodeWithTag(TEST_TAG)
+            .captureToImage()
+            .assertContainsColor(overrideColor, 100.0f)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun gives_custom_background_color_on_ConfirmationWithMaterialSlc() {
+        val overrideColor = Color.Yellow
+
+        rule.setContentWithTheme {
+            ConfirmationWithMaterialSlc(
+                onTimeout = {},
+                content = {},
+                backgroundColor = overrideColor,
+                modifier = Modifier.testTag(TEST_TAG),
+            )
+        }
+
+        rule.onNodeWithTag(TEST_TAG)
+            .captureToImage()
+            .assertContainsColor(overrideColor, 100.0f)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    private fun verifyBackgroundColor(
+        expected: @Composable () -> Color,
+        content: @Composable () -> Unit
+    ) {
+        val testBackground = Color.White
+        var expectedBackground = Color.Transparent
+
+        rule.setContentWithTheme {
+            Box(modifier = Modifier
+                .fillMaxSize()
+                .background(testBackground)) {
+                expectedBackground = expected()
+                content()
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG)
+            .captureToImage()
+            .assertContainsColor(expectedBackground, 100.0f)
+    }
+}
+
+@Suppress("DEPRECATION")
+class DialogWithMaterialSlcTextStyleTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun gives_title_correct_textstyle_on_alert_for_buttons() {
+        var actualTextStyle = TextStyle.Default
+        var expectedTextStyle = TextStyle.Default
+
+        rule.setContentWithTheme {
+            expectedTextStyle = MaterialTheme.typography.title3
+            AlertWithMaterialSlc(
+                title = { actualTextStyle = LocalTextStyle.current },
+                negativeButton = {},
+                positiveButton = {},
+            )
+        }
+
+        Assert.assertEquals(expectedTextStyle, actualTextStyle)
+    }
+
+    @Test
+    fun gives_title_correct_textstyle_on_alert_for_chips() {
+        var actualTextStyle = TextStyle.Default
+        var expectedTextStyle = TextStyle.Default
+
+        rule.setContentWithTheme {
+            expectedTextStyle = MaterialTheme.typography.title3
+            AlertWithMaterialSlc(
+                title = { actualTextStyle = LocalTextStyle.current },
+                message = {},
+                content = {},
+            )
+        }
+
+        Assert.assertEquals(expectedTextStyle, actualTextStyle)
+    }
+
+    @Test
+    fun gives_body_correct_textstyle_on_alert_for_buttons() {
+        var actualTextStyle = TextStyle.Default
+        var expectedTextStyle = TextStyle.Default
+
+        rule.setContentWithTheme {
+            expectedTextStyle = MaterialTheme.typography.body2
+            AlertWithMaterialSlc(
+                title = { Text("Title") },
+                negativeButton = {},
+                positiveButton = {},
+                content = { actualTextStyle = LocalTextStyle.current }
+            )
+        }
+
+        Assert.assertEquals(expectedTextStyle, actualTextStyle)
+    }
+
+    @Test
+    fun gives_body_correct_textstyle_on_alert_for_chips() {
+        var actualTextStyle = TextStyle.Default
+        var expectedTextStyle = TextStyle.Default
+
+        rule.setContentWithTheme {
+            expectedTextStyle = MaterialTheme.typography.body2
+            AlertWithMaterialSlc(
+                title = { Text("Title") },
+                message = { actualTextStyle = LocalTextStyle.current },
+                content = {},
+            )
+        }
+
+        Assert.assertEquals(expectedTextStyle, actualTextStyle)
+    }
+
+    @Test
+    fun gives_title_correct_textstyle_on_ConfirmationWithMaterialSlc() {
+        var actualTextStyle = TextStyle.Default
+        var expectedTextStyle = TextStyle.Default
+
+        rule.setContentWithTheme {
+            expectedTextStyle = MaterialTheme.typography.title3
+            ConfirmationWithMaterialSlc(
+                onTimeout = {},
+                content = { actualTextStyle = LocalTextStyle.current },
+            )
+        }
+
+        Assert.assertEquals(expectedTextStyle, actualTextStyle)
+    }
+}
diff --git a/wear/compose/compose-material/src/androidMain/kotlin/androidx/wear/compose/material/dialog/Dialog.android.kt b/wear/compose/compose-material/src/androidMain/kotlin/androidx/wear/compose/material/dialog/Dialog.android.kt
index 3ee68c0b..adcec0e 100644
--- a/wear/compose/compose-material/src/androidMain/kotlin/androidx/wear/compose/material/dialog/Dialog.android.kt
+++ b/wear/compose/compose-material/src/androidMain/kotlin/androidx/wear/compose/material/dialog/Dialog.android.kt
@@ -38,6 +38,8 @@
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.window.Dialog
 import androidx.compose.ui.window.DialogProperties
+import androidx.wear.compose.foundation.lazy.ScalingLazyListState
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
 import androidx.wear.compose.material.CASUAL
 import androidx.wear.compose.material.MaterialTheme
 import androidx.wear.compose.material.PositionIndicator
@@ -46,11 +48,9 @@
 import androidx.wear.compose.material.STANDARD_IN
 import androidx.wear.compose.material.STANDARD_OUT
 import androidx.wear.compose.material.Scaffold
-import androidx.wear.compose.material.ScalingLazyListState
 import androidx.wear.compose.material.SwipeToDismissBox
 import androidx.wear.compose.material.Vignette
 import androidx.wear.compose.material.VignettePosition
-import androidx.wear.compose.material.rememberScalingLazyListState
 import androidx.wear.compose.material.rememberSwipeToDismissBoxState
 
 /**
@@ -87,6 +87,80 @@
     properties: DialogProperties = DialogProperties(),
     content: @Composable () -> Unit,
 ) {
+    Dialog(
+        showDialog = showDialog,
+        onDismissRequest = onDismissRequest,
+        modifier = modifier,
+        properties = properties,
+        positionIndicator = { if (scrollState != null) PositionIndicator(scrollState) },
+        content = content
+    )
+}
+
+/**
+ * [Dialog] displays a full-screen dialog, layered over any other content. It takes a single slot,
+ * which is expected to be an opinionated Wear dialog content, such as [Alert]
+ * or [Confirmation].
+ *
+ * The dialog supports swipe-to-dismiss and reveals the parent content in the background
+ * during the swipe gesture.
+ *
+ * Example of content using [Dialog] to trigger an alert dialog using [Alert]:
+ * @sample androidx.wear.compose.material.samples.AlertDialogSample
+ *
+ * Example of content using [Dialog] to trigger a confirmation dialog using
+ * [Confirmation]:
+ * @sample androidx.wear.compose.material.samples.ConfirmationDialogSample
+
+ * @param showDialog Controls whether to display the [Dialog]. Set to true initially to trigger
+ * an 'intro' animation and display the [Dialog]. Subsequently, setting to false triggers
+ * an 'outro' animation, then [Dialog] calls [onDismissRequest] and hides itself.
+ * @param onDismissRequest Executes when the user dismisses the dialog.
+ * Must remove the dialog from the composition.
+ * @param modifier Modifier to be applied to the dialog.
+ * @param scrollState The scroll state for the dialog so that the scroll position can be displayed.
+ * @param properties Typically platform specific properties to further configure the dialog.
+ * @param content Slot for dialog content such as [Alert] or [Confirmation].
+ */
+@Suppress("DEPRECATION")
+@Deprecated(
+    "This overload is provided for backwards compatibility with Compose for Wear OS 1.1." +
+        "A newer overload is available which uses ScalingLazyListState from " +
+        "wear.compose.foundation.lazy package", level = DeprecationLevel.HIDDEN
+)
+@Composable
+public fun Dialog(
+    showDialog: Boolean,
+    onDismissRequest: () -> Unit,
+    modifier: Modifier = Modifier,
+    scrollState: androidx.wear.compose.material.ScalingLazyListState? =
+        androidx.wear.compose.material.rememberScalingLazyListState(),
+    properties: DialogProperties = DialogProperties(),
+    content: @Composable () -> Unit,
+) {
+    Dialog(
+        showDialog = showDialog,
+        onDismissRequest = onDismissRequest,
+        modifier = modifier,
+        properties = properties,
+        positionIndicator = { if (scrollState != null) PositionIndicator(scrollState) },
+        content = content
+    )
+}
+
+/**
+ * A Dialog composable which was created for sharing code between 2 versions
+ * of public [Dialog]s - with ScalingLazyListState from material and another from foundation.lazy
+ */
+@Composable
+private fun Dialog(
+    showDialog: Boolean,
+    onDismissRequest: () -> Unit,
+    modifier: Modifier = Modifier,
+    properties: DialogProperties = DialogProperties(),
+    positionIndicator: @Composable () -> Unit,
+    content: @Composable () -> Unit,
+) {
     // Transitions for background and 'dialog content' alpha.
     var alphaTransitionState by remember {
         mutableStateOf(MutableTransitionState(AlphaStage.IntroFadeOut))
@@ -101,7 +175,8 @@
 
     if (showDialog ||
         alphaTransitionState.targetState != AlphaStage.IntroFadeOut ||
-        scaleTransitionState.targetState != ScaleStage.Intro) {
+        scaleTransitionState.targetState != ScaleStage.Intro
+    ) {
         Dialog(
             onDismissRequest = onDismissRequest,
             properties = properties,
@@ -114,17 +189,19 @@
                 vignette = {
                     AnimatedVisibility(
                         visible = scaleTransitionState.targetState == ScaleStage.Display,
-                        enter = fadeIn(animationSpec =
+                        enter = fadeIn(
+                            animationSpec =
                             TweenSpec(durationMillis = CASUAL, easing = STANDARD_IN)
                         ),
-                        exit = fadeOut(animationSpec =
+                        exit = fadeOut(
+                            animationSpec =
                             TweenSpec(durationMillis = CASUAL, easing = STANDARD_OUT)
                         ),
                     ) {
                         Vignette(vignettePosition = VignettePosition.TopAndBottom)
                     }
                 },
-                positionIndicator = { if (scrollState != null) PositionIndicator(scrollState) },
+                positionIndicator = positionIndicator,
                 modifier = modifier,
             ) {
                 SwipeToDismissBox(
@@ -142,8 +219,11 @@
                     }
                 ) { isBackground ->
                     Box(
-                        modifier = Modifier.matchParentSize().background(
-                            MaterialTheme.colors.background.copy(alpha = backgroundAlpha))
+                        modifier = Modifier
+                            .matchParentSize()
+                            .background(
+                                MaterialTheme.colors.background.copy(alpha = backgroundAlpha)
+                            )
                     )
                     if (!isBackground) content()
                 }
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Button.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Button.kt
index 5d6eb8c..19cd249 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Button.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Button.kt
@@ -16,31 +16,21 @@
 package androidx.wear.compose.material
 
 import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.foundation.layout.defaultMinSize
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.State
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 
@@ -83,6 +73,7 @@
  * [Interaction]s for this Button. You can create and pass in your own remembered
  * [MutableInteractionSource] if you want to observe [Interaction]s and customize the
  * appearance / behavior of this Button in different [Interaction]s.
+ * @param content The content displayed on the [Button] such as text, icon or image.
  */
 @Deprecated("This overload is provided for backwards compatibility with Compose for Wear OS 1.0." +
     "A newer overload is available with an additional shape parameter.",
@@ -150,6 +141,7 @@
  * shape is a key characteristic of the Wear Material Theme.
  * @param border [ButtonBorder] that will be used to resolve the button border in different states.
  * See [ButtonDefaults.buttonBorder].
+ * @param content The content displayed on the [Button] such as text, icon or image.
  */
 @Composable
 public fun Button(
@@ -162,40 +154,23 @@
     border: ButtonBorder = ButtonDefaults.buttonBorder(),
     content: @Composable BoxScope.() -> Unit,
 ) {
-    val borderStroke = border.borderStroke(enabled = enabled).value
-    Box(
-        contentAlignment = Alignment.Center,
-        modifier = modifier
-            .defaultMinSize(
-                minWidth = ButtonDefaults.DefaultButtonSize,
-                minHeight = ButtonDefaults.DefaultButtonSize
-            )
-            .then(
-                if (borderStroke != null) Modifier.border(border = borderStroke, shape = shape)
-                else Modifier
-            )
-            .clip(shape)
-            .clickable(
-                onClick = onClick,
-                enabled = enabled,
-                role = Role.Button,
-                interactionSource = interactionSource,
-                indication = rememberRipple(),
-            )
-            .background(
-                color = colors.backgroundColor(enabled = enabled).value,
-                shape = shape
-            )
-    ) {
-        val contentColor = colors.contentColor(enabled = enabled).value
-        CompositionLocalProvider(
+    val contentColor = colors.contentColor(enabled = enabled).value
+    androidx.wear.compose.materialcore.Button(
+        onClick = onClick,
+        modifier = modifier,
+        enabled = enabled,
+        backgroundColor = { colors.backgroundColor(enabled = it) },
+        interactionSource = interactionSource,
+        shape = shape,
+        border = { border.borderStroke(enabled = it) },
+        minButtonSize = ButtonDefaults.DefaultButtonSize,
+        contentProviderValues = arrayOf(
             LocalContentColor provides contentColor,
             LocalContentAlpha provides contentColor.alpha,
-            LocalTextStyle provides MaterialTheme.typography.button,
-        ) {
-            content()
-        }
-    }
+            LocalTextStyle provides MaterialTheme.typography.button
+        ),
+        content = content
+    )
 }
 
 /**
@@ -234,6 +209,7 @@
  * shape is a key characteristic of the Wear Material Theme.
  * @param border [ButtonBorder] that will be used to resolve the button border in different states.
  * See [ButtonDefaults.outlinedButtonBorder].
+ * @param content The content displayed on the [OutlinedButton] such as text, icon or image.
  */
 @Composable
 public fun OutlinedButton(
@@ -282,6 +258,7 @@
  * [Interaction]s for this Button. You can create and pass in your own remembered
  * [MutableInteractionSource] if you want to observe [Interaction]s and customize the
  * appearance / behavior of this Button in different [Interaction]s.
+ * @param content The content displayed on the [CompactButton] such as text, icon or image.
  */
 @Deprecated("This overload is provided for backwards compatibility with Compose for Wear OS 1.0." +
     "A newer overload is available with an additional shape parameter.",
@@ -343,6 +320,9 @@
  * appearance / behavior of this Button in different [Interaction]s.
  * @param shape Defines the button's shape. It is strongly recommended to use the default as this
  * shape is a key characteristic of the Wear Material Theme.
+ * @param border [ButtonBorder] that will be used to resolve the button border in different states.
+ * See [ButtonDefaults.outlinedButtonBorder].
+ * @param content The content displayed on the [CompactButton] such as text, icon or image.
  */
 @Composable
 public fun CompactButton(
@@ -356,40 +336,25 @@
     border: ButtonBorder = ButtonDefaults.buttonBorder(),
     content: @Composable BoxScope.() -> Unit,
 ) {
-    val borderStroke = border.borderStroke(enabled).value
-    Box(
-        contentAlignment = Alignment.Center,
+    val contentColor = colors.contentColor(enabled = enabled).value
+    androidx.wear.compose.materialcore.Button(
+        onClick = onClick,
         modifier = modifier
-            .clip(shape)
-            .clickable(
-                onClick = onClick,
-                enabled = enabled,
-                role = Role.Button,
-                interactionSource = interactionSource,
-                indication = rememberRipple()
-            )
             .padding(backgroundPadding)
-            .requiredSize(ButtonDefaults.ExtraSmallButtonSize)
-            .then(
-                if (borderStroke != null) Modifier.border(
-                    border = borderStroke,
-                    shape = shape
-                ) else Modifier
-            )
-            .background(
-                color = colors.backgroundColor(enabled = enabled).value,
-                shape = shape
-            )
-    ) {
-        val contentColor = colors.contentColor(enabled = enabled).value
-        CompositionLocalProvider(
+            .requiredSize(ButtonDefaults.ExtraSmallButtonSize),
+        enabled = enabled,
+        backgroundColor = { colors.backgroundColor(enabled = it) },
+        interactionSource = interactionSource,
+        shape = shape,
+        border = { border.borderStroke(it) },
+        minButtonSize = ButtonDefaults.ExtraSmallButtonSize,
+        contentProviderValues = arrayOf(
             LocalContentColor provides contentColor,
             LocalContentAlpha provides contentColor.alpha,
             LocalTextStyle provides MaterialTheme.typography.button,
-        ) {
-            content()
-        }
-    }
+        ),
+        content = content
+    )
 }
 
 /**
@@ -430,6 +395,7 @@
  * shape is a key characteristic of the Wear Material Theme.
  * @param border [ButtonBorder] that will be used to resolve the button border in different states.
  * See [ButtonDefaults.outlinedButtonBorder].
+ * @param content The content displayed on the [OutlinedCompactButton] such as text, icon or image.
  */
 @Composable
 public fun OutlinedCompactButton(
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt
index 6bc2957..31db6b9 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt
@@ -1,4 +1,3 @@
-
 /*
  * Copyright 2021 The Android Open Source Project
  *
@@ -16,6 +15,11 @@
  */
 package androidx.wear.compose.material
 
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn as ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.ScalingParams as ScalingParams
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults as ScalingLazyColumnDefaults
+import androidx.wear.compose.foundation.lazy.AutoCenteringParams as AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.ScalingLazyListState as ScalingLazyListState
 import androidx.compose.animation.core.CubicBezierEasing
 import androidx.compose.animation.core.DecayAnimationSpec
 import androidx.compose.animation.core.Easing
@@ -113,7 +117,7 @@
     readOnly: Boolean = false,
     readOnlyLabel: @Composable (BoxScope.() -> Unit)? = null,
     onSelected: () -> Unit = {},
-    scalingParams: ScalingParams = PickerDefaults.scalingParams(),
+    scalingParams: ScalingParams = PickerDefaults.defaultScalingParams(),
     separation: Dp = 0.dp,
     /* @FloatRange(from = 0.0, to = 0.5) */
     gradientRatio: Float = PickerDefaults.DefaultGradientRatio,
@@ -224,6 +228,44 @@
     }
 }
 
+@Suppress("DEPRECATION")
+@Deprecated(
+    "This overload is provided for backwards compatibility with Compose for Wear OS 1.1." +
+        "A newer overload is available which uses ScalingParams from " +
+        "androidx.wear.compose.foundation.lazy package", level = DeprecationLevel.HIDDEN
+)
+@Composable
+public fun Picker(
+    state: PickerState,
+    contentDescription: String?,
+    modifier: Modifier = Modifier,
+    readOnly: Boolean = false,
+    readOnlyLabel: @Composable (BoxScope.() -> Unit)? = null,
+    onSelected: () -> Unit = {},
+    scalingParams: androidx.wear.compose.material.ScalingParams = PickerDefaults.scalingParams(),
+    separation: Dp = 0.dp,
+    /* @FloatRange(from = 0.0, to = 0.5) */
+    gradientRatio: Float = PickerDefaults.DefaultGradientRatio,
+    gradientColor: Color = MaterialTheme.colors.background,
+    flingBehavior: FlingBehavior = PickerDefaults.flingBehavior(state),
+    userScrollEnabled: Boolean = true,
+    option: @Composable PickerScope.(optionIndex: Int) -> Unit
+) = Picker(
+    state = state,
+    contentDescription = contentDescription,
+    modifier = modifier,
+    readOnly = readOnly,
+    readOnlyLabel = readOnlyLabel,
+    onSelected = onSelected,
+    scalingParams = convertToDefaultFoundationScalingParams(scalingParams),
+    separation = separation,
+    gradientRatio = gradientRatio,
+    gradientColor = gradientColor,
+    flingBehavior = flingBehavior,
+    userScrollEnabled = userScrollEnabled,
+    option = option
+)
+
 /**
  * A scrollable list of items to pick from. By default, items will be repeated
  * "infinitely" in both directions, unless [PickerState#repeatItems] is specified as false.
@@ -265,6 +307,7 @@
  * use on a screen, it is recommended that this content is given [Alignment.Center] in order to
  * align with the centrally selected Picker value.
  */
+@Suppress("DEPRECATION")
 @Deprecated("This overload is provided for backwards compatibility with Compose for Wear OS 1.1." +
     "A newer overload is available with additional userScrollEnabled parameter which improves " +
     "accessibility of [Picker].", level = DeprecationLevel.HIDDEN)
@@ -276,7 +319,7 @@
     readOnly: Boolean = false,
     readOnlyLabel: @Composable (BoxScope.() -> Unit)? = null,
     onSelected: () -> Unit = {},
-    scalingParams: ScalingParams = PickerDefaults.scalingParams(),
+    scalingParams: androidx.wear.compose.material.ScalingParams = PickerDefaults.scalingParams(),
     separation: Dp = 0.dp,
     /* @FloatRange(from = 0.0, to = 0.5) */
     gradientRatio: Float = PickerDefaults.DefaultGradientRatio,
@@ -290,7 +333,7 @@
     readOnly = readOnly,
     readOnlyLabel = readOnlyLabel,
     onSelected = onSelected,
-    scalingParams = scalingParams,
+    scalingParams = convertToDefaultFoundationScalingParams(scalingParams),
     separation = separation,
     gradientRatio = gradientRatio,
     gradientColor = gradientColor,
@@ -333,6 +376,7 @@
  * use on a screen, it is recommended that this content is given [Alignment.Center] in order to
  * align with the centrally selected Picker value.
  */
+@Suppress("DEPRECATION")
 @Deprecated("This overload is provided for backwards compatibility with Compose for Wear OS 1.0." +
   "A newer overload is available with additional contentDescription, onSelected and " +
   "userScrollEnabled parameters, which improves accessibility of [Picker].")
@@ -342,7 +386,7 @@
     modifier: Modifier = Modifier,
     readOnly: Boolean = false,
     readOnlyLabel: @Composable (BoxScope.() -> Unit)? = null,
-    scalingParams: ScalingParams = PickerDefaults.scalingParams(),
+    scalingParams: androidx.wear.compose.material.ScalingParams = PickerDefaults.scalingParams(),
     separation: Dp = 0.dp,
     /* @FloatRange(from = 0.0, to = 0.5) */
     gradientRatio: Float = PickerDefaults.DefaultGradientRatio,
@@ -355,7 +399,7 @@
     modifier = modifier,
     readOnly = readOnly,
     readOnlyLabel = readOnlyLabel,
-    scalingParams = scalingParams,
+    scalingParams = convertToDefaultFoundationScalingParams(scalingParams),
     separation = separation,
     gradientRatio = gradientRatio,
     gradientColor = gradientColor,
@@ -557,14 +601,27 @@
         }
     }
 }
+
 /**
  * Contains the default values used by [Picker]
  */
 public object PickerDefaults {
+
     /**
      * Scaling params are used to determine when items start to be scaled down and alpha applied,
      * and how much. For details, see [ScalingParams]
      */
+    @Suppress("DEPRECATION")
+    @Deprecated(
+        "This overload is provided for backwards compatibility with Compose for" +
+            " Wear OS 1.1 and was deprecated. Use [defaultScalingParams] instead",
+        replaceWith = ReplaceWith(
+            "PickerDefaults.defaultScalingParams(edgeScale," +
+                " edgeAlpha, minElementHeight, maxElementHeight, minTransitionArea, " +
+                "maxTransitionArea, scaleInterpolator, viewportVerticalOffsetResolver)"
+        ),
+        level = DeprecationLevel.WARNING
+    )
     public fun scalingParams(
         edgeScale: Float = 0.45f,
         edgeAlpha: Float = 1.0f,
@@ -574,16 +631,42 @@
         maxTransitionArea: Float = 0.45f,
         scaleInterpolator: Easing = CubicBezierEasing(0.25f, 0.00f, 0.75f, 1.00f),
         viewportVerticalOffsetResolver: (Constraints) -> Int = { (it.maxHeight / 5f).toInt() }
-    ): ScalingParams = DefaultScalingParams(
-        edgeScale = edgeScale,
-        edgeAlpha = edgeAlpha,
-        minElementHeight = minElementHeight,
-        maxElementHeight = maxElementHeight,
-        minTransitionArea = minTransitionArea,
-        maxTransitionArea = maxTransitionArea,
-        scaleInterpolator = scaleInterpolator,
-        viewportVerticalOffsetResolver = viewportVerticalOffsetResolver
-    )
+    ): androidx.wear.compose.material.ScalingParams =
+        androidx.wear.compose.material.ScalingLazyColumnDefaults.scalingParams(
+            edgeScale = edgeScale,
+            edgeAlpha = edgeAlpha,
+            minElementHeight = minElementHeight,
+            maxElementHeight = maxElementHeight,
+            minTransitionArea = minTransitionArea,
+            maxTransitionArea = maxTransitionArea,
+            scaleInterpolator = scaleInterpolator,
+            viewportVerticalOffsetResolver = viewportVerticalOffsetResolver
+        )
+
+    /**
+     * Scaling params are used to determine when items start to be scaled down and alpha applied,
+     * and how much. For details, see [ScalingParams]
+     */
+    public fun defaultScalingParams(
+        edgeScale: Float = 0.45f,
+        edgeAlpha: Float = 1.0f,
+        minElementHeight: Float = 0.0f,
+        maxElementHeight: Float = 0.0f,
+        minTransitionArea: Float = 0.45f,
+        maxTransitionArea: Float = 0.45f,
+        scaleInterpolator: Easing = CubicBezierEasing(0.25f, 0.00f, 0.75f, 1.00f),
+        viewportVerticalOffsetResolver: (Constraints) -> Int = { (it.maxHeight / 5f).toInt() }
+    ): ScalingParams =
+        ScalingLazyColumnDefaults.scalingParams(
+            edgeScale = edgeScale,
+            edgeAlpha = edgeAlpha,
+            minElementHeight = minElementHeight,
+            maxElementHeight = maxElementHeight,
+            minTransitionArea = minTransitionArea,
+            maxTransitionArea = maxTransitionArea,
+            scaleInterpolator = scaleInterpolator,
+            viewportVerticalOffsetResolver = viewportVerticalOffsetResolver
+        )
 
     /**
      * Create and remember a [FlingBehavior] that will represent natural fling curve with snap to
@@ -597,14 +680,13 @@
         state: PickerState,
         decay: DecayAnimationSpec<Float> = exponentialDecay()
     ): FlingBehavior {
-        return remember(state, decay) {
-            ScalingLazyColumnSnapFlingBehavior(
-                state = state.scalingLazyListState,
-                snapOffset = 0,
-                decay = decay
-            )
-        }
+        return ScalingLazyColumnDefaults.snapFlingBehavior(
+            state = state.scalingLazyListState,
+            snapOffset = 0.dp,
+            decay = decay
+        )
     }
+
     /**
      * Default Picker gradient ratio - the proportion of the Picker height allocated to each of the
      * of the top and bottom gradients.
@@ -624,6 +706,22 @@
 
 private fun positiveModule(n: Int, mod: Int) = ((n % mod) + mod) % mod
 
+private fun convertToDefaultFoundationScalingParams(
+    @Suppress("DEPRECATION")
+    scalingParams: androidx.wear.compose.material.ScalingParams
+): ScalingParams = PickerDefaults.defaultScalingParams(
+    edgeScale = scalingParams.edgeScale,
+    edgeAlpha = scalingParams.edgeAlpha,
+    minElementHeight = scalingParams.minElementHeight,
+    maxElementHeight = scalingParams.maxElementHeight,
+    minTransitionArea = scalingParams.minTransitionArea,
+    maxTransitionArea = scalingParams.maxTransitionArea,
+    scaleInterpolator = scalingParams.scaleInterpolator,
+    viewportVerticalOffsetResolver = { viewportConstraints ->
+        scalingParams.resolveViewportVerticalOffset(viewportConstraints)
+    }
+)
+
 @Stable
 private class PickerScopeImpl(
     private val pickerState: PickerState
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt
index ecd6bde..184e824 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt
@@ -16,6 +16,10 @@
 
 package androidx.wear.compose.material
 
+import androidx.wear.compose.foundation.lazy.ScalingLazyListState as ScalingLazyListState
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn as ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.ScalingLazyListItemInfo as ScalingLazyListItemInfo
+import androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType as ScalingLazyListAnchorType
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.AnimationSpec
@@ -204,6 +208,39 @@
 )
 
 /**
+ * Creates an [PositionIndicator] based on the values in a [ScalingLazyListState] object that
+ * a [ScalingLazyColumn] uses.
+ *
+ * For more information, see the
+ * [Scroll indicators](https://developer.android.com/training/wearables/components/scroll)
+ * guide.
+ *
+ * @param scalingLazyListState the [ScalingLazyListState] to use as the basis for the
+ * PositionIndicatorState.
+ * @param modifier The modifier to be applied to the component
+ * @param reverseDirection Reverses direction of PositionIndicator if true
+ */
+@Suppress("DEPRECATION")
+@Deprecated("This overload is provided for backwards compatibility with Compose for Wear OS 1.1." +
+        "A newer overload is available which uses ScalingLazyListState from " +
+        "androidx.wear.compose.foundation.lazy package", level = DeprecationLevel.WARNING)
+@Composable
+public fun PositionIndicator(
+    scalingLazyListState: androidx.wear.compose.material.ScalingLazyListState,
+    modifier: Modifier = Modifier,
+    reverseDirection: Boolean = false
+) = PositionIndicator(
+    state = MaterialScalingLazyColumnStateAdapter(
+        state = scalingLazyListState
+    ),
+    indicatorHeight = 50.dp,
+    indicatorWidth = 4.dp,
+    paddingHorizontal = 5.dp,
+    modifier = modifier,
+    reverseDirection = reverseDirection
+)
+
+/**
  * Creates an [PositionIndicator] based on the values in a [LazyListState] object that
  * a [LazyColumn] uses.
  *
@@ -654,51 +691,13 @@
  */
 internal class ScalingLazyColumnStateAdapter(
     private val state: ScalingLazyListState
-) : PositionIndicatorState {
-    override val positionFraction: Float
-        get() {
-            return if (state.layoutInfo.visibleItemsInfo.isEmpty()) {
-                0.0f
-            } else {
-                val decimalFirstItemIndex = decimalFirstItemIndex()
-                val decimalLastItemIndex = decimalLastItemIndex()
-                val decimalLastItemIndexDistanceFromEnd = state.layoutInfo.totalItemsCount -
-                    decimalLastItemIndex
+) : BaseScalingLazyColumnStateAdapter() {
 
-                if (decimalFirstItemIndex + decimalLastItemIndexDistanceFromEnd == 0.0f) {
-                    0.0f
-                } else {
-                    decimalFirstItemIndex /
-                        (decimalFirstItemIndex + decimalLastItemIndexDistanceFromEnd)
-                }
-            }
-        }
+    override fun noVisibleItems(): Boolean = state.layoutInfo.visibleItemsInfo.isEmpty()
 
-    override fun sizeFraction(scrollableContainerSizePx: Float) =
-        if (state.layoutInfo.totalItemsCount == 0) {
-            1.0f
-        } else {
-            val decimalFirstItemIndex = decimalFirstItemIndex()
-            val decimalLastItemIndex = decimalLastItemIndex()
+    override fun totalItemsCount(): Int = state.layoutInfo.totalItemsCount
 
-            (decimalLastItemIndex - decimalFirstItemIndex) /
-                state.layoutInfo.totalItemsCount.toFloat()
-        }
-
-    override fun visibility(scrollableContainerSizePx: Float): PositionIndicatorVisibility {
-        val canScroll = state.layoutInfo.visibleItemsInfo.isNotEmpty() &&
-            (decimalFirstItemIndex() > 0 ||
-                decimalLastItemIndex() < state.layoutInfo.totalItemsCount)
-        return if (canScroll) {
-            if (state.isScrollInProgress) {
-                PositionIndicatorVisibility.Show
-            } else {
-                PositionIndicatorVisibility.AutoHide
-            }
-        } else {
-            PositionIndicatorVisibility.Hide
-        }
-    }
+    override fun isScrollInProgress(): Boolean = state.isScrollInProgress
 
     override fun hashCode(): Int {
         return state.hashCode()
@@ -717,7 +716,91 @@
      * Note that decimal index calculations ignore spacing between list items both for determining
      * the number and the number of visible items.
      */
-    private fun decimalLastItemIndex(): Float {
+    override fun decimalLastItemIndex(): Float {
+        if (state.layoutInfo.visibleItemsInfo.isEmpty()) return 0f
+        val lastItem = state.layoutInfo.visibleItemsInfo.last()
+        // This is the offset of the last item w.r.t. the ScalingLazyColumn coordinate system where
+        // 0 in the center of the visible viewport and +/-(state.viewportHeightPx / 2f) are the
+        // start and end of the viewport.
+        //
+        // Note that [ScalingLazyListAnchorType] determines how the list items are anchored to the
+        // center of the viewport, it does not change viewport coordinates. As a result this
+        // calculation needs to take the anchorType into account to calculate the correct end
+        // of list item offset.
+        val lastItemEndOffset = lastItem.startOffset(state.layoutInfo.anchorType) + lastItem.size
+        val viewportEndOffset = state.layoutInfo.viewportSize.height / 2f
+        // Coerce item size to at least 1 to avoid divide by zero for zero height items
+        val lastItemVisibleFraction =
+            (1f - ((lastItemEndOffset - viewportEndOffset) /
+                lastItem.size.coerceAtLeast(1))).coerceAtMost(1f)
+
+        return lastItem.index.toFloat() + lastItemVisibleFraction
+    }
+
+    /**
+     * Provide a float value that represents the index of first visible list item in a scaling lazy
+     * column. The value should be in the range from [n,n+1] for a given index n, where n is the
+     * index of the first visible item and a value of n represents that all of the item is visible
+     * in the viewport and a value of n+1 means that only the very end|bottom of the list item is
+     * visible at the start|top of the viewport.
+     *
+     * Note that decimal index calculations ignore spacing between list items both for determining
+     * the number and the number of visible items.
+     */
+    override fun decimalFirstItemIndex(): Float {
+        if (state.layoutInfo.visibleItemsInfo.isEmpty()) return 0f
+        val firstItem = state.layoutInfo.visibleItemsInfo.first()
+        val firstItemStartOffset = firstItem.startOffset(state.layoutInfo.anchorType)
+        val viewportStartOffset = - (state.layoutInfo.viewportSize.height / 2f)
+        // Coerce item size to at least 1 to avoid divide by zero for zero height items
+        val firstItemInvisibleFraction =
+            ((viewportStartOffset - firstItemStartOffset) /
+                firstItem.size.coerceAtLeast(1)).coerceAtLeast(0f)
+
+        return firstItem.index.toFloat() + firstItemInvisibleFraction
+    }
+}
+
+/**
+ * An implementation of [PositionIndicatorState] to display the amount and position of a
+ * [ScalingLazyColumn] component via its [ScalingLazyListState].
+ *
+ * Note that size and position calculations ignore spacing between list items both for determining
+ * the number and the number of visible items.
+
+ * @param state the [ScalingLazyListState] to adapt.
+ */
+@Deprecated("Use [ScalingLazyColumnStateAdapter] instead")
+internal class MaterialScalingLazyColumnStateAdapter(
+    @Suppress("DEPRECATION")
+    private val state: androidx.wear.compose.material.ScalingLazyListState
+) : BaseScalingLazyColumnStateAdapter() {
+
+    override fun noVisibleItems(): Boolean = state.layoutInfo.visibleItemsInfo.isEmpty()
+
+    override fun totalItemsCount(): Int = state.layoutInfo.totalItemsCount
+
+    override fun isScrollInProgress(): Boolean = state.isScrollInProgress
+
+    override fun hashCode(): Int {
+        return state.hashCode()
+    }
+
+    @Suppress("DEPRECATION")
+    override fun equals(other: Any?): Boolean {
+        return (other as? MaterialScalingLazyColumnStateAdapter)?.state == state
+    }
+
+    /**
+     * Provide a float value that represents the index of the last visible list item in a scaling
+     * lazy column. The value should be in the range from [n,n+1] for a given index n, where n is
+     * the index of the last visible item and a value of n represents that only the very start|top
+     * of the item is visible, and n+1 means that whole of the item is visible in the viewport.
+     *
+     * Note that decimal index calculations ignore spacing between list items both for determining
+     * the number and the number of visible items.
+     */
+    override fun decimalLastItemIndex(): Float {
         if (state.layoutInfo.visibleItemsInfo.isEmpty()) return 0f
         val lastItem = state.layoutInfo.visibleItemsInfo.last()
         // This is the offset of the last item w.r.t. the ScalingLazyColumn coordinate system where
@@ -748,7 +831,7 @@
      * Note that decimal index calculations ignore spacing between list items both for determining
      * the number and the number of visible items.
      */
-    private fun decimalFirstItemIndex(): Float {
+    override fun decimalFirstItemIndex(): Float {
         if (state.layoutInfo.visibleItemsInfo.isEmpty()) return 0f
         val firstItem = state.layoutInfo.visibleItemsInfo.first()
         val firstItemStartOffset = firstItem.startOffset(state.anchorType.value!!)
@@ -762,6 +845,63 @@
     }
 }
 
+internal abstract class BaseScalingLazyColumnStateAdapter : PositionIndicatorState {
+    override val positionFraction: Float
+        get() {
+            return if (noVisibleItems()) {
+                0.0f
+            } else {
+                val decimalFirstItemIndex = decimalFirstItemIndex()
+                val decimalLastItemIndex = decimalLastItemIndex()
+                val decimalLastItemIndexDistanceFromEnd = totalItemsCount() -
+                    decimalLastItemIndex
+
+                if (decimalFirstItemIndex + decimalLastItemIndexDistanceFromEnd == 0.0f) {
+                    0.0f
+                } else {
+                    decimalFirstItemIndex /
+                        (decimalFirstItemIndex + decimalLastItemIndexDistanceFromEnd)
+                }
+            }
+        }
+
+    override fun sizeFraction(scrollableContainerSizePx: Float) =
+        if (totalItemsCount() == 0) {
+            1.0f
+        } else {
+            val decimalFirstItemIndex = decimalFirstItemIndex()
+            val decimalLastItemIndex = decimalLastItemIndex()
+
+            (decimalLastItemIndex - decimalFirstItemIndex) /
+                totalItemsCount().toFloat()
+        }
+
+    override fun visibility(scrollableContainerSizePx: Float): PositionIndicatorVisibility {
+        val canScroll = !noVisibleItems() &&
+            (decimalFirstItemIndex() > 0 ||
+                decimalLastItemIndex() < totalItemsCount())
+        return if (canScroll) {
+            if (isScrollInProgress()) {
+                PositionIndicatorVisibility.Show
+            } else {
+                PositionIndicatorVisibility.AutoHide
+            }
+        } else {
+            PositionIndicatorVisibility.Hide
+        }
+    }
+
+    abstract fun noVisibleItems(): Boolean
+
+    abstract fun totalItemsCount(): Int
+
+    abstract fun isScrollInProgress(): Boolean
+
+    abstract fun decimalLastItemIndex(): Float
+
+    abstract fun decimalFirstItemIndex(): Float
+}
+
 /**
  * An implementation of [PositionIndicatorState] to display the amount and position of a
  * [LazyColumn] component via its [LazyListState].
@@ -972,4 +1112,14 @@
     }
 )
 
-private fun sqr(x: Float) = x * x
\ No newline at end of file
+private fun sqr(x: Float) = x * x
+
+/**
+ * Find the start offset of the list item w.r.t. the
+ */
+internal fun ScalingLazyListItemInfo.startOffset(anchorType: ScalingLazyListAnchorType) =
+    offset - if (anchorType == ScalingLazyListAnchorType.ItemCenter) {
+        (size / 2f)
+    } else {
+        0f
+    }
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
index b6dc5d4..d9f36e0 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:Suppress("DEPRECATION")
+
 package androidx.wear.compose.material
 
 import androidx.compose.animation.core.CubicBezierEasing
@@ -35,7 +37,6 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -45,20 +46,22 @@
 import androidx.compose.ui.draw.clipToBounds
 import androidx.compose.ui.graphics.TransformOrigin
 import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.layout.layout
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalInspectionMode
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.offset
+import androidx.wear.compose.foundation.lazy.CombinedPaddingValues
+import androidx.wear.compose.foundation.lazy.verticalNegativePadding
 
 /**
  * Receiver scope which is used by [ScalingLazyColumn].
  */
+@Deprecated("Was moved to androidx.wear.compose.foundation.lazy package. " +
+    "Please use it instead")
 @ScalingLazyScopeMarker
 public sealed interface ScalingLazyListScope {
     /**
@@ -105,7 +108,9 @@
  * will be kept as the first visible one.
  * @param itemContent the content displayed by a single item
  */
-inline fun <T> ScalingLazyListScope.items(
+@Deprecated("Was moved to androidx.wear.compose.foundation.lazy package. " +
+    "Please use it instead")
+public inline fun <T> ScalingLazyListScope.items(
     items: List<T>,
     noinline key: ((item: T) -> Any)? = null,
     crossinline itemContent: @Composable ScalingLazyListItemScope.(item: T) -> Unit
@@ -125,6 +130,8 @@
  * will be kept as the first visible one.
  * @param itemContent the content displayed by a single item
  */
+@Deprecated("Was moved to androidx.wear.compose.foundation.lazy package. " +
+    "Please use it instead")
 inline fun <T> ScalingLazyListScope.itemsIndexed(
     items: List<T>,
     noinline key: ((index: Int, item: T) -> Any)? = null,
@@ -145,6 +152,8 @@
  * will be kept as the first visible one.
  * @param itemContent the content displayed by a single item
  */
+@Deprecated("Was moved to androidx.wear.compose.foundation.lazy package. " +
+    "Please use it instead")
 inline fun <T> ScalingLazyListScope.items(
     items: Array<T>,
     noinline key: ((item: T) -> Any)? = null,
@@ -165,6 +174,8 @@
  * will be kept as the first visible one.
  * @param itemContent the content displayed by a single item
  */
+@Deprecated("Was moved to androidx.wear.compose.foundation.lazy package. " +
+    "Please use it instead")
 public inline fun <T> ScalingLazyListScope.itemsIndexed(
     items: Array<T>,
     noinline key: ((index: Int, item: T) -> Any)? = null,
@@ -174,6 +185,8 @@
 }
 
 @Immutable
+@Deprecated("Was moved to androidx.wear.compose.foundation.lazy package. " +
+    "Please use it instead")
 @kotlin.jvm.JvmInline
 public value class ScalingLazyListAnchorType internal constructor(internal val type: Int) {
 
@@ -242,6 +255,8 @@
  * For an example of a [ScalingLazyColumn] with an explicit itemOffset see:
  * @sample androidx.wear.compose.material.samples.ScalingLazyColumnEdgeAnchoredAndAnimatedScrollTo
  */
+@Deprecated("Was moved to androidx.wear.compose.foundation.lazy package. " +
+    "Please use it instead")
 @Immutable
 public class AutoCenteringParams(
     // @IntRange(from = 0)
@@ -318,6 +333,8 @@
  * null no automatic space will be added and instead the developer can use [contentPadding] to
  * manually arrange the items.
  */
+@Deprecated("Was moved to androidx.wear.compose.foundation.lazy package. " +
+    "Please use it instead")
 @Composable
 public fun ScalingLazyColumn(
     modifier: Modifier = Modifier,
@@ -442,6 +459,8 @@
 /**
  * Contains the default values used by [ScalingLazyColumn]
  */
+@Deprecated("Was moved to androidx.wear.compose.foundation.lazy package. " +
+    "Please use it instead")
 public object ScalingLazyColumnDefaults {
     /**
      * Creates a [ScalingParams] that represents the scaling and alpha properties for a
@@ -670,64 +689,3 @@
         itemScope.content()
     }
 }
-
-@Immutable
-private class CombinedPaddingValues(
-    @Stable
-    val contentPadding: PaddingValues,
-    @Stable
-    val extraPadding: Dp
-) : PaddingValues {
-    override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp =
-        contentPadding.calculateLeftPadding(layoutDirection)
-
-    override fun calculateTopPadding(): Dp =
-        contentPadding.calculateTopPadding() + extraPadding
-
-    override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp =
-        contentPadding.calculateRightPadding(layoutDirection)
-
-    override fun calculateBottomPadding(): Dp =
-        contentPadding.calculateBottomPadding() + extraPadding
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other == null) return false
-        if (this::class != other::class) return false
-
-        other as CombinedPaddingValues
-
-        if (contentPadding != other.contentPadding) return false
-        if (extraPadding != other.extraPadding) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = contentPadding.hashCode()
-        result = 31 * result + extraPadding.hashCode()
-        return result
-    }
-
-    override fun toString(): String {
-        return "CombinedPaddingValuesImpl(contentPadding=$contentPadding, " +
-            "extraPadding=$extraPadding)"
-    }
-}
-
-private fun Modifier.verticalNegativePadding(
-    extraPadding: Dp,
-) = layout { measurable, constraints ->
-    require(constraints.hasBoundedWidth)
-    require(constraints.hasBoundedHeight)
-    val topAndBottomPadding = (extraPadding * 2).roundToPx()
-    val placeable = measurable.measure(
-        constraints.copy(
-            minHeight = constraints.minHeight + topAndBottomPadding,
-            maxHeight = constraints.maxHeight + topAndBottomPadding
-        )
-    )
-
-    layout(placeable.measuredWidth, constraints.maxHeight) {
-        placeable.place(0, -extraPadding.roundToPx())
-    }
-}
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
index 097b26c..382be50 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
@@ -14,16 +14,19 @@
  * limitations under the License.
  */
 
+@file:Suppress("DEPRECATION")
+
 package androidx.wear.compose.material
 
 import androidx.compose.animation.core.Easing
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.lazy.LazyListItemInfo
-import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.lerp
+import androidx.wear.compose.foundation.lazy.ScaleAndAlpha
+import androidx.wear.compose.foundation.lazy.inverseLerp
 import kotlin.math.min
 import kotlin.math.roundToInt
 
@@ -93,6 +96,8 @@
  * point for each item.
  */
 @Stable
+@Deprecated("Was moved to androidx.wear.compose.foundation.lazy package. " +
+    "Please use it instead")
 public interface ScalingParams {
     /**
      * What fraction of the full size of the item to scale it by when most
@@ -443,17 +448,6 @@
     }
 }
 
-@Immutable
-internal data class ScaleAndAlpha(
-    val scale: Float,
-    val alpha: Float
-
-) {
-    companion object {
-        internal val noScaling = ScaleAndAlpha(1.0f, 1.0f)
-    }
-}
-
 /**
  * Calculate the offset from the viewport center line of the Start|Center of an items unadjusted
  * or scaled size. The for items with an height that is an odd number and that have
@@ -501,11 +495,3 @@
     } else {
         0f
     }
-
-/**
- * Inverse linearly interpolate, return what fraction (0f..1f) that [value] is between [start] and
- * [stop]. Returns 0f if value =< start and 1f if value >= stop.
- */
-internal fun inverseLerp(start: Float, stop: Float, value: Float): Float {
-    return ((value - start) / (stop - start)).coerceIn(0f, 1f)
-}
\ No newline at end of file
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnSnapFlingBehavior.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnSnapFlingBehavior.kt
index a1fdfa3..145804d 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnSnapFlingBehavior.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnSnapFlingBehavior.kt
@@ -31,6 +31,7 @@
 import kotlin.math.roundToInt
 import kotlin.math.sqrt
 
+@Suppress("DEPRECATION")
 internal class ScalingLazyColumnSnapFlingBehavior(
     val state: ScalingLazyListState,
     val snapOffset: Int = 0,
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListItemInfo.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListItemInfo.kt
index 39ab142..def6417 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListItemInfo.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListItemInfo.kt
@@ -20,6 +20,8 @@
  *
  * @see ScalingLazyListLayoutInfo
  */
+@Deprecated("Was moved to androidx.wear.compose.foundation.lazy package. " +
+    "Please use it instead")
 public sealed interface ScalingLazyListItemInfo {
     /**
      * The index of the item in the list.
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListItemScope.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListItemScope.kt
index bb00507..d346a0b 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListItemScope.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListItemScope.kt
@@ -13,6 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+@file:Suppress("DEPRECATION")
+
 package androidx.wear.compose.material
 
 import androidx.compose.foundation.layout.height
@@ -28,6 +30,8 @@
  * Receiver scope being used by the item content parameter of ScalingLazyColumn.
  */
 @Stable
+@Deprecated("Was moved to androidx.wear.compose.foundation.lazy package. " +
+    "Please use it instead")
 @ScalingLazyScopeMarker
 public sealed interface ScalingLazyListItemScope {
     /**
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfo.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfo.kt
index 498ca2b..04e1adf 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfo.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfo.kt
@@ -24,10 +24,13 @@
  *
  * Use [ScalingLazyListState.layoutInfo] to retrieve this
  */
+@Deprecated("Was moved to androidx.wear.compose.foundation.lazy package. " +
+    "Please use it instead")
 public sealed interface ScalingLazyListLayoutInfo {
     /**
      * The list of [ScalingLazyListItemInfo] representing all the currently visible items.
      */
+    @Suppress("DEPRECATION")
     val visibleItemsInfo: List<ScalingLazyListItemInfo>
 
     /**
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
index e7e86c7..2926cca 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:Suppress("DEPRECATION")
+
 package androidx.wear.compose.material
 
 import androidx.compose.foundation.MutatePriority
@@ -44,6 +46,8 @@
  * @param initialCenterItemScrollOffset the initial value for
  * [ScalingLazyListState.centerItemScrollOffset] in pixels
  */
+@Deprecated("Was moved to androidx.wear.compose.foundation.lazy package. " +
+    "Please use it instead")
 @Composable
 public fun rememberScalingLazyListState(
     initialCenterItemIndex: Int = 1,
@@ -79,6 +83,8 @@
  * [ScalingLazyColumn]. After the [ScalingLazyColumn] is initially drawn the actual values for the
  * [centerItemIndex] and [centerItemScrollOffset] can be read from the state.
  */
+@Deprecated("Was moved to androidx.wear.compose.foundation.lazy package. " +
+    "Please use it instead")
 @Stable
 class ScalingLazyListState constructor(
     private var initialCenterItemIndex: Int = 1,
@@ -104,14 +110,17 @@
     private val incompleteScrollOffset = mutableStateOf<Int?>(null)
     private val incompleteScrollAnimated = mutableStateOf(false)
 
+    private val _centerItemIndex = derivedStateOf {
+        (layoutInfo as? DefaultScalingLazyListLayoutInfo)?.let {
+            if (it.initialized) it.centerItemIndex else null
+        } ?: initialCenterItemIndex
+    }
+
     /**
      * The index of the item positioned closest to the viewport center
      */
     public val centerItemIndex: Int
-        get() =
-            (layoutInfo as? DefaultScalingLazyListLayoutInfo)?.let {
-                if (it.initialized) it.centerItemIndex else null
-            } ?: initialCenterItemIndex
+        get() = _centerItemIndex.value
 
     internal val topAutoCenteringItemSizePx: Int by derivedStateOf {
         if (extraPaddingPx.value == null || scalingParams.value == null ||
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyScopeMarker.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyScopeMarker.kt
index aedeab3..aaa2487 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyScopeMarker.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyScopeMarker.kt
@@ -19,5 +19,7 @@
 /**
  * DSL marker used to distinguish between lazy layout scope and the item scope.
  */
+@Deprecated("Was moved to androidx.wear.compose.foundation.lazy package. " +
+    "Please use it instead")
 @DslMarker
 annotation class ScalingLazyScopeMarker
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
index f2150c7..b725fb2 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
@@ -31,6 +31,8 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.util.lerp
+import androidx.wear.compose.foundation.lazy.ScalingLazyListState
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 
 /**
  * Scroll an item vertically in/out of view based on a [ScrollState].
@@ -99,8 +101,39 @@
         )
     }
 
+/**
+ * Scroll an item vertically in/out of view based on a [ScalingLazyListState].
+ * Typically used to scroll a [TimeText] item out of view as the user starts to scroll
+ * a [ScalingLazyColumn] of items upwards and bring additional items into view.
+ *
+ * @param scrollState The [ScalingLazyListState] to used as the basis for the scroll-away.
+ * @param itemIndex The item for which the scroll offset will trigger scrolling away.
+ * @param offset Adjustment to the starting point for scrolling away. Positive values result in
+ * the scroll away starting later, negative values start scrolling away earlier.
+ */
+@Deprecated(
+    "This overload is provided for backwards compatibility with Compose for Wear OS 1.1." +
+        "A newer overload is available which uses ScalingLazyListState " +
+        "from wear.compose.foundation.lazy package", level = DeprecationLevel.WARNING
+)
+public fun Modifier.scrollAway(
+    @Suppress("DEPRECATION")
+    scrollState: androidx.wear.compose.material.ScalingLazyListState,
+    itemIndex: Int = 1,
+    offset: Dp = 0.dp,
+): Modifier =
+    scrollAway {
+        ScrollParams(
+            valid = itemIndex < scrollState.layoutInfo.totalItemsCount,
+            yPx = scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }?.let {
+                -it.offset - offset.toPx()
+            }
+        )
+    }
+
 private fun Modifier.scrollAway(scrollFn: Density.() -> ScrollParams): Modifier =
     this.then(
+        @Suppress("ModifierInspectorInfo")
         object : LayoutModifier {
             override fun MeasureScope.measure(
                 measurable: Measurable,
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Stepper.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Stepper.kt
index b9f03b2..ac33051 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Stepper.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Stepper.kt
@@ -55,8 +55,13 @@
  * Step value is calculated as the difference between min and max values divided by [steps]+1.
  * Stepper itself doesn't show the current value but can be displayed via the content slot or
  * [PositionIndicator] if required.
+ * If [value] is not equal to any step value, then it will be coerced to the closest step value.
+ * However, the [value] itself will not be changed and [onValueChange] in this case will
+ * not be triggered.
  *
  * @sample androidx.wear.compose.material.samples.StepperSample
+ * @sample androidx.wear.compose.material.samples.StepperWithoutRangeSemanticsSample
+ * @sample androidx.wear.compose.material.samples.StepperWithCustomSemanticsSample
  *
  * @param value Current value of the Stepper. If outside of [valueRange] provided, value will be
  * coerced to this range.
@@ -73,9 +78,13 @@
  * @param contentColor [Color] representing the color for [content] in the middle.
  * @param iconColor Icon tint [Color] which used by [increaseIcon] and [decreaseIcon]
  * that defaults to [contentColor], unless specifically overridden.
+ * @param enableRangeSemantics Boolean to decide if range semantics should be enabled.
+ * Set to false to disable default stepper range semantics. Alternatively to customize semantics
+ * set this value as false and chain new semantics to the modifier.
+ * @param content Content body for the Stepper.
  */
 @Composable
-public fun Stepper(
+fun Stepper(
     value: Float,
     onValueChange: (Float) -> Unit,
     steps: Int,
@@ -86,6 +95,7 @@
     backgroundColor: Color = MaterialTheme.colors.background,
     contentColor: Color = contentColorFor(backgroundColor),
     iconColor: Color = contentColor,
+    enableRangeSemantics: Boolean = true,
     content: @Composable BoxScope.() -> Unit
 ) {
     require(steps >= 0) { "steps should be >= 0" }
@@ -98,17 +108,15 @@
     }
 
     Column(
-        modifier = modifier
-            .fillMaxSize()
-            .background(backgroundColor)
-            .rangeSemantics(
-                currentStep,
-                true,
-                onValueChange,
-                valueRange,
-                steps
-            ),
-        verticalArrangement = Arrangement.spacedBy(8.dp)
+        modifier = modifier.fillMaxSize().background(backgroundColor).then(
+                if (enableRangeSemantics) {
+                    Modifier.rangeSemantics(
+                        currentStep, true, onValueChange, valueRange, steps
+                    )
+                } else {
+                    Modifier
+                }
+            ), verticalArrangement = Arrangement.spacedBy(8.dp)
     ) {
         // Increase button.
         FullScreenButton(
@@ -120,9 +128,7 @@
             content = increaseIcon
         )
         Box(
-            modifier = Modifier
-                .fillMaxWidth()
-                .weight(StepperDefaults.ContentWeight),
+            modifier = Modifier.fillMaxWidth().weight(StepperDefaults.ContentWeight),
             contentAlignment = Alignment.Center,
         ) {
             CompositionLocalProvider(
@@ -163,6 +169,10 @@
  * then [valueProgression].last will be adjusted to the closest divisible value in the range.
  * For example, 1..13 range and a step = 5, steps will be 1(first) , 6 , 11(last)
  *
+ * If [value] is not equal to any step value, then it will be coerced to the closest step value.
+ * However, the [value] itself will not be changed and [onValueChange] in this case will
+ * not be triggered.
+ *
  * @param value Current value of the Stepper. If outside of [valueProgression] provided, value will be
  * coerced to this range.
  * @param onValueChange Lambda in which value should be updated
@@ -175,9 +185,13 @@
  * @param contentColor [Color] representing the color for [content] in the middle.
  * @param iconColor Icon tint [Color] which used by [increaseIcon] and [decreaseIcon]
  * that defaults to [contentColor], unless specifically overridden.
+ * @param enableRangeSemantics Boolean to decide if default stepper semantics should be enabled.
+ * Set to false to disable default stepper range semantics. Alternatively to customize semantics
+ * set this value as false and chain new semantics to the modifier.
+ * @param content Content body for the Stepper.
  */
 @Composable
-public fun Stepper(
+fun Stepper(
     value: Int,
     onValueChange: (Int) -> Unit,
     valueProgression: IntProgression,
@@ -187,6 +201,7 @@
     backgroundColor: Color = MaterialTheme.colors.background,
     contentColor: Color = contentColorFor(backgroundColor),
     iconColor: Color = contentColor,
+    enableRangeSemantics: Boolean = true,
     content: @Composable BoxScope.() -> Unit
 ) {
     Stepper(
@@ -200,6 +215,134 @@
         backgroundColor = backgroundColor,
         contentColor = contentColor,
         iconColor = iconColor,
+        enableRangeSemantics = enableRangeSemantics,
+        content = content
+    )
+}
+
+/**
+ * [Stepper] allows users to make a selection from a range of values.
+ * It's a full-screen control with increase button on the top, decrease button on the bottom and
+ * a slot (expected to have either [Text] or [Chip]) in the middle.
+ * Value can be increased and decreased by clicking on the increase and decrease buttons.
+ * Buttons can have custom icons - [decreaseIcon] and [increaseIcon].
+ * Step value is calculated as the difference between min and max values divided by [steps]+1.
+ * Stepper itself doesn't show the current value but can be displayed via the content slot or
+ * [PositionIndicator] if required.
+ *
+ * @sample androidx.wear.compose.material.samples.StepperSample
+ *
+ * @param value Current value of the Stepper. If outside of [valueRange] provided, value will be
+ * coerced to this range.
+ * @param onValueChange Lambda in which value should be updated
+ * @param steps Specifies the number of discrete values, excluding min and max values, evenly
+ * distributed across the whole value range. Must not be negative. If 0, stepper will have only
+ * min and max values and no steps in between
+ * @param decreaseIcon A slot for an icon which is placed on the decrease (bottom) button
+ * @param increaseIcon A slot for an icon which is placed on the increase (top) button
+ * @param modifier Modifiers for the Stepper layout
+ * @param valueRange Range of values that Stepper value can take. Passed [value] will be coerced to
+ * this range
+ * @param backgroundColor [Color] representing the background color for the stepper.
+ * @param contentColor [Color] representing the color for [content] in the middle.
+ * @param iconColor Icon tint [Color] which used by [increaseIcon] and [decreaseIcon]
+ * that defaults to [contentColor], unless specifically overridden.
+ */
+@Deprecated(
+    "This overload is provided for backwards compatibility with Compose for Wear OS 1.1. " +
+        "A newer overload is available with an additional enableDefaultSemantics parameter.",
+    level = DeprecationLevel.HIDDEN
+)
+@Composable
+public fun Stepper(
+    value: Float,
+    onValueChange: (Float) -> Unit,
+    steps: Int,
+    decreaseIcon: @Composable () -> Unit,
+    increaseIcon: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    valueRange: ClosedFloatingPointRange<Float> = 0f..(steps + 1).toFloat(),
+    backgroundColor: Color = MaterialTheme.colors.background,
+    contentColor: Color = contentColorFor(backgroundColor),
+    iconColor: Color = contentColor,
+    content: @Composable BoxScope.() -> Unit
+) = Stepper(
+    value = value,
+    onValueChange = onValueChange,
+    steps = steps,
+    decreaseIcon = decreaseIcon,
+    increaseIcon = increaseIcon,
+    modifier = modifier,
+    valueRange = valueRange,
+    backgroundColor = backgroundColor,
+    contentColor = contentColor,
+    iconColor = iconColor,
+    enableRangeSemantics = true,
+    content = content
+)
+
+/**
+ * [Stepper] allows users to make a selection from a range of values.
+ * It's a full-screen control with increase button on the top, decrease button on the bottom and
+ * a slot (expected to have either [Text] or [Chip]) in the middle.
+ * Value can be increased and decreased by clicking on the increase and decrease buttons.
+ * Buttons can have custom icons - [decreaseIcon] and [increaseIcon].
+ * Stepper itself doesn't show the current value but can be displayed via the content slot or
+ * [PositionIndicator] if required.
+ *
+ * @sample androidx.wear.compose.material.samples.StepperWithIntegerSample
+ *
+ * A number of steps is calculated as the difference between max and min values of
+ * [valueProgression] divided by [valueProgression].step - 1.
+ * For example, with a range of 100..120 and a step 5,
+ * number of steps will be (120-100)/ 5 - 1 = 3. Steps are 100(first), 105, 110, 115, 120(last)
+ *
+ * If [valueProgression] range is not equally divisible by [valueProgression].step,
+ * then [valueProgression].last will be adjusted to the closest divisible value in the range.
+ * For example, 1..13 range and a step = 5, steps will be 1(first) , 6 , 11(last)
+ *
+ * @param value Current value of the Stepper. If outside of [valueProgression] provided, value will be
+ * coerced to this range.
+ * @param onValueChange Lambda in which value should be updated
+ * @param valueProgression Progression of values that Stepper value can take. Consists of
+ * rangeStart, rangeEnd and step. Range will be equally divided by step size
+ * @param decreaseIcon A slot for an icon which is placed on the decrease (bottom) button
+ * @param increaseIcon A slot for an icon which is placed on the increase (top) button
+ * @param modifier Modifiers for the Stepper layout
+ * @param backgroundColor [Color] representing the background color for the stepper.
+ * @param contentColor [Color] representing the color for [content] in the middle.
+ * @param iconColor Icon tint [Color] which used by [increaseIcon] and [decreaseIcon]
+ * that defaults to [contentColor], unless specifically overridden.
+ */
+@Deprecated(
+    "This overload is provided for backwards compatibility with Compose for Wear OS 1.1. " +
+        "A newer overload is available with an additional enableDefaultSemantics parameter.",
+    level = DeprecationLevel.HIDDEN
+)
+@Composable
+fun Stepper(
+    value: Int,
+    onValueChange: (Int) -> Unit,
+    valueProgression: IntProgression,
+    decreaseIcon: @Composable () -> Unit,
+    increaseIcon: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    backgroundColor: Color = MaterialTheme.colors.background,
+    contentColor: Color = contentColorFor(backgroundColor),
+    iconColor: Color = contentColor,
+    content: @Composable BoxScope.() -> Unit
+) {
+    Stepper(
+        value = value,
+        onValueChange = onValueChange,
+        valueProgression = valueProgression,
+        decreaseIcon = decreaseIcon,
+        increaseIcon = increaseIcon,
+        modifier = modifier,
+        backgroundColor = backgroundColor,
+        contentColor = contentColor,
+        iconColor = iconColor,
+        enableRangeSemantics = true,
         content = content
     )
 }
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
index 84af177e..ed29bf9 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
@@ -40,6 +40,7 @@
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
@@ -159,18 +160,13 @@
                 Modifiers(
                     contentForeground = Modifier
                         .fillMaxSize()
-                        .graphicsLayer(
-                            translationX = translationX,
-                            scaleX = scale,
-                            scaleY = scale,
-                        )
-                        .then(
-                            if (isRound && translationX > 0) {
-                                Modifier.clip(CircleShape)
-                            } else {
-                                Modifier
-                            }
-                        )
+                        .graphicsLayer {
+                            this.translationX = translationX
+                            scaleX = scale
+                            scaleY = scale
+                            clip = isRound && translationX > 0
+                            shape = if (isRound) CircleShape else RectangleShape
+                        }
                         .background(backgroundScrimColor),
                     scrimForeground =
                     Modifier
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Vignette.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Vignette.kt
index f33c89e..245a5d0 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Vignette.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Vignette.kt
@@ -23,6 +23,7 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.ContentScale
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 
 /**
  * Possible combinations for vignette state.
@@ -103,7 +104,9 @@
                 ),
                 contentScale = ContentScale.FillWidth,
                 contentDescription = null,
-                modifier = Modifier.align(Alignment.TopCenter).fillMaxWidth(),
+                modifier = Modifier
+                    .align(Alignment.TopCenter)
+                    .fillMaxWidth(),
             )
         }
         if (vignettePosition.drawBottom()) {
@@ -114,7 +117,9 @@
                 ),
                 contentScale = ContentScale.FillWidth,
                 contentDescription = null,
-                modifier = Modifier.align(Alignment.BottomCenter).fillMaxWidth(),
+                modifier = Modifier
+                    .align(Alignment.BottomCenter)
+                    .fillMaxWidth(),
             )
         }
     }
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/dialog/Dialog.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/dialog/Dialog.kt
index fac57f8..4f35115 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/dialog/Dialog.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/dialog/Dialog.kt
@@ -20,38 +20,38 @@
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.width
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
 import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
+import androidx.wear.compose.foundation.lazy.ScalingLazyListState
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
 import androidx.wear.compose.material.Button
 import androidx.wear.compose.material.Chip
-import androidx.wear.compose.material.ToggleChip
-import androidx.wear.compose.material.contentColorFor
 import androidx.wear.compose.material.Icon
-import androidx.wear.compose.material.isRoundDevice
 import androidx.wear.compose.material.LocalContentColor
 import androidx.wear.compose.material.LocalTextStyle
 import androidx.wear.compose.material.MaterialTheme
 import androidx.wear.compose.material.PositionIndicator
 import androidx.wear.compose.material.Scaffold
-import androidx.wear.compose.material.ScalingLazyColumn
-import androidx.wear.compose.material.ScalingLazyListScope
-import androidx.wear.compose.material.ScalingLazyListState
-import androidx.wear.compose.material.rememberScalingLazyListState
+import androidx.wear.compose.material.ToggleChip
+import androidx.wear.compose.material.contentColorFor
+import androidx.wear.compose.material.isRoundDevice
 import kotlinx.coroutines.delay
 
 /**
@@ -142,6 +142,138 @@
 
 /**
  * [Alert] lays out the content for an opinionated, alert screen.
+ * This overload offers 5 slots for title, negative button, positive button, optional icon and
+ * optional content. The buttons are shown side-by-side below the icon, text and content.
+ * [Alert] is scrollable by default if the content is taller than the viewport.
+ *
+ * [Alert] can be used as a destination in a navigation graph
+ * e.g. using SwipeDismissableNavHost. However, for a conventional fullscreen dialog,
+ * displayed on top of other content, use [Dialog].
+ *
+ * Example of an [Alert] with an icon, title, body text and buttons:
+ * @sample androidx.wear.compose.material.samples.AlertWithButtons
+ *
+ * @param title A slot for displaying the title of the dialog,
+ * expected to be one or two lines of text.
+ * @param negativeButton A slot for a [Button] indicating negative sentiment (e.g. No).
+ * Clicking the button must remove the dialog from the composition hierarchy.
+ * @param positiveButton A slot for a [Button] indicating positive sentiment (e.g. Yes).
+ * Clicking the button must remove the dialog from the composition hierarchy.
+ * @param modifier Modifier to be applied to the dialog content.
+ * @param icon Optional slot for an icon to be shown at the top of the dialog.
+ * @param scrollState The scroll state for the dialog so that the scroll position can be displayed
+ * e.g. by the [PositionIndicator] passed to [Scaffold].
+ * @param backgroundColor [Color] representing the background color for the dialog.
+ * @param contentColor [Color] representing the color for [content].
+ * @param titleColor [Color] representing the color for [title].
+ * @param iconColor Icon [Color] that defaults to [contentColor],
+ * unless specifically overridden.
+ * @param verticalArrangement The vertical arrangement of the dialog's children. This allows us
+ * to add spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
+ * @param contentPadding The padding to apply around the whole of the dialog's contents.
+ * @param content A slot for additional content, expected to be 2-3 lines of text.
+ */
+@Suppress("DEPRECATION")
+@Deprecated(
+    "This overload is provided for backwards compatibility with Compose for Wear OS 1.1." +
+        "A newer overload is available which uses ScalingLazyListState from " +
+        "wear.compose.foundation.lazy package", level = DeprecationLevel.HIDDEN
+)
+@Composable
+public fun Alert(
+    title: @Composable ColumnScope.() -> Unit,
+    negativeButton: @Composable () -> Unit,
+    positiveButton: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    icon: @Composable (ColumnScope.() -> Unit)? = null,
+    scrollState: androidx.wear.compose.material.ScalingLazyListState =
+        androidx.wear.compose.material.rememberScalingLazyListState(),
+    backgroundColor: Color = MaterialTheme.colors.background,
+    contentColor: Color = contentColorFor(backgroundColor),
+    titleColor: Color = contentColor,
+    iconColor: Color = contentColor,
+    verticalArrangement: Arrangement.Vertical = DialogDefaults.AlertVerticalArrangement,
+    contentPadding: PaddingValues = DialogDefaults.ContentPadding,
+    content: @Composable (ColumnScope.() -> Unit)? = null
+) {
+    AlertWithMaterialSlc(
+        title = title,
+        negativeButton = negativeButton,
+        positiveButton = positiveButton,
+        modifier = modifier,
+        icon = icon,
+        scrollState = scrollState,
+        backgroundColor = backgroundColor,
+        contentColor = contentColor,
+        titleColor = titleColor,
+        iconColor = iconColor,
+        verticalArrangement = verticalArrangement,
+        contentPadding = contentPadding,
+        content = content
+    )
+}
+
+/**
+ * @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ */
+@Suppress("DEPRECATION")
+@Deprecated("Used only for testing")
+@Composable
+internal fun AlertWithMaterialSlc(
+    title: @Composable ColumnScope.() -> Unit,
+    negativeButton: @Composable () -> Unit,
+    positiveButton: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    icon: @Composable (ColumnScope.() -> Unit)? = null,
+    scrollState: androidx.wear.compose.material.ScalingLazyListState =
+        androidx.wear.compose.material.rememberScalingLazyListState(),
+    backgroundColor: Color = MaterialTheme.colors.background,
+    contentColor: Color = contentColorFor(backgroundColor),
+    titleColor: Color = contentColor,
+    iconColor: Color = contentColor,
+    verticalArrangement: Arrangement.Vertical = DialogDefaults.AlertVerticalArrangement,
+    contentPadding: PaddingValues = DialogDefaults.ContentPadding,
+    content: @Composable (ColumnScope.() -> Unit)? = null
+) {
+    MaterialDialogImpl(
+        modifier = modifier,
+        scrollState = scrollState,
+        verticalArrangement = verticalArrangement,
+        contentPadding = contentPadding,
+        backgroundColor = backgroundColor,
+    ) {
+        if (icon != null) {
+            item {
+                DialogIconHeader(iconColor, content = icon)
+            }
+        }
+
+        item {
+            DialogTitle(titleColor, padding = DialogDefaults.TitlePadding, title)
+        }
+
+        if (content != null) {
+            item {
+                DialogBody(contentColor, content)
+            }
+        }
+
+        // Buttons
+        item {
+            Row(
+                verticalAlignment = Alignment.CenterVertically,
+            ) {
+                negativeButton()
+                Spacer(modifier = Modifier.width(DialogDefaults.ButtonSpacing))
+                positiveButton()
+            }
+        }
+    }
+}
+
+/**
+ * [Alert] lays out the content for an opinionated, alert screen.
  * This overload offers 4 slots for title, optional icon, optional message text and
  * a content slot expected to be one or more vertically stacked [Chip]s or [ToggleChip]s.
  * [Alert] is scrollable by default if the content is taller than the viewport.
@@ -213,6 +345,122 @@
 }
 
 /**
+ * [Alert] lays out the content for an opinionated, alert screen.
+ * This overload offers 4 slots for title, optional icon, optional message text and
+ * a content slot expected to be one or more vertically stacked [Chip]s or [ToggleChip]s.
+ * [Alert] is scrollable by default if the content is taller than the viewport.
+ *
+ * [Alert] can be used as a destination in a navigation graph
+ * e.g. using SwipeDismissableNavHost. However, for a conventional fullscreen dialog,
+ * displayed on top of other content, use [Dialog].
+ *
+ * Example of an [Alert] with an icon, title, message text and chips:
+ * @sample androidx.wear.compose.material.samples.AlertWithChips
+ *
+ * @param title A slot for displaying the title of the dialog,
+ * expected to be one or two lines of text.
+ * @param modifier Modifier to be applied to the dialog.
+ * @param icon Optional slot for an icon to be shown at the top of the dialog.
+ * @param message Optional slot for additional message content, expected to be 2-3 lines of text.
+ * @param scrollState The scroll state for the dialog so that the scroll position can be displayed
+ * e.g. by the [PositionIndicator] passed to [Scaffold].
+ * @param backgroundColor [Color] representing the background color for the dialog.
+ * @param titleColor [Color] representing the color for [title].
+ * @param messageColor [Color] representing the color for [message].
+ * @param iconColor [Color] representing the color for [icon].
+ * @param verticalArrangement The vertical arrangement of the dialog's children. This allows us
+ * to add spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
+ * @param contentPadding The padding to apply around the whole of the dialog's contents.
+ * @param content A slot for one or more spaced [Chip]s, stacked vertically.
+ */
+@Suppress("DEPRECATION")
+@Deprecated(
+    "This overload is provided for backwards compatibility with Compose for Wear OS 1.1." +
+        "A newer overload is available which uses ScalingLazyListState and ScalingLazyListScope " +
+        "from wear.compose.foundation.lazy package", level = DeprecationLevel.HIDDEN
+)
+@Composable
+public fun Alert(
+    title: @Composable ColumnScope.() -> Unit,
+    modifier: Modifier = Modifier,
+    icon: @Composable (ColumnScope.() -> Unit)? = null,
+    message: @Composable (ColumnScope.() -> Unit)? = null,
+    scrollState: androidx.wear.compose.material.ScalingLazyListState =
+        androidx.wear.compose.material.rememberScalingLazyListState(),
+    backgroundColor: Color = MaterialTheme.colors.background,
+    titleColor: Color = contentColorFor(backgroundColor),
+    messageColor: Color = contentColorFor(backgroundColor),
+    iconColor: Color = contentColorFor(backgroundColor),
+    verticalArrangement: Arrangement.Vertical = DialogDefaults.AlertVerticalArrangement,
+    contentPadding: PaddingValues = DialogDefaults.ContentPadding,
+    content: androidx.wear.compose.material.ScalingLazyListScope.() -> Unit
+) {
+    AlertWithMaterialSlc(
+        title = title,
+        modifier = modifier,
+        icon = icon,
+        message = message,
+        scrollState = scrollState,
+        backgroundColor = backgroundColor,
+        titleColor = titleColor,
+        messageColor = messageColor,
+        iconColor = iconColor,
+        verticalArrangement = verticalArrangement,
+        contentPadding = contentPadding,
+        content = content
+    )
+}
+
+/**
+ * @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ */
+@Suppress("DEPRECATION")
+@Deprecated("Used only for testing")
+@Composable
+internal fun AlertWithMaterialSlc(
+    title: @Composable ColumnScope.() -> Unit,
+    modifier: Modifier = Modifier,
+    icon: @Composable (ColumnScope.() -> Unit)? = null,
+    message: @Composable (ColumnScope.() -> Unit)? = null,
+    scrollState: androidx.wear.compose.material.ScalingLazyListState =
+        androidx.wear.compose.material.rememberScalingLazyListState(),
+    backgroundColor: Color = MaterialTheme.colors.background,
+    titleColor: Color = contentColorFor(backgroundColor),
+    messageColor: Color = contentColorFor(backgroundColor),
+    iconColor: Color = contentColorFor(backgroundColor),
+    verticalArrangement: Arrangement.Vertical = DialogDefaults.AlertVerticalArrangement,
+    contentPadding: PaddingValues = DialogDefaults.ContentPadding,
+    content: androidx.wear.compose.material.ScalingLazyListScope.() -> Unit
+) {
+    MaterialDialogImpl(
+        modifier = modifier,
+        scrollState = scrollState,
+        verticalArrangement = verticalArrangement,
+        contentPadding = contentPadding,
+        backgroundColor = backgroundColor,
+    ) {
+        if (icon != null) {
+            item {
+                DialogIconHeader(iconColor, content = icon)
+            }
+        }
+
+        item {
+            DialogTitle(titleColor, padding = DialogDefaults.TitlePadding, content = title)
+        }
+
+        if (message != null) {
+            item {
+                DialogBody(messageColor, message)
+            }
+        }
+
+        content()
+    }
+}
+
+/**
  * [Confirmation] lays out the content for an opinionated confirmation screen that
  * displays a message to the user for [durationMillis]. It has a slot for an icon or image
  * (which could be animated).
@@ -289,6 +537,124 @@
 }
 
 /**
+ * [Confirmation] lays out the content for an opinionated confirmation screen that
+ * displays a message to the user for [durationMillis]. It has a slot for an icon or image
+ * (which could be animated).
+ *
+ * [Confirmation] can be used as a destination in a navigation graph
+ * e.g. using SwipeDismissableNavHost. However, for a conventional fullscreen dialog,
+ * displayed on top of other content, use [Dialog].
+ *
+ * Example of a [Confirmation] with animation:
+ * @sample androidx.wear.compose.material.samples.ConfirmationWithAnimation
+ *
+ * @param onTimeout Event invoked when the dialog has been shown for [durationMillis].
+ * @param modifier Modifier to be applied to the dialog.
+ * @param icon An optional slot for displaying an icon or image.
+ * @param scrollState The scroll state for the dialog so that the scroll position can be displayed
+ * e.g. by the [PositionIndicator] passed to [Scaffold].
+ * @param durationMillis The number of milliseconds for which the dialog is displayed,
+ * must be positive. Suggested values are [DialogDefaults.ShortDurationMillis],
+ * [DialogDefaults.LongDurationMillis] or [DialogDefaults.IndefiniteDurationMillis].
+ * @param backgroundColor [Color] representing the background color for this dialog.
+ * @param contentColor [Color] representing the color for [content].
+ * @param iconColor Icon [Color] that defaults to the [contentColor],
+ * unless specifically overridden.
+ * @param verticalArrangement The vertical arrangement of the dialog's children. This allows us
+ * to add spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
+ * @param contentPadding The padding to apply around the whole of the dialog's contents.
+ * @param content A slot for the dialog title, expected to be one line of text.
+ */
+@Suppress("DEPRECATION")
+@Deprecated(
+        "This overload is provided for backwards compatibility with Compose for Wear OS 1.1." +
+        "A newer overload is available which uses ScalingLazyListState from " +
+        "wear.compose.foundation.lazy package", level = DeprecationLevel.HIDDEN
+)
+@Composable
+public fun Confirmation(
+    onTimeout: () -> Unit,
+    modifier: Modifier = Modifier,
+    icon: @Composable (ColumnScope.() -> Unit)? = null,
+    scrollState: androidx.wear.compose.material.ScalingLazyListState =
+        androidx.wear.compose.material.rememberScalingLazyListState(),
+    durationMillis: Long = DialogDefaults.ShortDurationMillis,
+    backgroundColor: Color = MaterialTheme.colors.background,
+    contentColor: Color = contentColorFor(backgroundColor),
+    iconColor: Color = contentColor,
+    verticalArrangement: Arrangement.Vertical = DialogDefaults.ConfirmationVerticalArrangement,
+    contentPadding: PaddingValues = DialogDefaults.ContentPadding,
+    content: @Composable ColumnScope.() -> Unit
+) {
+    ConfirmationWithMaterialSlc(
+        onTimeout = onTimeout,
+        modifier = modifier,
+        icon = icon,
+        scrollState = scrollState,
+        durationMillis = durationMillis,
+        backgroundColor = backgroundColor,
+        contentColor = contentColor,
+        iconColor = iconColor,
+        verticalArrangement = verticalArrangement,
+        contentPadding = contentPadding,
+        content = content
+    )
+}
+
+/**
+ * @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ */
+@Suppress("DEPRECATION")
+@Deprecated("Used only for testing")
+@Composable
+internal fun ConfirmationWithMaterialSlc(
+    onTimeout: () -> Unit,
+    modifier: Modifier = Modifier,
+    icon: @Composable (ColumnScope.() -> Unit)? = null,
+    scrollState: androidx.wear.compose.material.ScalingLazyListState =
+        androidx.wear.compose.material.rememberScalingLazyListState(),
+    durationMillis: Long = DialogDefaults.ShortDurationMillis,
+    backgroundColor: Color = MaterialTheme.colors.background,
+    contentColor: Color = contentColorFor(backgroundColor),
+    iconColor: Color = contentColor,
+    verticalArrangement: Arrangement.Vertical = DialogDefaults.ConfirmationVerticalArrangement,
+    contentPadding: PaddingValues = DialogDefaults.ContentPadding,
+    content: @Composable ColumnScope.() -> Unit
+) {
+    require(durationMillis > 0) { "Duration must be a positive integer" }
+
+    // Always refer to the latest inputs with which ConfirmationDialog was recomposed.
+    val currentOnTimeout by rememberUpdatedState(onTimeout)
+
+    LaunchedEffect(durationMillis) {
+        delay(durationMillis)
+        currentOnTimeout()
+    }
+    MaterialDialogImpl(
+        modifier = modifier,
+        scrollState = scrollState,
+        verticalArrangement = verticalArrangement,
+        contentPadding = contentPadding,
+        backgroundColor = backgroundColor,
+    ) {
+        if (icon != null) {
+            item {
+                DialogIconHeader(iconColor, content = icon)
+            }
+        }
+
+        item {
+            DialogTitle(
+                titleColor = contentColor,
+                padding = DialogDefaults.TitleBottomPadding,
+                content = content
+            )
+        }
+    }
+}
+
+/**
  * Contains the default values used by [Alert] and [Confirmation].
  */
 public object DialogDefaults {
@@ -394,6 +760,37 @@
 }
 
 /**
+ * Common Wear Material dialog implementation that offers a single content slot,
+ * fills the screen and is scrollable by default if the content is taller than the viewport.
+ */
+@Suppress("DEPRECATION")
+@Deprecated(
+    "Use [DialogImpl] targeting " +
+        "androidx.wear.compose.foundation.lazy ScalingLazyColumn instead"
+)
+@Composable
+private fun MaterialDialogImpl(
+    modifier: Modifier = Modifier,
+    scrollState: androidx.wear.compose.material.ScalingLazyListState,
+    verticalArrangement: Arrangement.Vertical,
+    backgroundColor: Color,
+    contentPadding: PaddingValues,
+    content: androidx.wear.compose.material.ScalingLazyListScope.() -> Unit
+) {
+    androidx.wear.compose.material.ScalingLazyColumn(
+        state = scrollState,
+        autoCentering = null,
+        horizontalAlignment = Alignment.CenterHorizontally,
+        verticalArrangement = verticalArrangement,
+        contentPadding = contentPadding,
+        modifier = modifier
+            .fillMaxSize()
+            .background(backgroundColor),
+        content = content
+    )
+}
+
+/**
  * [DialogIconHeader] displays an icon at the top of the dialog
  * followed by the recommended spacing.
  *
@@ -411,7 +808,11 @@
             horizontalAlignment = Alignment.CenterHorizontally
         ) {
             content()
-            Spacer(Modifier.fillMaxWidth().height(DialogDefaults.IconSpacing))
+            Spacer(
+                Modifier
+                    .fillMaxWidth()
+                    .height(DialogDefaults.IconSpacing)
+            )
         }
     }
 }
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index b85810f..4d48f3b 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -29,14 +29,17 @@
     api(project(":compose:runtime:runtime"))
     api("androidx.navigation:navigation-runtime:2.4.0")
     api(project(":wear:compose:compose-material"))
+    api(project(":activity:activity-compose"))
     api(project(":lifecycle:lifecycle-viewmodel-compose"))
 
     implementation(libs.kotlinStdlib)
+    implementation(project(":navigation:navigation-common"))
     implementation("androidx.navigation:navigation-compose:2.4.0")
     implementation("androidx.profileinstaller:profileinstaller:1.2.0")
 
     androidTestImplementation(project(":compose:test-utils"))
     androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+    androidTestImplementation(project(":navigation:navigation-common"))
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(project(":wear:compose:compose-material"))
     androidTestImplementation(project(":wear:compose:compose-navigation-samples"))
diff --git a/wear/compose/compose-navigation/samples/src/main/java/androidx/wear/compose/navigation/samples/SwipeDismissableNavHostSample.kt b/wear/compose/compose-navigation/samples/src/main/java/androidx/wear/compose/navigation/samples/SwipeDismissableNavHostSample.kt
index 32fae6d..5e7b3f48 100644
--- a/wear/compose/compose-navigation/samples/src/main/java/androidx/wear/compose/navigation/samples/SwipeDismissableNavHostSample.kt
+++ b/wear/compose/compose-navigation/samples/src/main/java/androidx/wear/compose/navigation/samples/SwipeDismissableNavHostSample.kt
@@ -29,10 +29,10 @@
 import androidx.compose.ui.unit.dp
 import androidx.navigation.NavType
 import androidx.navigation.navArgument
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material.Button
 import androidx.wear.compose.material.CompactChip
 import androidx.wear.compose.material.ListHeader
-import androidx.wear.compose.material.ScalingLazyColumn
 import androidx.wear.compose.material.Text
 import androidx.wear.compose.navigation.SwipeDismissableNavHost
 import androidx.wear.compose.navigation.composable
diff --git a/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt b/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt
index e35c959..4ad9e64 100644
--- a/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt
+++ b/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt
@@ -45,6 +45,7 @@
 import androidx.compose.ui.test.swipeRight
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.navigation.NavBackStackEntry
 import androidx.navigation.NavHostController
@@ -99,12 +100,11 @@
 
     @Test
     fun navigates_back_to_previous_level_with_back_button() {
-        val lifecycleOwner = TestLifecycleOwner()
         val onBackPressedDispatcher = OnBackPressedDispatcher()
-        val dispatcherOwner = object : OnBackPressedDispatcherOwner {
-            override fun getLifecycle() = lifecycleOwner.lifecycle
-            override fun getOnBackPressedDispatcher() = onBackPressedDispatcher
-        }
+        val dispatcherOwner =
+            object : OnBackPressedDispatcherOwner, LifecycleOwner by TestLifecycleOwner() {
+                override val onBackPressedDispatcher = onBackPressedDispatcher
+            }
         lateinit var navController: NavHostController
 
         rule.setContentWithTheme {
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index f826f7d..2d08931 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -27,7 +27,7 @@
         minSdk 25
         targetSdk 30
         versionCode 12
-        versionName "1.11"
+        versionName "1.12"
         // Change the APK name to match the *testapp regex we use to pick up APKs for testing as
         // part of CI.
         archivesBaseName = "wear-compose-demos-testapp"
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ButtonDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ButtonDemo.kt
index 891a178..98fb88f 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ButtonDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ButtonDemo.kt
@@ -35,6 +35,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
 import androidx.wear.compose.material.Button
 import androidx.wear.compose.material.ButtonDefaults
 import androidx.wear.compose.material.CompactButton
@@ -44,7 +45,6 @@
 import androidx.wear.compose.material.OutlinedCompactButton
 import androidx.wear.compose.material.Text
 import androidx.wear.compose.material.ToggleButton
-import androidx.wear.compose.material.rememberScalingLazyListState
 
 @Composable
 fun ButtonSizes() {
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ChipDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ChipDemo.kt
index 0276717..8835d23 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ChipDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ChipDemo.kt
@@ -48,6 +48,7 @@
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material.Chip
 import androidx.wear.compose.material.ChipColors
 import androidx.wear.compose.material.ChipDefaults
@@ -58,7 +59,6 @@
 import androidx.wear.compose.material.MaterialTheme
 import androidx.wear.compose.material.OutlinedChip
 import androidx.wear.compose.material.OutlinedCompactChip
-import androidx.wear.compose.material.ScalingLazyColumn
 import androidx.wear.compose.material.Text
 import androidx.wear.compose.material.ToggleButton
 import androidx.wear.compose.material.ToggleChip
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
index 03a2344..c6be1c3 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
@@ -43,23 +43,23 @@
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
-import androidx.wear.compose.material.AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults
+import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
+import androidx.wear.compose.foundation.lazy.ScalingLazyListState
+import androidx.wear.compose.foundation.lazy.ScalingParams
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
 import androidx.wear.compose.material.Chip
 import androidx.wear.compose.material.ChipDefaults
 import androidx.wear.compose.material.ListHeader
 import androidx.wear.compose.material.LocalTextStyle
 import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.ScalingLazyColumn
-import androidx.wear.compose.material.ScalingLazyColumnDefaults
-import androidx.wear.compose.material.ScalingLazyListScope
-import androidx.wear.compose.material.ScalingLazyListState
-import androidx.wear.compose.material.ScalingParams
 import androidx.wear.compose.material.SwipeToDismissBox
 import androidx.wear.compose.material.SwipeToDismissBoxState
 import androidx.wear.compose.material.SwipeToDismissKeys
 import androidx.wear.compose.material.SwipeToDismissValue
 import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.rememberScalingLazyListState
 import androidx.wear.compose.material.rememberSwipeToDismissBoxState
 import kotlinx.coroutines.channels.BufferOverflow
 import kotlinx.coroutines.channels.Channel
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DialogDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DialogDemo.kt
index d878605..fd110fe 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DialogDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DialogDemo.kt
@@ -35,6 +35,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
 import androidx.wear.compose.material.Button
 import androidx.wear.compose.material.ButtonDefaults
 import androidx.wear.compose.material.Chip
@@ -44,7 +45,6 @@
 import androidx.wear.compose.material.dialog.Alert
 import androidx.wear.compose.material.dialog.Confirmation
 import androidx.wear.compose.material.dialog.Dialog
-import androidx.wear.compose.material.rememberScalingLazyListState
 
 @Composable
 fun DialogPowerOff() {
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
index ae0f7a3..1c592b2 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
@@ -26,7 +26,11 @@
 import androidx.wear.compose.foundation.samples.CurvedWeight
 import androidx.wear.compose.foundation.samples.HierarchicalFocusCoordinatorSample
 import androidx.wear.compose.foundation.samples.OversizeComposable
+import androidx.wear.compose.foundation.samples.ScalingLazyColumnEdgeAnchoredAndAnimatedScrollTo
 import androidx.wear.compose.foundation.samples.SimpleCurvedWorld
+import androidx.wear.compose.foundation.samples.SimpleScalingLazyColumn
+import androidx.wear.compose.foundation.samples.SimpleScalingLazyColumnWithContentPadding
+import androidx.wear.compose.foundation.samples.SimpleScalingLazyColumnWithSnap
 
 val WearFoundationDemos = DemoCategory(
     "Foundation",
@@ -52,5 +56,35 @@
         ComposableDemo("Scrollable Row") { ScrollableRowDemo() },
         DemoCategory("Rotary Input", RotaryInputDemos),
         ComposableDemo("Focus Sample") { HierarchicalFocusCoordinatorSample() },
+        DemoCategory("Scaling Lazy Column", listOf(
+                ComposableDemo(
+                    "Defaults",
+                    "Basic ScalingLazyColumn using default values"
+                ) {
+                    SimpleScalingLazyColumn()
+                },
+                ComposableDemo(
+                    "With Content Padding",
+                    "Basic ScalingLazyColumn with autoCentering disabled and explicit " +
+                        "content padding of top = 20.dp, bottom = 20.dp"
+                ) {
+                    SimpleScalingLazyColumnWithContentPadding()
+                },
+                ComposableDemo(
+                    "With Snap",
+                    "Basic ScalingLazyColumn, center aligned with snap enabled"
+                ) {
+                    SimpleScalingLazyColumnWithSnap()
+                },
+                ComposableDemo(
+                    "Edge Anchor",
+                    "A ScalingLazyColumn with Edge (rather than center) item anchoring. " +
+                        "If you click on an item there will be an animated scroll of the " +
+                        "items edge to the center"
+                ) {
+                    ScalingLazyColumnEdgeAnchoredAndAnimatedScrollTo()
+                },
+            )
+        )
     ),
 )
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
index c12dbc7..d2edf1d 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
@@ -69,7 +69,9 @@
 import androidx.wear.compose.material.samples.SplitToggleChipWithCheckbox
 import androidx.wear.compose.material.samples.StatefulSwipeToDismissBox
 import androidx.wear.compose.material.samples.StepperSample
+import androidx.wear.compose.material.samples.StepperWithCustomSemanticsSample
 import androidx.wear.compose.material.samples.StepperWithIntegerSample
+import androidx.wear.compose.material.samples.StepperWithoutRangeSemanticsSample
 import androidx.wear.compose.material.samples.TextPlaceholder
 import androidx.wear.compose.material.samples.TimeTextAnimation
 import androidx.wear.compose.material.samples.TimeTextWithFullDateAndTimeFormat
@@ -182,7 +184,28 @@
                                 params.navigateBack()
                             },
                             date = datePickerDate
-
+                        )
+                    },
+                    ComposableDemo("From Date Picker") { params ->
+                        var datePickerDate by remember { mutableStateOf(LocalDate.now()) }
+                        DatePicker(
+                            onDateConfirm = {
+                                datePickerDate = it
+                                params.navigateBack()
+                            },
+                            date = datePickerDate,
+                            fromDate = datePickerDate
+                        )
+                    },
+                    ComposableDemo("To Date Picker") { params ->
+                        var datePickerDate by remember { mutableStateOf(LocalDate.now()) }
+                        DatePicker(
+                            onDateConfirm = {
+                                datePickerDate = it
+                                params.navigateBack()
+                            },
+                            date = datePickerDate,
+                            toDate = datePickerDate
                         )
                     },
                     ComposableDemo("Simple Picker") { SimplePicker() },
@@ -241,6 +264,12 @@
                         ComposableDemo("Integer Stepper") {
                             Centralize { StepperWithIntegerSample() }
                         },
+                        ComposableDemo("Stepper without RangeSemantics") {
+                            Centralize { StepperWithoutRangeSemanticsSample() }
+                        },
+                        ComposableDemo("Stepper with customSemantics") {
+                            Centralize { StepperWithCustomSemanticsSample() }
+                        }
                     )
                 ),
                 DemoCategory(
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
index 9380d7b..11cadf3 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
@@ -582,6 +582,8 @@
  * @param onDateConfirm the button event handler.
  * @param modifier the modifiers for the `Box` containing the UI elements.
  * @param date the initial value to seed the picker with.
+ * @param fromDate the minimum date to be selected in picker
+ * @param toDate the maximum date to be selected in picker
  */
 @SuppressLint("ClassVerificationFailure")
 @RequiresApi(Build.VERSION_CODES.O)
@@ -589,8 +591,24 @@
 public fun DatePicker(
     onDateConfirm: (LocalDate) -> Unit,
     modifier: Modifier = Modifier,
-    date: LocalDate = LocalDate.now()
+    date: LocalDate = LocalDate.now(),
+    fromDate: LocalDate? = null,
+    toDate: LocalDate? = null
 ) {
+    if (fromDate != null && toDate != null) {
+        verifyDates(date, fromDate, toDate)
+    }
+
+    val datePickerState = remember(date) {
+        if (fromDate != null && toDate == null) {
+            DatePickerState(date = date, fromDate = fromDate, toDate = fromDate.plusYears(3000))
+        } else if (fromDate == null && toDate != null) {
+            DatePickerState(date = date, fromDate = toDate.minusYears(3000), toDate = toDate)
+        } else {
+            DatePickerState(date, fromDate, toDate)
+        }
+    }
+
     // Omit scaling according to Settings > Display > Font size for this screen
     val typography = MaterialTheme.typography.copy(
         display2 = MaterialTheme.typography.display2.copy(
@@ -600,30 +618,6 @@
     val talkbackEnabled by rememberTalkBackState()
 
     MaterialTheme(typography = typography) {
-        val yearState = rememberPickerState(
-            initialNumberOfOptions = 3000,
-            initiallySelectedOption = date.year - 1
-        )
-        val monthState = rememberPickerState(
-            initialNumberOfOptions = 12,
-            initiallySelectedOption = date.monthValue - 1
-        )
-        val initiallySelectedMonthDays = date.with(TemporalAdjusters.lastDayOfMonth()).dayOfMonth
-        val dayState = rememberPickerState(
-            initialNumberOfOptions = initiallySelectedMonthDays,
-            initiallySelectedOption = date.dayOfMonth - 1
-        )
-        val maxDayInMonth by remember {
-            derivedStateOf {
-                val firstDayOfMonth =
-                    LocalDate.of(
-                        yearState.selectedOption + 1,
-                        monthState.selectedOption + 1,
-                        1
-                    )
-                firstDayOfMonth.with(TemporalAdjusters.lastDayOfMonth()).dayOfMonth
-            }
-        }
         var focusedElement by remember {
             mutableStateOf(
                 if (talkbackEnabled)
@@ -635,35 +629,41 @@
         val focusRequesterYear = remember { FocusRequester() }
         val focusRequesterConfirmButton = remember { FocusRequester() }
 
-        LaunchedEffect(maxDayInMonth) {
-            if (maxDayInMonth != dayState.numberOfOptions) {
-                dayState.numberOfOptions = maxDayInMonth
+        LaunchedEffect(
+            datePickerState.yearState.selectedOption,
+            datePickerState.monthState.selectedOption
+        ) {
+            if (datePickerState.numOfMonths != datePickerState.monthState.numberOfOptions) {
+                datePickerState.monthState.numberOfOptions = datePickerState.numOfMonths
+            }
+            if (datePickerState.numOfDays != datePickerState.dayState.numberOfOptions) {
+                datePickerState.dayState.numberOfOptions = datePickerState.numOfDays
             }
         }
-        val monthNames = remember {
-            val monthFormatter = DateTimeFormatter.ofPattern("MMM")
-            val months = 1..12
-            months.map {
-                // Translate month index into 3-character month string e.g. Jan.
-                LocalDate.of(2022, it, 1).format(monthFormatter)
-            }
-        }
-        val yearContentDescription by remember {
+        val shortMonthNames = remember { getMonthNames("MMM") }
+        val fullMonthNames = remember { getMonthNames("MMMM") }
+        val yearContentDescription by remember(focusedElement, datePickerState.currentYear()) {
             derivedStateOf {
-            createDescriptionDatePicker(focusedElement, yearState.selectedOption + 1,
-                "${yearState.selectedOption + 1}")
-        } }
-        val monthContentDescription by remember {
+                createDescriptionDatePicker(
+                    focusedElement,
+                    datePickerState.currentYear(),
+                    "${datePickerState.currentYear()}"
+                )
+            } }
+        val monthContentDescription by remember(focusedElement, datePickerState.currentMonth()) {
             derivedStateOf {
-            createDescriptionDatePicker(
-                focusedElement, monthState.selectedOption, monthNames[monthState.selectedOption]
-            )
-        } }
-        val dayContentDescription by remember {
+                createDescriptionDatePicker(
+                    focusedElement,
+                    datePickerState.currentMonth(),
+                    fullMonthNames[(datePickerState.currentMonth() - 1) % 12]
+                )
+            } }
+        val dayContentDescription by remember(focusedElement, datePickerState.currentDay()) {
             derivedStateOf {
-                createDescriptionDatePicker(focusedElement, dayState.selectedOption + 1,
-                    "${dayState.selectedOption + 1}")
-        } }
+                createDescriptionDatePicker(
+                    focusedElement, datePickerState.currentDay(), "${datePickerState.currentDay()}"
+                )
+            } }
 
         BoxWithConstraints(
             modifier = modifier
@@ -671,11 +671,12 @@
                 .then(
                     if (talkbackEnabled) {
                         when (focusedElement) {
-                            FocusableElementDatePicker.DAY -> Modifier.scrollablePicker(dayState)
+                            FocusableElementDatePicker.DAY ->
+                                Modifier.scrollablePicker(datePickerState.dayState)
                             FocusableElementDatePicker.MONTH ->
-                                Modifier.scrollablePicker(monthState)
-
-                            FocusableElementDatePicker.YEAR -> Modifier.scrollablePicker(yearState)
+                                Modifier.scrollablePicker(datePickerState.monthState)
+                            FocusableElementDatePicker.YEAR ->
+                                Modifier.scrollablePicker(datePickerState.yearState)
                             else -> Modifier
                         }
                     } else {
@@ -731,7 +732,7 @@
                 ) {
                     if (focusedElement.index < 2) {
                         DatePickerImpl(
-                            state = dayState,
+                            state = datePickerState.dayState,
                             readOnly = focusedElement != FocusableElementDatePicker.DAY,
                             onSelected = {
                                 doubleTapToNext(
@@ -739,7 +740,7 @@
                                     FocusableElementDatePicker.MONTH
                                 )
                             },
-                            text = { day: Int -> "%d".format(day + 1) },
+                            text = { day: Int -> "%d".format(datePickerState.currentDay(day)) },
                             width = dayWidth,
                             focusRequester = focusRequesterDay,
                             contentDescription = dayContentDescription,
@@ -750,7 +751,7 @@
                         Spacer(modifier = Modifier.width(spacerWidth))
                     }
                     DatePickerImpl(
-                        state = monthState,
+                        state = datePickerState.monthState,
                         readOnly = focusedElement != FocusableElementDatePicker.MONTH,
                         onSelected = {
                             doubleTapToNext(
@@ -758,7 +759,8 @@
                                 FocusableElementDatePicker.YEAR
                             )
                         },
-                        text = { month: Int -> monthNames[month] },
+                        text = { month: Int ->
+                            shortMonthNames[(datePickerState.currentMonth(month) - 1) % 12] },
                         width = monthWidth,
                         focusRequester = focusRequesterMonth,
                         contentDescription = monthContentDescription,
@@ -769,7 +771,7 @@
                     if (focusedElement.index > 0) {
                         Spacer(modifier = Modifier.width(spacerWidth))
                         DatePickerImpl(
-                            state = yearState,
+                            state = datePickerState.yearState,
                             readOnly = focusedElement != FocusableElementDatePicker.YEAR,
                             onSelected = {
                                 doubleTapToNext(
@@ -777,7 +779,7 @@
                                     FocusableElementDatePicker.CONFIRM_BUTTON
                                 )
                             },
-                            text = { year: Int -> "%4d".format(year + 1) },
+                            text = { year: Int -> "%4d".format(datePickerState.currentYear(year)) },
                             width = yearWidth,
                             focusRequester = focusRequesterYear,
                             contentDescription = yearContentDescription,
@@ -795,17 +797,17 @@
                 Button(
                     onClick = {
                         if (focusedElement.index >= 2) {
-                            val confirmedDate = LocalDate.of(
-                                yearState.selectedOption + 1,
-                                monthState.selectedOption + 1,
-                                dayState.selectedOption + 1
-                            )
+                            val confirmedYear: Int = datePickerState.currentYear()
+                            val confirmedMonth: Int = datePickerState.currentMonth()
+                            val confirmedDay: Int = datePickerState.currentDay()
+                            val confirmedDate =
+                                LocalDate.of(confirmedYear, confirmedMonth, confirmedDay)
                             onDateConfirm(confirmedDate)
                         } else if (focusedElement == FocusableElementDatePicker.DAY) {
-                             doubleTapToNext(
-                                 FocusableElementDatePicker.DAY,
-                                 FocusableElementDatePicker.MONTH
-                             )
+                            doubleTapToNext(
+                                FocusableElementDatePicker.DAY,
+                                FocusableElementDatePicker.MONTH
+                            )
                         } else if (focusedElement == FocusableElementDatePicker.MONTH) {
                             doubleTapToNext(
                                 FocusableElementDatePicker.MONTH,
@@ -1086,6 +1088,98 @@
         else -> label
     }
 }
+
+@RequiresApi(Build.VERSION_CODES.O)
+private fun verifyDates(
+    date: LocalDate,
+    fromDate: LocalDate,
+    toDate: LocalDate
+) {
+    require(toDate >= fromDate) { "toDate should be greater than or equal to fromDate" }
+    require(date in fromDate..toDate) { "date should lie between fromDate and toDate" }
+}
+
+@SuppressLint("ClassVerificationFailure")
+@RequiresApi(Build.VERSION_CODES.O)
+private fun getMonthNames(pattern: String): List<String> {
+    val monthFormatter = DateTimeFormatter.ofPattern(pattern)
+    val months = 1..12
+    return months.map {
+        LocalDate.of(2022, it, 1).format(monthFormatter)
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+internal class DatePickerState constructor(
+    private val date: LocalDate,
+    private val fromDate: LocalDate?,
+    private val toDate: LocalDate?
+) {
+    private val yearOffset = fromDate?.year ?: 1
+    val numOfYears = (toDate?.year ?: 3000) - (yearOffset - 1)
+    val yearState =
+        PickerState(
+            initialNumberOfOptions = numOfYears,
+            initiallySelectedOption = date.year - yearOffset
+        )
+    val selectedYearEqualsFromYear: Boolean
+        get() = (yearState.selectedOption == 0)
+    val selectedYearEqualsToYear: Boolean
+        get() = (yearState.selectedOption == yearState.numberOfOptions - 1)
+
+    private val monthOffset: Int
+        get() = (if (selectedYearEqualsFromYear) (fromDate?.monthValue ?: 1) else 1)
+    private val maxMonths: Int
+        get() = (if (selectedYearEqualsToYear) (toDate?.monthValue ?: 12) else 12)
+    val numOfMonths: Int
+        get() = (maxMonths.minus(monthOffset - 1))
+    val monthState =
+        PickerState(
+            initialNumberOfOptions = numOfMonths,
+            initiallySelectedOption = date.monthValue - monthOffset
+        )
+    val selectedMonthEqualsFromMonth: Boolean
+        get() = (selectedYearEqualsFromYear && monthState.selectedOption == 0)
+    val selectedMonthEqualsToMonth: Boolean
+        get() = (selectedYearEqualsToYear &&
+            monthState.selectedOption == monthState.numberOfOptions - 1)
+
+    private val dayOffset: Int
+        get() = (if (selectedMonthEqualsFromMonth) (fromDate?.dayOfMonth ?: 1) else 1)
+    private val firstDayOfMonth: LocalDate
+        get() = LocalDate.of(
+            currentYear(),
+            currentMonth(),
+            1
+        )
+    private val maxDaysInMonth: Int
+        get() = (
+            if (toDate != null && selectedMonthEqualsToMonth) {
+                toDate.dayOfMonth
+            } else {
+                firstDayOfMonth.with(TemporalAdjusters.lastDayOfMonth()).dayOfMonth
+            }
+            )
+    val numOfDays: Int
+        get() = maxDaysInMonth.minus(dayOffset - 1)
+
+    val dayState =
+        PickerState(
+            initialNumberOfOptions = numOfDays,
+            initiallySelectedOption = date.dayOfMonth - dayOffset
+        )
+
+    fun currentYear(year: Int = yearState.selectedOption): Int {
+        return year + yearOffset
+    }
+    fun currentMonth(month: Int = monthState.selectedOption): Int {
+        return month + monthOffset
+    }
+    fun currentDay(day: Int = dayState.selectedOption): Int {
+        return day + dayOffset
+    }
+}
+
 enum class FocusableElement(val index: Int) {
     HOURS(0),
     MINUTES(1),
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PositionIndicatorDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PositionIndicatorDemos.kt
index 76fe1ef..d8f3599 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PositionIndicatorDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PositionIndicatorDemos.kt
@@ -41,6 +41,8 @@
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
 import androidx.wear.compose.material.Button
 import androidx.wear.compose.material.Chip
 import androidx.wear.compose.material.MaterialTheme
@@ -49,10 +51,8 @@
 import androidx.wear.compose.material.PositionIndicatorState
 import androidx.wear.compose.material.PositionIndicatorVisibility
 import androidx.wear.compose.material.Scaffold
-import androidx.wear.compose.material.ScalingLazyColumn
 import androidx.wear.compose.material.Text
 import androidx.wear.compose.material.ToggleButton
-import androidx.wear.compose.material.rememberScalingLazyListState
 
 @Composable
 fun HideWhenFullDemo() {
@@ -109,8 +109,8 @@
 
 @Composable
 fun ControllablePositionIndicator() {
-    var position = remember { mutableStateOf(0.2f) }
-    var size = remember { mutableStateOf(0.5f) }
+    val position = remember { mutableStateOf(0.2f) }
+    val size = remember { mutableStateOf(0.5f) }
     var alignment by remember { mutableStateOf(0) }
     var reverseDirection by remember { mutableStateOf(false) }
     var layoutDirection by remember { mutableStateOf(false) }
@@ -139,7 +139,9 @@
             }
         ) {
             Box(
-                modifier = Modifier.fillMaxHeight().padding(horizontal = 20.dp),
+                modifier = Modifier
+                    .fillMaxHeight()
+                    .padding(horizontal = 20.dp),
                 contentAlignment = Alignment.Center
             ) {
                 Column {
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScalingLazyColumnDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScalingLazyColumnDemo.kt
index 33dd48b..7597a57 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScalingLazyColumnDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScalingLazyColumnDemo.kt
@@ -28,15 +28,15 @@
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.Dp
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
 import androidx.wear.compose.material.AppCard
 import androidx.wear.compose.material.CardDefaults
 import androidx.wear.compose.material.Chip
 import androidx.wear.compose.material.ChipDefaults
 import androidx.wear.compose.material.ListHeader
 import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.ScalingLazyColumn
 import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.rememberScalingLazyListState
 import kotlin.math.roundToInt
 
 @Composable
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
index 6afd2a2..5ff8866 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
@@ -33,7 +33,9 @@
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import androidx.wear.compose.material.AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
 import androidx.wear.compose.material.Card
 import androidx.wear.compose.material.Chip
 import androidx.wear.compose.material.ChipDefaults
@@ -41,10 +43,8 @@
 import androidx.wear.compose.material.MaterialTheme
 import androidx.wear.compose.material.PositionIndicator
 import androidx.wear.compose.material.Scaffold
-import androidx.wear.compose.material.ScalingLazyColumn
 import androidx.wear.compose.material.Text
 import androidx.wear.compose.material.TimeText
-import androidx.wear.compose.material.rememberScalingLazyListState
 import androidx.wear.compose.material.scrollAway
 
 @Composable
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SliderDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SliderDemo.kt
index 7eb5827..df5330a 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SliderDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SliderDemo.kt
@@ -31,7 +31,7 @@
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
-import androidx.wear.compose.material.AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.AutoCenteringParams
 import androidx.wear.compose.material.Icon
 import androidx.wear.compose.material.InlineSlider
 import androidx.wear.compose.material.InlineSliderColors
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ToggleChipDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ToggleChipDemo.kt
index b5c6dfe..0b71905 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ToggleChipDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ToggleChipDemo.kt
@@ -35,20 +35,20 @@
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.ScalingLazyListState
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
 import androidx.wear.compose.material.Checkbox
 import androidx.wear.compose.material.CheckboxDefaults
 import androidx.wear.compose.material.ListHeader
 import androidx.wear.compose.material.MaterialTheme
 import androidx.wear.compose.material.RadioButton
 import androidx.wear.compose.material.RadioButtonDefaults
-import androidx.wear.compose.material.ScalingLazyListState
 import androidx.wear.compose.material.SplitToggleChip
 import androidx.wear.compose.material.Switch
 import androidx.wear.compose.material.SwitchDefaults
 import androidx.wear.compose.material.Text
 import androidx.wear.compose.material.ToggleChip
 import androidx.wear.compose.material.ToggleChipDefaults
-import androidx.wear.compose.material.rememberScalingLazyListState
 
 @Composable
 fun ToggleChips(
diff --git a/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/ScrollActivity.kt b/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/ScrollActivity.kt
index e35455c..ee57c4f 100644
--- a/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/ScrollActivity.kt
+++ b/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/ScrollActivity.kt
@@ -16,8 +16,8 @@
 
 package androidx.wear.compose.integration.macrobenchmark.target
 
-import androidx.activity.ComponentActivity
 import android.os.Bundle
+import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Arrangement
@@ -31,10 +31,10 @@
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
 import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.ScalingLazyColumn
 import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.rememberScalingLazyListState
 
 class ScrollActivity : ComponentActivity() {
     private var itemHeightDp: Dp = 20.dp
@@ -56,7 +56,8 @@
                     modifier = Modifier.semantics { contentDescription = CONTENT_DESCRIPTION }
                 ) {
                     items(5000) { it ->
-                        Box(Modifier
+                        Box(
+                            Modifier
                                 .requiredHeight(itemHeightDp)
                                 .background(MaterialTheme.colors.surface)
                                 .fillMaxSize()
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/current.txt b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
index e6f50d0..654b5cb 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
@@ -1 +1,58 @@
 // Signature format: 4.0
+package androidx.wear.protolayout.expression.pipeline {
+
+  public interface BoundDynamicType extends java.lang.AutoCloseable {
+    method @UiThread public void close();
+  }
+
+  public class DynamicTypeEvaluator implements java.lang.AutoCloseable {
+    ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore);
+    ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.QuotaManager);
+    method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString, android.icu.util.ULocale, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.String!>);
+    method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
+    method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Float!>);
+    method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
+    method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Boolean!>);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void close();
+    method @UiThread public void disablePlatformDataSources();
+    method @UiThread public void enablePlatformDataSources();
+  }
+
+  public interface DynamicTypeValueReceiver<T> {
+    method @UiThread public void onData(T);
+    method @UiThread public void onInvalidated();
+  }
+
+  public class ObservableStateStore {
+    ctor public ObservableStateStore(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue!>);
+    method @UiThread public void setStateEntryValues(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue!>);
+  }
+
+  public interface QuotaManager {
+    method public void releaseQuota(int);
+    method public boolean tryAcquireQuota(int);
+  }
+
+}
+
+package androidx.wear.protolayout.expression.pipeline.sensor {
+
+  public interface SensorGateway extends java.io.Closeable {
+    method public void close();
+    method @UiThread public void registerSensorGatewayConsumer(androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.Consumer);
+    method @UiThread public void registerSensorGatewayConsumer(java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.Consumer);
+    method @UiThread public void unregisterSensorGatewayConsumer(androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.Consumer);
+    field @RequiresPermission(android.Manifest.permission.ACTIVITY_RECOGNITION) public static final int SENSOR_DATA_TYPE_DAILY_STEP_COUNT = 1; // 0x1
+    field @RequiresPermission(android.Manifest.permission.BODY_SENSORS) public static final int SENSOR_DATA_TYPE_HEART_RATE = 0; // 0x0
+    field public static final int SENSOR_DATA_TYPE_INVALID = -1; // 0xffffffff
+  }
+
+  public static interface SensorGateway.Consumer {
+    method public int getRequestedDataType();
+    method @AnyThread public void onData(double);
+    method @AnyThread public default void onInvalidated();
+    method @AnyThread public default void onPreUpdate();
+  }
+
+}
+
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
index e6f50d0..654b5cb 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
@@ -1 +1,58 @@
 // Signature format: 4.0
+package androidx.wear.protolayout.expression.pipeline {
+
+  public interface BoundDynamicType extends java.lang.AutoCloseable {
+    method @UiThread public void close();
+  }
+
+  public class DynamicTypeEvaluator implements java.lang.AutoCloseable {
+    ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore);
+    ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.QuotaManager);
+    method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString, android.icu.util.ULocale, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.String!>);
+    method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
+    method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Float!>);
+    method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
+    method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Boolean!>);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void close();
+    method @UiThread public void disablePlatformDataSources();
+    method @UiThread public void enablePlatformDataSources();
+  }
+
+  public interface DynamicTypeValueReceiver<T> {
+    method @UiThread public void onData(T);
+    method @UiThread public void onInvalidated();
+  }
+
+  public class ObservableStateStore {
+    ctor public ObservableStateStore(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue!>);
+    method @UiThread public void setStateEntryValues(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue!>);
+  }
+
+  public interface QuotaManager {
+    method public void releaseQuota(int);
+    method public boolean tryAcquireQuota(int);
+  }
+
+}
+
+package androidx.wear.protolayout.expression.pipeline.sensor {
+
+  public interface SensorGateway extends java.io.Closeable {
+    method public void close();
+    method @UiThread public void registerSensorGatewayConsumer(androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.Consumer);
+    method @UiThread public void registerSensorGatewayConsumer(java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.Consumer);
+    method @UiThread public void unregisterSensorGatewayConsumer(androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.Consumer);
+    field @RequiresPermission(android.Manifest.permission.ACTIVITY_RECOGNITION) public static final int SENSOR_DATA_TYPE_DAILY_STEP_COUNT = 1; // 0x1
+    field @RequiresPermission(android.Manifest.permission.BODY_SENSORS) public static final int SENSOR_DATA_TYPE_HEART_RATE = 0; // 0x0
+    field public static final int SENSOR_DATA_TYPE_INVALID = -1; // 0xffffffff
+  }
+
+  public static interface SensorGateway.Consumer {
+    method public int getRequestedDataType();
+    method @AnyThread public void onData(double);
+    method @AnyThread public default void onInvalidated();
+    method @AnyThread public default void onPreUpdate();
+  }
+
+}
+
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
index e6f50d0..25b65f7 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
@@ -1 +1,60 @@
 // Signature format: 4.0
+package androidx.wear.protolayout.expression.pipeline {
+
+  public interface BoundDynamicType extends java.lang.AutoCloseable {
+    method @UiThread public void close();
+  }
+
+  public class DynamicTypeEvaluator implements java.lang.AutoCloseable {
+    ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore);
+    ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.QuotaManager);
+    method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString, android.icu.util.ULocale, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.String!>);
+    method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
+    method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Float!>);
+    method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
+    method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Boolean!>);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void close();
+    method @UiThread public void disablePlatformDataSources();
+    method @UiThread public void enablePlatformDataSources();
+  }
+
+  public interface DynamicTypeValueReceiver<T> {
+    method @UiThread public void onData(T);
+    method @UiThread public void onInvalidated();
+  }
+
+  public class ObservableStateStore {
+    ctor public ObservableStateStore(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue!>);
+    method @UiThread public void setStateEntryValues(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue!>);
+  }
+
+  public interface QuotaManager {
+    method public void releaseQuota(int);
+    method public boolean tryAcquireQuota(int);
+  }
+
+}
+
+package androidx.wear.protolayout.expression.pipeline.sensor {
+
+  public interface SensorGateway extends java.io.Closeable {
+    method public void close();
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void disableUpdates();
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void enableUpdates();
+    method @UiThread public void registerSensorGatewayConsumer(androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.Consumer);
+    method @UiThread public void registerSensorGatewayConsumer(java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.Consumer);
+    method @UiThread public void unregisterSensorGatewayConsumer(androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.Consumer);
+    field @RequiresPermission(android.Manifest.permission.ACTIVITY_RECOGNITION) public static final int SENSOR_DATA_TYPE_DAILY_STEP_COUNT = 1; // 0x1
+    field @RequiresPermission(android.Manifest.permission.BODY_SENSORS) public static final int SENSOR_DATA_TYPE_HEART_RATE = 0; // 0x0
+    field public static final int SENSOR_DATA_TYPE_INVALID = -1; // 0xffffffff
+  }
+
+  public static interface SensorGateway.Consumer {
+    method public int getRequestedDataType();
+    method @AnyThread public void onData(double);
+    method @AnyThread public default void onInvalidated();
+    method @AnyThread public default void onPreUpdate();
+  }
+
+}
+
diff --git a/wear/protolayout/protolayout-expression-pipeline/build.gradle b/wear/protolayout/protolayout-expression-pipeline/build.gradle
index 7a9f216..24961ce 100644
--- a/wear/protolayout/protolayout-expression-pipeline/build.gradle
+++ b/wear/protolayout/protolayout-expression-pipeline/build.gradle
@@ -23,10 +23,28 @@
 
 dependencies {
     annotationProcessor(libs.nullaway)
+    api("androidx.annotation:annotation:1.2.0")
+    implementation("androidx.collection:collection:1.2.0")
+
+    implementation("androidx.annotation:annotation-experimental:1.2.0")
+    implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
+    implementation(project(":wear:protolayout:protolayout-expression"))
+
+    compileOnly(libs.kotlinStdlib) // For annotation-experimental
+
+    testImplementation(libs.testExtJunit)
+    testImplementation(libs.testExtTruth)
+    testImplementation(libs.testRunner)
+    testImplementation(libs.robolectric)
+    testImplementation(libs.truth)
 }
 
 android {
     namespace "androidx.wear.protolayout.expression.pipeline"
+
+    defaultConfig {
+        minSdkVersion 26
+    }
 }
 
 androidx {
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/AnimatableNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/AnimatableNode.java
new file mode 100644
index 0000000..c36521a
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/AnimatableNode.java
@@ -0,0 +1,103 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.UiThread;
+import androidx.annotation.VisibleForTesting;
+
+/** Data animatable source node within a dynamic data pipeline. */
+abstract class AnimatableNode {
+
+    private boolean mIsVisible = false;
+    @NonNull final QuotaAwareAnimator mQuotaAwareAnimator;
+
+    protected AnimatableNode(@NonNull QuotaManager quotaManager) {
+        mQuotaAwareAnimator = new QuotaAwareAnimator(null, quotaManager);
+    }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    AnimatableNode(@NonNull QuotaAwareAnimator quotaAwareAnimator) {
+        mQuotaAwareAnimator = quotaAwareAnimator;
+    }
+
+    /**
+     * Starts the animator (if present) if the node is visible and there is a quota, otherwise, skip
+     * it.
+     */
+    @UiThread
+    @SuppressLint("CheckResult") // (b/247804720)
+    protected void startOrSkipAnimator() {
+        if (mIsVisible) {
+            mQuotaAwareAnimator.tryStartAnimation();
+        } else {
+            stopOrPauseAnimator();
+        }
+    }
+
+    /**
+     * Sets the node's visibility and resumes or stops the corresponding animator (if present).
+     *
+     * <p>If it's becoming visible, paused animations are resumed, other infinite animations that
+     * haven't started yet will start.
+     *
+     * <p>If it's becoming invisible, all animations should skip to end or pause.
+     */
+    @UiThread
+    void setVisibility(boolean visible) {
+        if (mIsVisible == visible) {
+            return;
+        }
+        mIsVisible = visible;
+        if (mIsVisible) {
+            startOrResumeAnimator();
+        } else if (mQuotaAwareAnimator.hasRunningAnimation()) {
+            stopOrPauseAnimator();
+        }
+    }
+
+    /**
+     * Starts or resumes the animator if there is a quota, depending on whether the animation was
+     * paused.
+     */
+    @SuppressLint("CheckResult") // (b/247804720)
+    private void startOrResumeAnimator() {
+        mQuotaAwareAnimator.tryStartOrResumeAnimator();
+    }
+
+    /** Returns whether this node has a running animation. */
+    boolean hasRunningAnimation() {
+        return mQuotaAwareAnimator.hasRunningAnimation();
+    }
+
+    /** Returns whether the animator in this node has an infinite duration. */
+    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+    protected boolean isInfiniteAnimator() {
+        return mQuotaAwareAnimator.isInfiniteAnimator();
+    }
+
+    /**
+     * Pauses the animator in this node if it has infinite duration, stop it otherwise. Note that
+     * this method has no effect on infinite animators that are not running since Animator#pause()
+     * will be a no-op in that case.
+     */
+    private void stopOrPauseAnimator() {
+        mQuotaAwareAnimator.stopOrPauseAnimator();
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/AnimationsHelper.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/AnimationsHelper.java
new file mode 100644
index 0000000..57490a3
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/AnimationsHelper.java
@@ -0,0 +1,165 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import android.animation.ValueAnimator;
+import android.view.animation.Animation;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.PathInterpolator;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
+import androidx.wear.protolayout.expression.proto.AnimationParameterProto.CubicBezierEasing;
+import androidx.wear.protolayout.expression.proto.AnimationParameterProto.RepeatMode;
+import androidx.wear.protolayout.expression.proto.AnimationParameterProto.Repeatable;
+
+import java.time.Duration;
+import java.util.EnumMap;
+import java.util.Map;
+
+/**
+ * Helper class for Animations in ProtoLayout. It contains helper methods used in rendered and
+ * constants for default values.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class AnimationsHelper {
+
+    private static final Duration DEFAULT_ANIM_DURATION = Duration.ofMillis(300);
+    private static final Interpolator DEFAULT_ANIM_INTERPOLATOR = new LinearInterpolator();
+    private static final Duration DEFAULT_ANIM_DELAY = Duration.ZERO;
+    private static final int DEFAULT_REPEAT_COUNT = 0;
+    private static final int DEFAULT_REPEAT_MODE = ValueAnimator.RESTART;
+    private static final Map<RepeatMode, Integer> sRepeatModeForAnimator =
+            new EnumMap<>(RepeatMode.class);
+
+    static {
+        sRepeatModeForAnimator.put(RepeatMode.REPEAT_MODE_UNKNOWN, DEFAULT_REPEAT_MODE);
+        sRepeatModeForAnimator.put(RepeatMode.REPEAT_MODE_RESTART, ValueAnimator.RESTART);
+        sRepeatModeForAnimator.put(RepeatMode.REPEAT_MODE_REVERSE, ValueAnimator.REVERSE);
+    }
+
+    private AnimationsHelper() {}
+
+    /** Returns the duration from the given {@link AnimationSpec} or default value if not set. */
+    @NonNull
+    public static Duration getDurationOrDefault(@NonNull AnimationSpec spec) {
+        return spec.getDurationMillis() > 0
+                ? Duration.ofMillis(spec.getDurationMillis())
+                : DEFAULT_ANIM_DURATION;
+    }
+
+    /** Returns the delay from the given {@link AnimationSpec} or default value if not set. */
+    @NonNull
+    public static Duration getDelayOrDefault(@NonNull AnimationSpec spec) {
+        return spec.getDelayMillis() > 0
+                ? Duration.ofMillis(spec.getDelayMillis())
+                : DEFAULT_ANIM_DELAY;
+    }
+
+    /**
+     * Returns the easing converted to the Interpolator from the given {@link AnimationSpec} or
+     * default value if not set.
+     */
+    @NonNull
+    public static Interpolator getInterpolatorOrDefault(@NonNull AnimationSpec spec) {
+        Interpolator interpolator = DEFAULT_ANIM_INTERPOLATOR;
+
+        if (spec.hasEasing()) {
+            switch (spec.getEasing().getInnerCase()) {
+                case CUBIC_BEZIER:
+                    if (spec.getEasing().hasCubicBezier()) {
+                        CubicBezierEasing cbe = spec.getEasing().getCubicBezier();
+                        interpolator =
+                                new PathInterpolator(
+                                        cbe.getX1(), cbe.getY1(), cbe.getX2(), cbe.getY2());
+                    }
+                    break;
+                case INNER_NOT_SET:
+                    break;
+            }
+        }
+
+        return interpolator;
+    }
+
+    /**
+     * Returns the repeat count from the given {@link AnimationSpec} or default value if not set.
+     */
+    public static int getRepeatCountOrDefault(@NonNull AnimationSpec spec) {
+        int repeatCount = DEFAULT_REPEAT_COUNT;
+
+        if (spec.hasRepeatable()) {
+            Repeatable repeatable = spec.getRepeatable();
+            if (repeatable.getIterations() <= 0) {
+                repeatCount = ValueAnimator.INFINITE;
+            } else {
+                // -1 because ValueAnimator uses count as number of how many times will animation be
+                // repeated in addition to the first play.
+                repeatCount = repeatable.getIterations() - 1;
+            }
+        }
+
+        return repeatCount;
+    }
+
+    /** Returns the repeat mode from the given {@link AnimationSpec} or default value if not set. */
+    public static int getRepeatModeOrDefault(@NonNull AnimationSpec spec) {
+        int repeatMode = DEFAULT_REPEAT_MODE;
+
+        if (spec.hasRepeatable()) {
+            Repeatable repeatable = spec.getRepeatable();
+            Integer repeatModeFromMap = sRepeatModeForAnimator.get(repeatable.getRepeatMode());
+            if (repeatModeFromMap != null) {
+                repeatMode = repeatModeFromMap;
+            }
+        }
+
+        return repeatMode;
+    }
+
+    /**
+     * Sets animation parameters (duration, delay, easing, repeat mode and count) to the given
+     * animator. These will be values from the given AnimationSpec if they are set and default
+     * values otherwise.
+     */
+    public static void applyAnimationSpecToAnimator(
+            @NonNull ValueAnimator animator, @NonNull AnimationSpec spec) {
+        animator.setDuration(getDurationOrDefault(spec).toMillis());
+        animator.setStartDelay(getDelayOrDefault(spec).toMillis());
+        animator.setInterpolator(getInterpolatorOrDefault(spec));
+        animator.setRepeatCount(getRepeatCountOrDefault(spec));
+        animator.setRepeatMode(getRepeatModeOrDefault(spec));
+    }
+
+    /**
+     * Sets animation parameters (duration, delay, easing, repeat mode and count) to the given
+     * animation. These will be values from the given AnimationSpec if they are set and default
+     * values otherwise.
+     */
+    public static void applyAnimationSpecToAnimation(
+            @NonNull Animation animation, @NonNull AnimationSpec spec) {
+        animation.setDuration(getDurationOrDefault(spec).toMillis());
+        animation.setStartOffset(getDelayOrDefault(spec).toMillis());
+        animation.setInterpolator(getInterpolatorOrDefault(spec));
+        animation.setRepeatCount(getRepeatCountOrDefault(spec));
+        animation.setRepeatMode(getRepeatModeOrDefault(spec));
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoolNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoolNodes.java
new file mode 100644
index 0000000..a5b484f
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoolNodes.java
@@ -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.wear.protolayout.expression.pipeline;
+
+import android.util.Log;
+
+import androidx.annotation.UiThread;
+import androidx.wear.protolayout.expression.proto.DynamicProto;
+import androidx.wear.protolayout.expression.proto.DynamicProto.ComparisonFloatOp;
+import androidx.wear.protolayout.expression.proto.DynamicProto.ComparisonInt32Op;
+import androidx.wear.protolayout.expression.proto.DynamicProto.StateBoolSource;
+import androidx.wear.protolayout.expression.proto.FixedProto.FixedBool;
+
+/** Dynamic data nodes which yield boleans. */
+class BoolNodes {
+    private BoolNodes() {}
+
+    /** Dynamic boolean node that has a fixed value. */
+    static class FixedBoolNode implements DynamicDataSourceNode<Boolean> {
+        private final boolean mValue;
+        private final DynamicTypeValueReceiver<Boolean> mDownstream;
+
+        FixedBoolNode(FixedBool protoNode, DynamicTypeValueReceiver<Boolean> downstream) {
+            mValue = protoNode.getValue();
+            mDownstream = downstream;
+        }
+
+        @Override
+        @UiThread
+        public void preInit() {
+            mDownstream.onPreUpdate();
+        }
+
+        @Override
+        @UiThread
+        public void init() {
+            mDownstream.onData(mValue);
+        }
+
+        @Override
+        @UiThread
+        public void destroy() {}
+    }
+
+    /** Dynamic boolean node that gets value from the state. */
+    static class StateBoolNode extends StateSourceNode<Boolean> {
+        StateBoolNode(
+                ObservableStateStore stateStore,
+                StateBoolSource protoNode,
+                DynamicTypeValueReceiver<Boolean> downstream) {
+            super(
+                    stateStore,
+                    protoNode.getSourceKey(),
+                    se -> se.getBoolVal().getValue(),
+                    downstream);
+        }
+    }
+
+    /** Dynamic boolean node that gets value from comparing two integers. */
+    static class ComparisonInt32Node extends DynamicDataBiTransformNode<Integer, Integer, Boolean> {
+        private static final String TAG = "ComparisonInt32Node";
+
+        ComparisonInt32Node(
+                ComparisonInt32Op protoNode, DynamicTypeValueReceiver<Boolean> downstream) {
+            super(
+                    downstream,
+                    (lhs, rhs) -> {
+                        int unboxedLhs = lhs;
+                        int unboxedRhs = rhs;
+
+                        switch (protoNode.getOperationType()) {
+                            case COMPARISON_OP_TYPE_EQUALS:
+                                return unboxedLhs == unboxedRhs;
+                            case COMPARISON_OP_TYPE_NOT_EQUALS:
+                                return unboxedLhs != unboxedRhs;
+                            case COMPARISON_OP_TYPE_LESS_THAN:
+                                return unboxedLhs < unboxedRhs;
+                            case COMPARISON_OP_TYPE_LESS_THAN_OR_EQUAL_TO:
+                                return unboxedLhs <= unboxedRhs;
+                            case COMPARISON_OP_TYPE_GREATER_THAN:
+                                return unboxedLhs > unboxedRhs;
+                            case COMPARISON_OP_TYPE_GREATER_THAN_OR_EQUAL_TO:
+                                return unboxedLhs >= unboxedRhs;
+                            default:
+                                Log.e(TAG, "Unknown operation type in ComparisonInt32Node");
+                                return false;
+                        }
+                    });
+        }
+    }
+
+    /** Dynamic boolean node that gets value from comparing two floats. */
+    static class ComparisonFloatNode extends DynamicDataBiTransformNode<Float, Float, Boolean> {
+        private static final String TAG = "ComparisonFloatNode";
+        public static final float EPSILON = 1e-6f;
+
+        ComparisonFloatNode(
+                ComparisonFloatOp protoNode, DynamicTypeValueReceiver<Boolean> downstream) {
+            super(
+                    downstream,
+                    (lhs, rhs) -> {
+                        float unboxedLhs = lhs;
+                        float unboxedRhs = rhs;
+
+                        switch (protoNode.getOperationType()) {
+                            case COMPARISON_OP_TYPE_EQUALS:
+                                return equalFloats(unboxedLhs, unboxedRhs);
+                            case COMPARISON_OP_TYPE_NOT_EQUALS:
+                                return !equalFloats(unboxedLhs, unboxedRhs);
+                            case COMPARISON_OP_TYPE_LESS_THAN:
+                                return (unboxedLhs < unboxedRhs)
+                                        && !equalFloats(unboxedLhs, unboxedRhs);
+                            case COMPARISON_OP_TYPE_LESS_THAN_OR_EQUAL_TO:
+                                return (unboxedLhs < unboxedRhs)
+                                        || equalFloats(unboxedLhs, unboxedRhs);
+                            case COMPARISON_OP_TYPE_GREATER_THAN:
+                                return (unboxedLhs > unboxedRhs)
+                                        && !equalFloats(unboxedLhs, unboxedRhs);
+                            case COMPARISON_OP_TYPE_GREATER_THAN_OR_EQUAL_TO:
+                                return (unboxedLhs > unboxedRhs)
+                                        || equalFloats(unboxedLhs, unboxedRhs);
+                            default:
+                                Log.e(TAG, "Unknown operation type in ComparisonInt32Node");
+                                return false;
+                        }
+                    });
+        }
+
+        private static boolean equalFloats(float lhs, float rhs) {
+            return Math.abs(lhs - rhs) < EPSILON;
+        }
+    }
+
+    /** Dynamic boolean node that gets opposite value from another boolean node. */
+    static class NotBoolOp extends DynamicDataTransformNode<Boolean, Boolean> {
+        NotBoolOp(DynamicTypeValueReceiver<Boolean> downstream) {
+            super(downstream, b -> !b);
+        }
+    }
+
+    /** Dynamic boolean node that gets value from logical operation. */
+    static class LogicalBoolOp extends DynamicDataBiTransformNode<Boolean, Boolean, Boolean> {
+        private static final String TAG = "LogicalBooleanOp";
+
+        LogicalBoolOp(
+                DynamicProto.LogicalBoolOp protoNode,
+                DynamicTypeValueReceiver<Boolean> downstream) {
+            super(
+                    downstream,
+                    (a, b) -> {
+                        switch (protoNode.getOperationType()) {
+                            case LOGICAL_OP_TYPE_AND:
+                                return a && b;
+                            case LOGICAL_OP_TYPE_OR:
+                                return a || b;
+                            default:
+                                Log.e(TAG, "Unknown operation type in LogicalBoolOp");
+                                return false;
+                        }
+                    });
+        }
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicType.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicType.java
new file mode 100644
index 0000000..af3b6f8
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicType.java
@@ -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.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.UiThread;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * An object representing a dynamic type that is being evaluated by {@link
+ * DynamicTypeEvaluator#bind}.
+ */
+public interface BoundDynamicType extends AutoCloseable {
+    /**
+     * Sets the visibility to all animations in this dynamic type. They can be triggered when
+     * visible.
+     *
+     * @hide
+     */
+    @UiThread
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    void setAnimationVisibility(boolean visible);
+
+    /**
+     * Returns the number of currently running animations in this dynamic type.
+     *
+     * @hide
+     */
+    @UiThread
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    int getRunningAnimationCount();
+
+    /** Destroys this dynamic type and it shouldn't be used after this. */
+    @UiThread
+    @Override
+    void close();
+
+    /**
+     * Returns the number of dynamic nodes that this dynamic type contains.
+     *
+     * @hide
+     */
+    @UiThread
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    int getDynamicNodeCount();
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
new file mode 100644
index 0000000..4ab8ecd
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
@@ -0,0 +1,64 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import java.util.List;
+
+/**
+ * Dynamic type node implementation that contains a list of {@link DynamicDataNode} created during
+ * evaluation.
+ */
+class BoundDynamicTypeImpl implements BoundDynamicType {
+    private final List<DynamicDataNode<?>> mNodes;
+
+    BoundDynamicTypeImpl(List<DynamicDataNode<?>> nodes) {
+        this.mNodes = nodes;
+    }
+
+    /**
+     * Sets visibility for all {@link AnimatableNode} in this dynamic type. For others, this is
+     * no-op.
+     */
+    @Override
+    public void setAnimationVisibility(boolean visible) {
+        mNodes.stream()
+                .filter(n -> n instanceof AnimatableNode)
+                .forEach(n -> ((AnimatableNode) n).setVisibility(visible));
+    }
+
+    /** Returns how many of {@link AnimatableNode} are running. */
+    @Override
+    public int getRunningAnimationCount() {
+        return (int)
+                mNodes.stream()
+                        .filter(n -> n instanceof AnimatableNode)
+                        .filter(n -> ((AnimatableNode) n).hasRunningAnimation())
+                        .count();
+    }
+
+    @Override
+    public int getDynamicNodeCount() {
+        return mNodes.size();
+    }
+
+    @Override
+    public void close() {
+        mNodes.stream()
+                .filter(n -> n instanceof DynamicDataSourceNode)
+                .forEach(n -> ((DynamicDataSourceNode<?>) n).destroy());
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ColorNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ColorNodes.java
new file mode 100644
index 0000000..54cbc77
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ColorNodes.java
@@ -0,0 +1,196 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import static androidx.wear.protolayout.expression.pipeline.AnimationsHelper.applyAnimationSpecToAnimator;
+
+import android.animation.ValueAnimator;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
+import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableFixedColor;
+import androidx.wear.protolayout.expression.proto.DynamicProto.StateColorSource;
+import androidx.wear.protolayout.expression.proto.FixedProto.FixedColor;
+
+/** Dynamic data nodes which yield colors. */
+class ColorNodes {
+    private ColorNodes() {}
+
+    /** Dynamic color node that has a fixed value. */
+    static class FixedColorNode implements DynamicDataSourceNode<Integer> {
+        private final int mValue;
+        private final DynamicTypeValueReceiver<Integer> mDownstream;
+
+        FixedColorNode(FixedColor protoNode, DynamicTypeValueReceiver<Integer> downstream) {
+            this.mValue = protoNode.getArgb();
+            this.mDownstream = downstream;
+        }
+
+        @Override
+        @UiThread
+        public void preInit() {
+            mDownstream.onPreUpdate();
+        }
+
+        @Override
+        @UiThread
+        public void init() {
+            mDownstream.onData(mValue);
+        }
+
+        @Override
+        public void destroy() {}
+    }
+
+    /** Dynamic color node that gets value from the platform source. */
+    static class StateColorSourceNode extends StateSourceNode<Integer> {
+        StateColorSourceNode(
+                ObservableStateStore observableStateStore,
+                StateColorSource protoNode,
+                DynamicTypeValueReceiver<Integer> downstream) {
+            super(
+                    observableStateStore,
+                    protoNode.getSourceKey(),
+                    se -> se.getColorVal().getArgb(),
+                    downstream);
+        }
+    }
+
+    /** Dynamic color node that gets animatable value from fixed source. */
+    static class AnimatableFixedColorNode extends AnimatableNode
+            implements DynamicDataSourceNode<Integer> {
+
+        private final AnimatableFixedColor mProtoNode;
+        private final DynamicTypeValueReceiver<Integer> mDownstream;
+
+        AnimatableFixedColorNode(
+                AnimatableFixedColor protoNode,
+                DynamicTypeValueReceiver<Integer> mDownstream,
+                QuotaManager quotaManager) {
+            super(quotaManager);
+            this.mProtoNode = protoNode;
+            this.mDownstream = mDownstream;
+        }
+
+        @Override
+        @UiThread
+        public void preInit() {
+            mDownstream.onPreUpdate();
+        }
+
+        @Override
+        @UiThread
+        public void init() {
+            ValueAnimator animator =
+                    ValueAnimator.ofArgb(mProtoNode.getFromArgb(), mProtoNode.getToArgb());
+            animator.addUpdateListener(a -> mDownstream.onData((Integer) a.getAnimatedValue()));
+
+            applyAnimationSpecToAnimator(animator, mProtoNode.getSpec());
+
+            mQuotaAwareAnimator.updateAnimator(animator);
+            startOrSkipAnimator();
+        }
+
+        @Override
+        @UiThread
+        public void destroy() {
+            mQuotaAwareAnimator.stopAnimator();
+        }
+    }
+
+    /** Dynamic color node that gets animatable value from dynamic source. */
+    static class DynamicAnimatedColorNode extends AnimatableNode
+            implements DynamicDataNode<Integer> {
+
+        final DynamicTypeValueReceiver<Integer> mDownstream;
+        private final DynamicTypeValueReceiver<Integer> mInputCallback;
+
+        @Nullable Integer mCurrentValue = null;
+        int mPendingCalls = 0;
+
+        // Static analysis complains about calling methods of parent class AnimatableNode under
+        // initialization but mInputCallback is only used after the constructor is finished.
+        @SuppressWarnings("method.invocation.invalid")
+        DynamicAnimatedColorNode(
+                DynamicTypeValueReceiver<Integer> downstream,
+                @NonNull AnimationSpec spec,
+                QuotaManager quotaManager) {
+            super(quotaManager);
+            this.mDownstream = downstream;
+            this.mInputCallback =
+                    new DynamicTypeValueReceiver<Integer>() {
+                        @Override
+                        public void onPreUpdate() {
+                            mPendingCalls++;
+
+                            if (mPendingCalls == 1) {
+                                mDownstream.onPreUpdate();
+
+                                mQuotaAwareAnimator.resetAnimator();
+                            }
+                        }
+
+                        @Override
+                        public void onData(@NonNull Integer newData) {
+                            if (mPendingCalls > 0) {
+                                mPendingCalls--;
+                            }
+
+                            if (mPendingCalls == 0) {
+                                if (mCurrentValue == null) {
+                                    mCurrentValue = newData;
+                                    mDownstream.onData(mCurrentValue);
+                                } else {
+                                    ValueAnimator animator =
+                                            ValueAnimator.ofArgb(mCurrentValue, newData);
+
+                                    applyAnimationSpecToAnimator(animator, spec);
+                                    animator.addUpdateListener(
+                                            a -> {
+                                                if (mPendingCalls == 0) {
+                                                    mCurrentValue = (Integer) a.getAnimatedValue();
+                                                    mDownstream.onData(mCurrentValue);
+                                                }
+                                            });
+
+                                    mQuotaAwareAnimator.updateAnimator(animator);
+                                    startOrSkipAnimator();
+                                }
+                            }
+                        }
+
+                        @Override
+                        public void onInvalidated() {
+                            if (mPendingCalls > 0) {
+                                mPendingCalls--;
+                            }
+
+                            if (mPendingCalls == 0) {
+                                mCurrentValue = null;
+                                mDownstream.onInvalidated();
+                            }
+                        }
+                    };
+        }
+
+        public DynamicTypeValueReceiver<Integer> getInputCallback() {
+            return mInputCallback;
+        }
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ConditionalOpNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ConditionalOpNode.java
new file mode 100644
index 0000000..4e53d20
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ConditionalOpNode.java
@@ -0,0 +1,193 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Dynamic data nodes which yield result based on the given condition. */
+class ConditionalOpNode<T> implements DynamicDataNode<T> {
+    private final DynamicTypeValueReceiver<T> mTrueValueIncomingCallback;
+    private final DynamicTypeValueReceiver<T> mFalseValueIncomingCallback;
+    private final DynamicTypeValueReceiver<Boolean> mConditionIncomingCallback;
+
+    final DynamicTypeValueReceiver<T> mDownstream;
+
+    @Nullable Boolean mLastConditional;
+    @Nullable T mLastTrueValue;
+    @Nullable T mLastFalseValue;
+
+    // Counters to track how many "in-flight" updates are pending for each input. If any of these
+    // are >0, then we're still waiting for onData() to be called for one of the callbacks,
+    // so we shouldn't emit any values until they have all resolved.
+    int mPendingConditionalUpdates = 0;
+    int mPendingTrueValueUpdates = 0;
+    int mPendingFalseValueUpdates = 0;
+
+    ConditionalOpNode(DynamicTypeValueReceiver<T> downstream) {
+        mDownstream = downstream;
+
+        // These classes refer to this.handleUpdate, which is @UnderInitialization when these
+        // initializers run, and hence raise an error. It's invalid to annotate
+        // handle{Pre}StateUpdate as @UnderInitialization (since it refers to initialized fields),
+        // and moving this assignment into the constructor yields the same error (since one of the
+        // fields has to be assigned first, when the class is still under initialization).
+        //
+        // The only path to get these is via get*IncomingCallback, which can only be called when the
+        // class is initialized (and which also cannot be called from a sub-constructor, as that
+        // will again complain that it's calling something which is @UnderInitialization). Given
+        // that, suppressing the warning in onData should be safe.
+        mTrueValueIncomingCallback =
+                new DynamicTypeValueReceiver<T>() {
+                    @Override
+                    public void onPreUpdate() {
+                        mPendingTrueValueUpdates++;
+
+                        if (mPendingTrueValueUpdates == 1
+                                && mPendingFalseValueUpdates == 0
+                                && mPendingConditionalUpdates == 0) {
+                            mDownstream.onPreUpdate();
+                        }
+                    }
+
+                    @SuppressWarnings("method.invocation")
+                    @Override
+                    public void onData(@NonNull T newData) {
+                        if (mPendingTrueValueUpdates > 0) {
+                            mPendingTrueValueUpdates--;
+                        }
+
+                        mLastTrueValue = newData;
+                        handleUpdate();
+                    }
+
+                    @SuppressWarnings("method.invocation")
+                    @Override
+                    public void onInvalidated() {
+                        if (mPendingTrueValueUpdates > 0) {
+                            mPendingTrueValueUpdates--;
+                        }
+
+                        mLastTrueValue = null;
+                        handleUpdate();
+                    }
+                };
+
+        mFalseValueIncomingCallback =
+                new DynamicTypeValueReceiver<T>() {
+                    @Override
+                    public void onPreUpdate() {
+                        mPendingFalseValueUpdates++;
+
+                        if (mPendingTrueValueUpdates == 0
+                                && mPendingFalseValueUpdates == 1
+                                && mPendingConditionalUpdates == 0) {
+                            mDownstream.onPreUpdate();
+                        }
+                    }
+
+                    @SuppressWarnings("method.invocation")
+                    @Override
+                    public void onData(@NonNull T newData) {
+                        if (mPendingFalseValueUpdates > 0) {
+                            mPendingFalseValueUpdates--;
+                        }
+
+                        mLastFalseValue = newData;
+                        handleUpdate();
+                    }
+
+                    @SuppressWarnings("method.invocation")
+                    @Override
+                    public void onInvalidated() {
+                        if (mPendingFalseValueUpdates > 0) {
+                            mPendingFalseValueUpdates--;
+                        }
+
+                        mLastFalseValue = null;
+                        handleUpdate();
+                    }
+                };
+
+        mConditionIncomingCallback =
+                new DynamicTypeValueReceiver<Boolean>() {
+                    @Override
+                    public void onPreUpdate() {
+                        mPendingConditionalUpdates++;
+
+                        if (mPendingTrueValueUpdates == 0
+                                && mPendingFalseValueUpdates == 0
+                                && mPendingConditionalUpdates == 1) {
+                            mDownstream.onPreUpdate();
+                        }
+                    }
+
+                    @SuppressWarnings("method.invocation")
+                    @Override
+                    public void onData(@NonNull Boolean newData) {
+                        if (mPendingConditionalUpdates > 0) {
+                            mPendingConditionalUpdates--;
+                        }
+
+                        mLastConditional = newData;
+                        handleUpdate();
+                    }
+
+                    @SuppressWarnings("method.invocation")
+                    @Override
+                    public void onInvalidated() {
+                        if (mPendingConditionalUpdates > 0) {
+                            mPendingConditionalUpdates--;
+                        }
+
+                        mLastConditional = null;
+                        handleUpdate();
+                    }
+                };
+    }
+
+    public DynamicTypeValueReceiver<T> getTrueValueIncomingCallback() {
+        return mTrueValueIncomingCallback;
+    }
+
+    public DynamicTypeValueReceiver<T> getFalseValueIncomingCallback() {
+        return mFalseValueIncomingCallback;
+    }
+
+    public DynamicTypeValueReceiver<Boolean> getConditionIncomingCallback() {
+        return mConditionIncomingCallback;
+    }
+
+    void handleUpdate() {
+        if (mPendingTrueValueUpdates > 0
+                || mPendingFalseValueUpdates > 0
+                || mPendingConditionalUpdates > 0) {
+            return;
+        }
+
+        if (mLastTrueValue == null || mLastFalseValue == null || mLastConditional == null) {
+            mDownstream.onInvalidated();
+            return;
+        }
+
+        if (mLastConditional) {
+            mDownstream.onData(mLastTrueValue);
+        } else {
+            mDownstream.onData(mLastFalseValue);
+        }
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataBiTransformNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataBiTransformNode.java
new file mode 100644
index 0000000..8bf5274
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataBiTransformNode.java
@@ -0,0 +1,167 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+/**
+ * Dynamic data node that can perform a transformation from two upstream nodes. This should be
+ * created by passing a {@link Function} in, which implements the transformation.
+ *
+ * <p>The two inputs to this are called the left/right-hand side of the operation, since many of the
+ * operations extending this class are likely to be simple maths operations. Conventionally then,
+ * descendants of this class will implement operations of the form "O = LHS [op] RHS", or "O =
+ * op(LHS, RHS)".
+ *
+ * @param <LhsT> The source data type for the left-hand side of the operation.
+ * @param <RhsT> The source data type for the right-hand side of the operation.
+ * @param <O> The data type that this node emits.
+ */
+class DynamicDataBiTransformNode<LhsT, RhsT, O> implements DynamicDataNode<O> {
+    private static final String TAG = "DynamicDataBiTransform";
+
+    private final DynamicTypeValueReceiver<LhsT> mLhsIncomingCallback;
+    private final DynamicTypeValueReceiver<RhsT> mRhsIncomingCallback;
+
+    final DynamicTypeValueReceiver<O> mDownstream;
+    private final BiFunction<LhsT, RhsT, O> mTransformer;
+
+    @Nullable LhsT mCachedLhsData;
+    @Nullable RhsT mCachedRhsData;
+
+    int mPendingLhsStateUpdates = 0;
+    int mPendingRhsStateUpdates = 0;
+
+    DynamicDataBiTransformNode(
+            DynamicTypeValueReceiver<O> downstream, BiFunction<LhsT, RhsT, O> transformer) {
+        this.mDownstream = downstream;
+        this.mTransformer = transformer;
+
+        // These classes refer to handlePreStateUpdate, which is @UnderInitialization when these
+        // initializers run, and hence raise an error. It's invalid to annotate
+        // handle{Pre}StateUpdate as @UnderInitialization (since it refers to initialized fields),
+        // and moving this assignment into the constructor yields the same error (since one of the
+        // fields has to be assigned first, when the class is still under initialization).
+        //
+        // The only path to get these is via get{Lhs,Rhs}IncomingCallback, which can only be called
+        // when the class is initialized (and which also cannot be called from a sub-constructor, as
+        // that will again complain that it's calling something which is @UnderInitialization).
+        // Given that, suppressing the warning in onStateUpdate should be safe.
+        this.mLhsIncomingCallback =
+                new DynamicTypeValueReceiver<LhsT>() {
+                    @Override
+                    public void onPreUpdate() {
+                        mPendingLhsStateUpdates++;
+
+                        if (mPendingLhsStateUpdates == 1 && mPendingRhsStateUpdates == 0) {
+                            mDownstream.onPreUpdate();
+                        }
+                    }
+
+                    @SuppressWarnings("method.invocation")
+                    @Override
+                    public void onData(@NonNull LhsT newData) {
+                        onUpdatedImpl(newData);
+                    }
+
+                    private void onUpdatedImpl(@Nullable LhsT newData) {
+                        if (mPendingLhsStateUpdates == 0) {
+                            Log.w(
+                                    TAG,
+                                    "Received a state update, but one or more suppliers did not"
+                                            + " call onPreStateUpdate");
+                        } else {
+                            mPendingLhsStateUpdates--;
+                        }
+
+                        mCachedLhsData = newData;
+                        handleStateUpdate();
+                    }
+
+                    @Override
+                    public void onInvalidated() {
+                        // Note: Casts are required here to help out the null checker.
+                        onUpdatedImpl((LhsT) null);
+                    }
+                };
+
+        this.mRhsIncomingCallback =
+                new DynamicTypeValueReceiver<RhsT>() {
+                    @Override
+                    public void onPreUpdate() {
+                        mPendingRhsStateUpdates++;
+
+                        if (mPendingLhsStateUpdates == 0 && mPendingRhsStateUpdates == 1) {
+                            mDownstream.onPreUpdate();
+                        }
+                    }
+
+                    @SuppressWarnings("method.invocation")
+                    @Override
+                    public void onData(@NonNull RhsT newData) {
+                        onUpdatedImpl(newData);
+                    }
+
+                    private void onUpdatedImpl(@Nullable RhsT newData) {
+                        if (mPendingRhsStateUpdates == 0) {
+                            Log.w(
+                                    TAG,
+                                    "Received a state update, but one or more suppliers did not"
+                                            + " call onPreStateUpdate");
+                        } else {
+                            mPendingRhsStateUpdates--;
+                        }
+
+                        mCachedRhsData = newData;
+                        handleStateUpdate();
+                    }
+
+                    @Override
+                    public void onInvalidated() {
+                        onUpdatedImpl((RhsT) null);
+                    }
+                };
+    }
+
+    void handleStateUpdate() {
+        if (mPendingLhsStateUpdates == 0 && mPendingRhsStateUpdates == 0) {
+            LhsT lhs = mCachedLhsData;
+            RhsT rhs = mCachedRhsData;
+
+            if (lhs == null || rhs == null) {
+                mDownstream.onInvalidated();
+            } else {
+                O result = mTransformer.apply(lhs, rhs);
+                mDownstream.onData(result);
+            }
+        }
+    }
+
+    public DynamicTypeValueReceiver<LhsT> getLhsIncomingCallback() {
+        return mLhsIncomingCallback;
+    }
+
+    public DynamicTypeValueReceiver<RhsT> getRhsIncomingCallback() {
+        return mRhsIncomingCallback;
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataNode.java
new file mode 100644
index 0000000..a454577
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataNode.java
@@ -0,0 +1,59 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+/**
+ * Node within a dynamic data pipeline.
+ *
+ * <p>Each node should either be a {@link DynamicDataSourceNode}, in which case it pushes data into
+ * the pipeline, or it should expose one or more callbacks (generally instances of {@link
+ * DynamicTypeValueReceiver}), which can be used by an upstream node to "push" data through the
+ * pipeline. A node would typically look like the following:
+ *
+ * <pre>{@code
+ * class IntToStringNode implements DynamicDataNode<String> {
+ *   // The consumer on the downstream node to push data to.
+ *   private final DynamicTypeValueReceiver<String> downstreamNode;
+ *
+ *   private final DynamicTypeValueReceiver<Integer> myNode =
+ *     new DynamicTypeValueReceiver<Integer>() {
+ *       @Override
+ *       public void onPreStateUpdate() {
+ *         // Don't need to do anything here; just relay.
+ *         downstreamNode.onPreStateUpdate();
+ *       }
+ *
+ *       @Override
+ *       public void onStateUpdate(Integer newData) {
+ *         downstreamNode.onStateUpdate(newData.toString());
+ *       }
+ *     };
+ *
+ *   public DynamicTypeValueReceiver<Integer> getConsumer() { return myNode; }
+ * }
+ * }</pre>
+ *
+ * An upstream node (i.e. one which yields an Integer) would then push data in to this node, via the
+ * consumer returned from {@code IntToStringNode#getConsumer}.
+ *
+ * <p>Generally, node implementations will not use this interface directly; {@link
+ * DynamicDataTransformNode} and {@link DynamicDataBiTransformNode} provide canonical
+ * implementations for transforming data pushed from one or two source nodes, to a downstream node.
+ *
+ * @param <O> The data type that this node yields.
+ */
+interface DynamicDataNode<O> {}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataSourceNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataSourceNode.java
new file mode 100644
index 0000000..7e770dc
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataSourceNode.java
@@ -0,0 +1,47 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.UiThread;
+
+/**
+ * Data source node within a dynamic data pipeline. This node type should push data into the
+ * pipeline, either once when the pipeline is inited, or periodically from its data source (e.g. for
+ * sensor nodes).
+ *
+ * @param <T> The type of data this node emits.
+ */
+interface DynamicDataSourceNode<T> extends DynamicDataNode<T> {
+    /**
+     * Called on all source nodes before {@link DynamicDataSourceNode#init()} is called on any node.
+     * This should generally only call {@link DynamicTypeValueReceiver#onPreUpdate()} on all
+     * downstream nodes.
+     */
+    @UiThread
+    void preInit();
+
+    /**
+     * Initialize this node. This should cause it to bind to any data sources, and emit its first
+     * value.
+     */
+    @UiThread
+    void init();
+
+    /** Destroy this node. This should cause it to unbind from any data sources. */
+    @UiThread
+    void destroy();
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataTransformNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataTransformNode.java
new file mode 100644
index 0000000..f9e476a
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataTransformNode.java
@@ -0,0 +1,64 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.NonNull;
+
+import java.util.function.Function;
+
+/**
+ * Dynamic data node that can perform a transformation from an upstream node. This should be created
+ * by passing a {@link Function} in, which implements the transformation
+ *
+ * @param <I> The source data type of this node.
+ * @param <O> The data type that this node emits.
+ */
+class DynamicDataTransformNode<I, O> implements DynamicDataNode<O> {
+    private final DynamicTypeValueReceiver<I> mCallback;
+
+    final DynamicTypeValueReceiver<O> mDownstream;
+    final Function<I, O> mTransformer;
+
+    DynamicDataTransformNode(DynamicTypeValueReceiver<O> downstream, Function<I, O> transformer) {
+        this.mDownstream = downstream;
+        this.mTransformer = transformer;
+
+        mCallback =
+                new DynamicTypeValueReceiver<I>() {
+                    @Override
+                    public void onPreUpdate() {
+                        // Don't need to do anything here; just relay.
+                        mDownstream.onPreUpdate();
+                    }
+
+                    @Override
+                    public void onData(@NonNull I newData) {
+                        O result = mTransformer.apply(newData);
+                        mDownstream.onData(result);
+                    }
+
+                    @Override
+                    public void onInvalidated() {
+                        mDownstream.onInvalidated();
+                    }
+                };
+    }
+
+    public DynamicTypeValueReceiver<I> getIncomingCallback() {
+        return mCallback;
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
new file mode 100644
index 0000000..35936c3
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
@@ -0,0 +1,985 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import android.icu.util.ULocale;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.UiThread;
+import androidx.wear.protolayout.expression.DynamicBuilders;
+import androidx.wear.protolayout.expression.pipeline.BoolNodes.ComparisonFloatNode;
+import androidx.wear.protolayout.expression.pipeline.BoolNodes.ComparisonInt32Node;
+import androidx.wear.protolayout.expression.pipeline.BoolNodes.FixedBoolNode;
+import androidx.wear.protolayout.expression.pipeline.BoolNodes.LogicalBoolOp;
+import androidx.wear.protolayout.expression.pipeline.BoolNodes.NotBoolOp;
+import androidx.wear.protolayout.expression.pipeline.BoolNodes.StateBoolNode;
+import androidx.wear.protolayout.expression.pipeline.ColorNodes.AnimatableFixedColorNode;
+import androidx.wear.protolayout.expression.pipeline.ColorNodes.DynamicAnimatedColorNode;
+import androidx.wear.protolayout.expression.pipeline.ColorNodes.FixedColorNode;
+import androidx.wear.protolayout.expression.pipeline.ColorNodes.StateColorSourceNode;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.AnimatableFixedFloatNode;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.ArithmeticFloatNode;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.DynamicAnimatedFloatNode;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.FixedFloatNode;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.Int32ToFloatNode;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.StateFloatNode;
+import androidx.wear.protolayout.expression.pipeline.Int32Nodes.ArithmeticInt32Node;
+import androidx.wear.protolayout.expression.pipeline.Int32Nodes.FixedInt32Node;
+import androidx.wear.protolayout.expression.pipeline.Int32Nodes.FloatToInt32Node;
+import androidx.wear.protolayout.expression.pipeline.Int32Nodes.PlatformInt32SourceNode;
+import androidx.wear.protolayout.expression.pipeline.Int32Nodes.StateInt32SourceNode;
+import androidx.wear.protolayout.expression.pipeline.PlatformDataSources.EpochTimePlatformDataSource;
+import androidx.wear.protolayout.expression.pipeline.PlatformDataSources.SensorGatewayPlatformDataSource;
+import androidx.wear.protolayout.expression.pipeline.StringNodes.FixedStringNode;
+import androidx.wear.protolayout.expression.pipeline.StringNodes.FloatFormatNode;
+import androidx.wear.protolayout.expression.pipeline.StringNodes.Int32FormatNode;
+import androidx.wear.protolayout.expression.pipeline.StringNodes.StateStringNode;
+import androidx.wear.protolayout.expression.pipeline.StringNodes.StringConcatOpNode;
+import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway;
+import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableDynamicColor;
+import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableDynamicFloat;
+import androidx.wear.protolayout.expression.proto.DynamicProto.ConditionalFloatOp;
+import androidx.wear.protolayout.expression.proto.DynamicProto.ConditionalInt32Op;
+import androidx.wear.protolayout.expression.proto.DynamicProto.ConditionalStringOp;
+import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicBool;
+import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicColor;
+import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicFloat;
+import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicInt32;
+import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicString;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Evaluates protolayout dynamic types.
+ *
+ * <p>Given a dynamic ProtoLayout data source, this builds up a sequence of {@link DynamicDataNode}
+ * instances, which can source the required data, and transform it into its final form.
+ *
+ * <p>Data source can includes animations which will then emit value transitions.
+ *
+ * <p>In order to evaluate dynamic types, the caller needs to add any number of pending dynamic
+ * types with {@link #bind} methods and then call {@link #processPendingBindings()} to start
+ * evaluation on those dynamic types. Starting evaluation can be done for batches of dynamic types.
+ *
+ * <p>It's the callers responsibility to destroy those dynamic types after use, with {@link
+ * BoundDynamicType#close()}.
+ */
+public class DynamicTypeEvaluator implements AutoCloseable {
+    private static final String TAG = "DynamicTypeEvaluator";
+
+    @Nullable private final SensorGateway mSensorGateway;
+    @Nullable private final SensorGatewayPlatformDataSource mSensorGatewayDataSource;
+    @NonNull private final TimeGatewayImpl mTimeGateway;
+    @Nullable private final EpochTimePlatformDataSource mTimeDataSource;
+    @NonNull private final ObservableStateStore mStateStore;
+    private final boolean mEnableAnimations;
+    @NonNull private final QuotaManager mAnimationQuotaManager;
+    @NonNull private final List<DynamicDataNode<?>> mDynamicTypeNodes = new ArrayList<>();
+
+    @NonNull
+    private static final QuotaManager DISABLED_ANIMATIONS_QUOTA_MANAGER =
+            new QuotaManager() {
+                @Override
+                public boolean tryAcquireQuota(int quotaNum) {
+                    return false;
+                }
+
+                @Override
+                public void releaseQuota(int quotaNum) {
+                    throw new IllegalStateException(
+                            "releaseQuota method is called when no quota is acquired!");
+                }
+            };
+
+    /**
+     * Creates a {@link DynamicTypeEvaluator} without animation support.
+     *
+     * @param platformDataSourcesInitiallyEnabled Whether sending updates from sensor and time
+     *     sources should be allowed initially. After that, enabling updates from sensor and time
+     *     sources can be done via {@link #enablePlatformDataSources()} or {@link
+     *     #disablePlatformDataSources()}.
+     * @param sensorGateway The gateway for sensor data.
+     * @param stateStore The state store that will be used for dereferencing the state keys in the
+     *     dynamic types.
+     */
+    public DynamicTypeEvaluator(
+            boolean platformDataSourcesInitiallyEnabled,
+            @Nullable SensorGateway sensorGateway,
+            @NonNull ObservableStateStore stateStore) {
+        // Build pipeline with quota that doesn't allow any animations.
+        this(
+                platformDataSourcesInitiallyEnabled,
+                sensorGateway,
+                stateStore,
+                /* enableAnimations= */ false,
+                DISABLED_ANIMATIONS_QUOTA_MANAGER);
+    }
+
+    /**
+     * Creates a {@link DynamicTypeEvaluator} with animation support. Maximum number of concurrently
+     * running animations is defined in the given {@link QuotaManager}. Passing in animatable data
+     * source to any of the methods will emit value transitions, for example animatable float from 5
+     * to 10 will emit all values between those numbers (i.e. 5, 6, 7, 8, 9, 10).
+     *
+     * @param platformDataSourcesInitiallyEnabled Whether sending updates from sensor and time
+     *     sources should be allowed initially. After that, enabling updates from sensor and time
+     *     sources can be done via {@link #enablePlatformDataSources()} or {@link
+     *     #disablePlatformDataSources()}.
+     * @param sensorGateway The gateway for sensor data.
+     * @param stateStore The state store that will be used for dereferencing the state keys in the
+     *     dynamic types.
+     * @param animationQuotaManager The quota manager used for limiting the number of concurrently
+     *     running animations.
+     */
+    public DynamicTypeEvaluator(
+            boolean platformDataSourcesInitiallyEnabled,
+            @Nullable SensorGateway sensorGateway,
+            @NonNull ObservableStateStore stateStore,
+            @NonNull QuotaManager animationQuotaManager) {
+        this(
+                platformDataSourcesInitiallyEnabled,
+                sensorGateway,
+                stateStore,
+                /* enableAnimations= */ true,
+                animationQuotaManager);
+    }
+
+    /**
+     * Creates a {@link DynamicTypeEvaluator}.
+     *
+     * @param platformDataSourcesInitiallyEnabled Whether sending updates from sensor and time
+     *     sources should be allowed initially. After that, enabling updates from sensor and time
+     *     sources can be done via {@link #enablePlatformDataSources()} or {@link
+     *     #disablePlatformDataSources()}.
+     * @param sensorGateway The gateway for sensor data.
+     * @param stateStore The state store that will be used for dereferencing the state keys in the
+     *     dynamic types.
+     * @param animationQuotaManager The quota manager used for limiting the number of concurrently
+     *     running animations.
+     */
+    private DynamicTypeEvaluator(
+            boolean platformDataSourcesInitiallyEnabled,
+            @Nullable SensorGateway sensorGateway,
+            @NonNull ObservableStateStore stateStore,
+            boolean enableAnimations,
+            @NonNull QuotaManager animationQuotaManager) {
+
+        this.mSensorGateway = sensorGateway;
+        Handler uiHandler = new Handler(Looper.getMainLooper());
+        Executor uiExecutor = new MainThreadExecutor(uiHandler);
+        if (this.mSensorGateway != null) {
+            if (platformDataSourcesInitiallyEnabled) {
+                this.mSensorGateway.enableUpdates();
+            } else {
+                this.mSensorGateway.disableUpdates();
+            }
+            this.mSensorGatewayDataSource =
+                    new SensorGatewayPlatformDataSource(uiExecutor, this.mSensorGateway);
+        } else {
+            this.mSensorGatewayDataSource = null;
+        }
+
+        this.mTimeGateway = new TimeGatewayImpl(uiHandler, platformDataSourcesInitiallyEnabled);
+        this.mTimeDataSource = new EpochTimePlatformDataSource(uiExecutor, mTimeGateway);
+
+        this.mEnableAnimations = enableAnimations;
+        this.mStateStore = stateStore;
+        this.mAnimationQuotaManager = animationQuotaManager;
+    }
+
+    /**
+     * Starts evaluating all stored pending dynamic types.
+     *
+     * <p>This needs to be called when new pending dynamic types are added via any {@code bind}
+     * method, either when one or a batch is added.
+     *
+     * <p>Any pending dynamic type will be initialized for evaluation. All other already initialized
+     * dynamic types will remain unaffected.
+     *
+     * <p>It's the callers responsibility to destroy those dynamic types after use, with {@link
+     * BoundDynamicType#close()}.
+     *
+     * @hide
+     */
+    @UiThread
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public void processPendingBindings() {
+        processBindings(mDynamicTypeNodes);
+
+        // This method empties the array with dynamic type nodes.
+        clearDynamicTypesArray();
+    }
+
+    @UiThread
+    private static void processBindings(List<DynamicDataNode<?>> bindings) {
+        preInitNodes(bindings);
+        initNodes(bindings);
+    }
+
+    /**
+     * Removes any stored pending bindings by clearing the list that stores them. Note that this
+     * doesn't destroy them.
+     */
+    @UiThread
+    private void clearDynamicTypesArray() {
+        mDynamicTypeNodes.clear();
+    }
+
+    /** This should be called before initNodes() */
+    @UiThread
+    private static void preInitNodes(List<DynamicDataNode<?>> bindings) {
+        bindings.stream()
+                .filter(n -> n instanceof DynamicDataSourceNode)
+                .forEach(n -> ((DynamicDataSourceNode<?>) n).preInit());
+    }
+
+    @UiThread
+    private static void initNodes(List<DynamicDataNode<?>> bindings) {
+        bindings.stream()
+                .filter(n -> n instanceof DynamicDataSourceNode)
+                .forEach(n -> ((DynamicDataSourceNode<?>) n).init());
+    }
+
+    /**
+     * Adds dynamic type from the given {@link DynamicBuilders.DynamicString} for evaluation.
+     * Evaluation will start immediately.
+     *
+     * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+     *
+     * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+     * by caller, results of evaluation will be sent through the given {@link
+     * DynamicTypeValueReceiver}.
+     *
+     * @param stringSource The given String dynamic type that should be evaluated.
+     * @param consumer The registered consumer for results of the evaluation. It will be called from
+     *     UI thread.
+     * @param locale The locale used for the given String source.
+     */
+    @NonNull
+    public BoundDynamicType bind(
+            @NonNull DynamicBuilders.DynamicString stringSource,
+            @NonNull ULocale locale,
+            @NonNull DynamicTypeValueReceiver<String> consumer) {
+        List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+        bindRecursively(stringSource.toDynamicStringProto(), consumer, locale, resultBuilder);
+        processBindings(resultBuilder);
+        return new BoundDynamicTypeImpl(resultBuilder);
+    }
+
+    /**
+     * Adds pending dynamic type from the given {@link DynamicString} for future evaluation.
+     *
+     * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+     *
+     * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+     * by caller, results of evaluation will be sent through the given {@link
+     * DynamicTypeValueReceiver}.
+     *
+     * @param stringSource The given String dynamic type that should be evaluated.
+     * @param consumer The registered consumer for results of the evaluation. It will be called from
+     *     UI thread.
+     * @param locale The locale used for the given String source.
+     * @hide
+     */
+    @NonNull
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public BoundDynamicType bind(
+            @NonNull DynamicString stringSource,
+            @NonNull ULocale locale,
+            @NonNull DynamicTypeValueReceiver<String> consumer) {
+        List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+        bindRecursively(stringSource, consumer, locale, resultBuilder);
+        mDynamicTypeNodes.addAll(resultBuilder);
+        return new BoundDynamicTypeImpl(resultBuilder);
+    }
+
+    /**
+     * Adds dynamic type from the given {@link DynamicBuilders.DynamicInt32} for evaluation.
+     * Evaluation will start immediately.
+     *
+     * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+     *
+     * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+     * by caller, results of evaluation will be sent through the given {@link
+     * DynamicTypeValueReceiver}.
+     *
+     * @param int32Source The given integer dynamic type that should be evaluated.
+     * @param consumer The registered consumer for results of the evaluation. It will be called from
+     *     UI thread.
+     */
+    @NonNull
+    public BoundDynamicType bind(
+            @NonNull DynamicBuilders.DynamicInt32 int32Source,
+            @NonNull DynamicTypeValueReceiver<Integer> consumer) {
+        List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+        bindRecursively(int32Source.toDynamicInt32Proto(), consumer, resultBuilder);
+        processBindings(resultBuilder);
+        return new BoundDynamicTypeImpl(resultBuilder);
+    }
+
+    /**
+     * Adds pending dynamic type from the given {@link DynamicInt32} for future evaluation.
+     *
+     * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+     *
+     * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+     * by caller, results of evaluation will be sent through the given {@link
+     * DynamicTypeValueReceiver}.
+     *
+     * @param int32Source The given integer dynamic type that should be evaluated.
+     * @param consumer The registered consumer for results of the evaluation. It will be called from
+     *     UI thread.
+     * @hide
+     */
+    @NonNull
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public BoundDynamicType bind(
+            @NonNull DynamicInt32 int32Source,
+            @NonNull DynamicTypeValueReceiver<Integer> consumer) {
+        List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+        bindRecursively(int32Source, consumer, resultBuilder);
+        mDynamicTypeNodes.addAll(resultBuilder);
+        return new BoundDynamicTypeImpl(resultBuilder);
+    }
+
+    /**
+     * Adds dynamic type from the given {@link DynamicBuilders.DynamicFloat} for evaluation.
+     * Evaluation will start immediately.
+     *
+     * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+     *
+     * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+     * by caller, results of evaluation will be sent through the given {@link
+     * DynamicTypeValueReceiver}.
+     *
+     * @param floatSource The given float dynamic type that should be evaluated.
+     * @param consumer The registered consumer for results of the evaluation. It will be called from
+     *     UI thread.
+     */
+    @NonNull
+    public BoundDynamicType bind(
+            @NonNull DynamicBuilders.DynamicFloat floatSource,
+            @NonNull DynamicTypeValueReceiver<Float> consumer) {
+        List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+        bindRecursively(floatSource.toDynamicFloatProto(), consumer, resultBuilder);
+        processBindings(resultBuilder);
+        return new BoundDynamicTypeImpl(resultBuilder);
+    }
+
+    /**
+     * Adds pending dynamic type from the given {@link DynamicFloat} for future evaluation.
+     *
+     * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+     *
+     * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+     * by caller, results of evaluation will be sent through the given {@link
+     * DynamicTypeValueReceiver}.
+     *
+     * @param floatSource The given float dynamic type that should be evaluated.
+     * @param consumer The registered consumer for results of the evaluation. It will be called from
+     *     UI thread.
+     * @hide
+     */
+    @NonNull
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public BoundDynamicType bind(
+            @NonNull DynamicFloat floatSource, @NonNull DynamicTypeValueReceiver<Float> consumer) {
+        List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+        bindRecursively(floatSource, consumer, resultBuilder);
+        mDynamicTypeNodes.addAll(resultBuilder);
+        return new BoundDynamicTypeImpl(resultBuilder);
+    }
+
+    /**
+     * Adds dynamic type from the given {@link DynamicBuilders.DynamicColor} for evaluation.
+     * Evaluation will start immediately.
+     *
+     * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+     *
+     * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+     * by caller, results of evaluation will be sent through the given {@link
+     * DynamicTypeValueReceiver}.
+     *
+     * @param colorSource The given color dynamic type that should be evaluated.
+     * @param consumer The registered consumer for results of the evaluation. It will be called from
+     *     UI thread.
+     */
+    @NonNull
+    public BoundDynamicType bind(
+            @NonNull DynamicBuilders.DynamicColor colorSource,
+            @NonNull DynamicTypeValueReceiver<Integer> consumer) {
+        List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+        bindRecursively(colorSource.toDynamicColorProto(), consumer, resultBuilder);
+        processBindings(resultBuilder);
+        return new BoundDynamicTypeImpl(resultBuilder);
+    }
+
+    /**
+     * Adds pending dynamic type from the given {@link DynamicColor} for future evaluation.
+     *
+     * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+     *
+     * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+     * by caller, results of evaluation will be sent through the given {@link
+     * DynamicTypeValueReceiver}.
+     *
+     * @param colorSource The given color dynamic type that should be evaluated.
+     * @param consumer The registered consumer for results of the evaluation. It will be called from
+     *     UI thread.
+     * @hide
+     */
+    @NonNull
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public BoundDynamicType bind(
+            @NonNull DynamicColor colorSource,
+            @NonNull DynamicTypeValueReceiver<Integer> consumer) {
+        List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+        bindRecursively(colorSource, consumer, resultBuilder);
+        mDynamicTypeNodes.addAll(resultBuilder);
+        return new BoundDynamicTypeImpl(resultBuilder);
+    }
+
+    /**
+     * Adds dynamic type from the given {@link DynamicBuilders.DynamicBool} for evaluation.
+     * Evaluation will start immediately.
+     *
+     * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+     *
+     * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+     * by caller, results of evaluation will be sent through the given {@link
+     * DynamicTypeValueReceiver}.
+     *
+     * @param boolSource The given boolean dynamic type that should be evaluated.
+     * @param consumer The registered consumer for results of the evaluation. It will be called from
+     *     UI thread.
+     */
+    @NonNull
+    public BoundDynamicType bind(
+            @NonNull DynamicBuilders.DynamicBool boolSource,
+            @NonNull DynamicTypeValueReceiver<Boolean> consumer) {
+        List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+        bindRecursively(boolSource.toDynamicBoolProto(), consumer, resultBuilder);
+        processBindings(resultBuilder);
+        return new BoundDynamicTypeImpl(resultBuilder);
+    }
+
+    /**
+     * Adds pending dynamic type from the given {@link DynamicBool} for future evaluation.
+     *
+     * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+     *
+     * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+     * by caller, results of evaluation will be sent through the given {@link
+     * DynamicTypeValueReceiver}.
+     *
+     * @param boolSource The given boolean dynamic type that should be evaluated.
+     * @param consumer The registered consumer for results of the evaluation. It will be called from
+     *     UI thread.
+     * @hide
+     */
+    @NonNull
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public BoundDynamicType bind(
+            @NonNull DynamicBool boolSource, @NonNull DynamicTypeValueReceiver<Boolean> consumer) {
+        List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+        bindRecursively(boolSource, consumer, resultBuilder);
+        mDynamicTypeNodes.addAll(resultBuilder);
+        return new BoundDynamicTypeImpl(resultBuilder);
+    }
+
+    /**
+     * Same as {@link #bind(DynamicBuilders.DynamicString, ULocale, DynamicTypeValueReceiver)}, but
+     * instead of returning one {@link BoundDynamicType}, all {@link DynamicDataNode} produced by
+     * evaluating given dynamic type are added to the given list.
+     */
+    private void bindRecursively(
+            @NonNull DynamicString stringSource,
+            @NonNull DynamicTypeValueReceiver<String> consumer,
+            @NonNull ULocale locale,
+            @NonNull List<DynamicDataNode<?>> resultBuilder) {
+        DynamicDataNode<?> node;
+
+        switch (stringSource.getInnerCase()) {
+            case FIXED:
+                node = new FixedStringNode(stringSource.getFixed(), consumer);
+                break;
+            case INT32_FORMAT_OP:
+                {
+                    NumberFormatter formatter =
+                            new NumberFormatter(stringSource.getInt32FormatOp(), locale);
+                    Int32FormatNode int32FormatNode = new Int32FormatNode(formatter, consumer);
+                    node = int32FormatNode;
+                    bindRecursively(
+                            stringSource.getInt32FormatOp().getInput(),
+                            int32FormatNode.getIncomingCallback(),
+                            resultBuilder);
+                    break;
+                }
+            case FLOAT_FORMAT_OP:
+                {
+                    NumberFormatter formatter =
+                            new NumberFormatter(stringSource.getFloatFormatOp(), locale);
+                    FloatFormatNode floatFormatNode = new FloatFormatNode(formatter, consumer);
+                    node = floatFormatNode;
+                    bindRecursively(
+                            stringSource.getFloatFormatOp().getInput(),
+                            floatFormatNode.getIncomingCallback(),
+                            resultBuilder);
+                    break;
+                }
+            case STATE_SOURCE:
+                {
+                    node =
+                            new StateStringNode(
+                                    mStateStore, stringSource.getStateSource(), consumer);
+                    break;
+                }
+            case CONDITIONAL_OP:
+                {
+                    ConditionalOpNode<String> conditionalNode = new ConditionalOpNode<>(consumer);
+
+                    ConditionalStringOp op = stringSource.getConditionalOp();
+                    bindRecursively(
+                            op.getCondition(),
+                            conditionalNode.getConditionIncomingCallback(),
+                            resultBuilder);
+                    bindRecursively(
+                            op.getValueIfTrue(),
+                            conditionalNode.getTrueValueIncomingCallback(),
+                            locale,
+                            resultBuilder);
+                    bindRecursively(
+                            op.getValueIfFalse(),
+                            conditionalNode.getFalseValueIncomingCallback(),
+                            locale,
+                            resultBuilder);
+
+                    node = conditionalNode;
+                    break;
+                }
+            case CONCAT_OP:
+                {
+                    StringConcatOpNode concatNode = new StringConcatOpNode(consumer);
+                    node = concatNode;
+                    bindRecursively(
+                            stringSource.getConcatOp().getInputLhs(),
+                            concatNode.getLhsIncomingCallback(),
+                            locale,
+                            resultBuilder);
+                    bindRecursively(
+                            stringSource.getConcatOp().getInputRhs(),
+                            concatNode.getRhsIncomingCallback(),
+                            locale,
+                            resultBuilder);
+                    break;
+                }
+            case INNER_NOT_SET:
+                throw new IllegalArgumentException("DynamicString has no inner source set");
+            default:
+                throw new IllegalArgumentException("Unknown DynamicString source type");
+        }
+
+        resultBuilder.add(node);
+    }
+
+    /**
+     * Same as {@link #bind(DynamicBuilders.DynamicInt32, DynamicTypeValueReceiver)}, all {@link
+     * DynamicDataNode} produced by evaluating given dynamic type are added to the given list.
+     */
+    private void bindRecursively(
+            @NonNull DynamicInt32 int32Source,
+            @NonNull DynamicTypeValueReceiver<Integer> consumer,
+            @NonNull List<DynamicDataNode<?>> resultBuilder) {
+        DynamicDataNode<Integer> node;
+
+        switch (int32Source.getInnerCase()) {
+            case FIXED:
+                node = new FixedInt32Node(int32Source.getFixed(), consumer);
+                break;
+            case PLATFORM_SOURCE:
+                node =
+                        new PlatformInt32SourceNode(
+                                int32Source.getPlatformSource(),
+                                mTimeDataSource,
+                                mSensorGatewayDataSource,
+                                consumer);
+                break;
+            case ARITHMETIC_OPERATION:
+                {
+                    ArithmeticInt32Node arithmeticNode =
+                            new ArithmeticInt32Node(int32Source.getArithmeticOperation(), consumer);
+                    node = arithmeticNode;
+
+                    bindRecursively(
+                            int32Source.getArithmeticOperation().getInputLhs(),
+                            arithmeticNode.getLhsIncomingCallback(),
+                            resultBuilder);
+                    bindRecursively(
+                            int32Source.getArithmeticOperation().getInputRhs(),
+                            arithmeticNode.getRhsIncomingCallback(),
+                            resultBuilder);
+
+                    break;
+                }
+            case STATE_SOURCE:
+                {
+                    node =
+                            new StateInt32SourceNode(
+                                    mStateStore, int32Source.getStateSource(), consumer);
+                    break;
+                }
+            case CONDITIONAL_OP:
+                {
+                    ConditionalOpNode<Integer> conditionalNode = new ConditionalOpNode<>(consumer);
+
+                    ConditionalInt32Op op = int32Source.getConditionalOp();
+                    bindRecursively(
+                            op.getCondition(),
+                            conditionalNode.getConditionIncomingCallback(),
+                            resultBuilder);
+                    bindRecursively(
+                            op.getValueIfTrue(),
+                            conditionalNode.getTrueValueIncomingCallback(),
+                            resultBuilder);
+                    bindRecursively(
+                            op.getValueIfFalse(),
+                            conditionalNode.getFalseValueIncomingCallback(),
+                            resultBuilder);
+
+                    node = conditionalNode;
+                    break;
+                }
+            case FLOAT_TO_INT:
+                {
+                    FloatToInt32Node conversionNode =
+                            new FloatToInt32Node(int32Source.getFloatToInt(), consumer);
+                    node = conversionNode;
+
+                    bindRecursively(
+                            int32Source.getFloatToInt().getInput(),
+                            conversionNode.getIncomingCallback(),
+                            resultBuilder);
+                    break;
+                }
+            case INNER_NOT_SET:
+                throw new IllegalArgumentException("DynamicInt32 has no inner source set");
+            default:
+                throw new IllegalArgumentException("Unknown DynamicInt32 source type");
+        }
+
+        resultBuilder.add(node);
+    }
+
+    /**
+     * Same as {@link #bind(DynamicBuilders.DynamicFloat, DynamicTypeValueReceiver)}, all {@link
+     * DynamicDataNode} produced by evaluating given dynamic type are added to the given list.
+     */
+    private void bindRecursively(
+            @NonNull DynamicFloat floatSource,
+            @NonNull DynamicTypeValueReceiver<Float> consumer,
+            @NonNull List<DynamicDataNode<?>> resultBuilder) {
+        DynamicDataNode<?> node;
+
+        switch (floatSource.getInnerCase()) {
+            case FIXED:
+                node = new FixedFloatNode(floatSource.getFixed(), consumer);
+                break;
+            case STATE_SOURCE:
+                node =
+                        new StateFloatNode(
+                                mStateStore, floatSource.getStateSource().getSourceKey(), consumer);
+                break;
+            case ARITHMETIC_OPERATION:
+                {
+                    ArithmeticFloatNode arithmeticNode =
+                            new ArithmeticFloatNode(floatSource.getArithmeticOperation(), consumer);
+                    node = arithmeticNode;
+
+                    bindRecursively(
+                            floatSource.getArithmeticOperation().getInputLhs(),
+                            arithmeticNode.getLhsIncomingCallback(),
+                            resultBuilder);
+                    bindRecursively(
+                            floatSource.getArithmeticOperation().getInputRhs(),
+                            arithmeticNode.getRhsIncomingCallback(),
+                            resultBuilder);
+
+                    break;
+                }
+            case INT32_TO_FLOAT_OPERATION:
+                {
+                    Int32ToFloatNode toFloatNode = new Int32ToFloatNode(consumer);
+                    node = toFloatNode;
+
+                    bindRecursively(
+                            floatSource.getInt32ToFloatOperation().getInput(),
+                            toFloatNode.getIncomingCallback(),
+                            resultBuilder);
+                    break;
+                }
+            case CONDITIONAL_OP:
+                {
+                    ConditionalOpNode<Float> conditionalNode = new ConditionalOpNode<>(consumer);
+
+                    ConditionalFloatOp op = floatSource.getConditionalOp();
+                    bindRecursively(
+                            op.getCondition(),
+                            conditionalNode.getConditionIncomingCallback(),
+                            resultBuilder);
+                    bindRecursively(
+                            op.getValueIfTrue(),
+                            conditionalNode.getTrueValueIncomingCallback(),
+                            resultBuilder);
+                    bindRecursively(
+                            op.getValueIfFalse(),
+                            conditionalNode.getFalseValueIncomingCallback(),
+                            resultBuilder);
+
+                    node = conditionalNode;
+                    break;
+                }
+            case ANIMATABLE_FIXED:
+                {
+                    if (mEnableAnimations) {
+                        node =
+                                new AnimatableFixedFloatNode(
+                                        floatSource.getAnimatableFixed(),
+                                        consumer,
+                                        mAnimationQuotaManager);
+                    } else {
+                        throw new IllegalStateException(
+                                "Cannot translate static_animated_float; animations are disabled.");
+                    }
+                    break;
+                }
+            case ANIMATABLE_DYNAMIC:
+                {
+                    if (mEnableAnimations) {
+                        AnimatableDynamicFloat dynamicNode = floatSource.getAnimatableDynamic();
+                        DynamicAnimatedFloatNode animationNode =
+                                new DynamicAnimatedFloatNode(
+                                        consumer, dynamicNode.getSpec(), mAnimationQuotaManager);
+                        node = animationNode;
+
+                        bindRecursively(
+                                dynamicNode.getInput(),
+                                animationNode.getInputCallback(),
+                                resultBuilder);
+                    } else {
+                        throw new IllegalStateException(
+                                "Cannot translate dynamic_animated_float; animations are"
+                                        + " disabled.");
+                    }
+                    break;
+                }
+
+            case INNER_NOT_SET:
+                throw new IllegalArgumentException("DynamicFloat has no inner source set");
+            default:
+                throw new IllegalArgumentException("Unknown DynamicFloat source type");
+        }
+
+        resultBuilder.add(node);
+    }
+
+    /**
+     * Same as {@link #bind(DynamicBuilders.DynamicColor, DynamicTypeValueReceiver)}, all {@link
+     * DynamicDataNode} produced by evaluating given dynamic type are added to the given list.
+     */
+    private void bindRecursively(
+            @NonNull DynamicColor colorSource,
+            @NonNull DynamicTypeValueReceiver<Integer> consumer,
+            @NonNull List<DynamicDataNode<?>> resultBuilder) {
+        DynamicDataNode<?> node;
+
+        switch (colorSource.getInnerCase()) {
+            case FIXED:
+                node = new FixedColorNode(colorSource.getFixed(), consumer);
+                break;
+            case STATE_SOURCE:
+                node =
+                        new StateColorSourceNode(
+                                mStateStore, colorSource.getStateSource(), consumer);
+                break;
+            case ANIMATABLE_FIXED:
+                if (mEnableAnimations) {
+                    node =
+                            new AnimatableFixedColorNode(
+                                    colorSource.getAnimatableFixed(),
+                                    consumer,
+                                    mAnimationQuotaManager);
+                } else {
+                    throw new IllegalStateException(
+                            "Cannot translate animatable_fixed color; animations are disabled.");
+                }
+                break;
+            case ANIMATABLE_DYNAMIC:
+                if (mEnableAnimations) {
+                    AnimatableDynamicColor dynamicNode = colorSource.getAnimatableDynamic();
+                    DynamicAnimatedColorNode animationNode =
+                            new DynamicAnimatedColorNode(
+                                    consumer, dynamicNode.getSpec(), mAnimationQuotaManager);
+                    node = animationNode;
+
+                    bindRecursively(
+                            dynamicNode.getInput(),
+                            animationNode.getInputCallback(),
+                            resultBuilder);
+                } else {
+                    throw new IllegalStateException(
+                            "Cannot translate dynamic_animated_float; animations are disabled.");
+                }
+                break;
+            case INNER_NOT_SET:
+                throw new IllegalArgumentException("DynamicColor has no inner source set");
+            default:
+                throw new IllegalArgumentException("Unknown DynamicColor source type");
+        }
+
+        resultBuilder.add(node);
+    }
+
+    /**
+     * Same as {@link #bind(DynamicBuilders.DynamicBool, DynamicTypeValueReceiver)}, all {@link
+     * DynamicDataNode} produced by evaluating given dynamic type are added to the given list.
+     */
+    private void bindRecursively(
+            @NonNull DynamicBool boolSource,
+            @NonNull DynamicTypeValueReceiver<Boolean> consumer,
+            @NonNull List<DynamicDataNode<?>> resultBuilder) {
+        DynamicDataNode<?> node;
+
+        switch (boolSource.getInnerCase()) {
+            case FIXED:
+                node = new FixedBoolNode(boolSource.getFixed(), consumer);
+                break;
+            case STATE_SOURCE:
+                node = new StateBoolNode(mStateStore, boolSource.getStateSource(), consumer);
+                break;
+            case INT32_COMPARISON:
+                {
+                    ComparisonInt32Node compNode =
+                            new ComparisonInt32Node(boolSource.getInt32Comparison(), consumer);
+                    node = compNode;
+
+                    bindRecursively(
+                            boolSource.getInt32Comparison().getInputLhs(),
+                            compNode.getLhsIncomingCallback(),
+                            resultBuilder);
+                    bindRecursively(
+                            boolSource.getInt32Comparison().getInputRhs(),
+                            compNode.getRhsIncomingCallback(),
+                            resultBuilder);
+
+                    break;
+                }
+            case LOGICAL_OP:
+                {
+                    LogicalBoolOp logicalNode =
+                            new LogicalBoolOp(boolSource.getLogicalOp(), consumer);
+                    node = logicalNode;
+
+                    bindRecursively(
+                            boolSource.getLogicalOp().getInputLhs(),
+                            logicalNode.getLhsIncomingCallback(),
+                            resultBuilder);
+                    bindRecursively(
+                            boolSource.getLogicalOp().getInputRhs(),
+                            logicalNode.getRhsIncomingCallback(),
+                            resultBuilder);
+
+                    break;
+                }
+            case NOT_OP:
+                {
+                    NotBoolOp notNode = new NotBoolOp(consumer);
+                    node = notNode;
+                    bindRecursively(
+                            boolSource.getNotOp().getInput(),
+                            notNode.getIncomingCallback(),
+                            resultBuilder);
+                    break;
+                }
+            case FLOAT_COMPARISON:
+                {
+                    ComparisonFloatNode compNode =
+                            new ComparisonFloatNode(boolSource.getFloatComparison(), consumer);
+                    node = compNode;
+
+                    bindRecursively(
+                            boolSource.getFloatComparison().getInputLhs(),
+                            compNode.getLhsIncomingCallback(),
+                            resultBuilder);
+                    bindRecursively(
+                            boolSource.getFloatComparison().getInputRhs(),
+                            compNode.getRhsIncomingCallback(),
+                            resultBuilder);
+
+                    break;
+                }
+            case INNER_NOT_SET:
+                throw new IllegalArgumentException("DynamicBool has no inner source set");
+            default:
+                throw new IllegalArgumentException("Unknown DynamicBool source type");
+        }
+
+        resultBuilder.add(node);
+    }
+
+    /** Enables sending updates on sensor and time. */
+    @UiThread
+    public void enablePlatformDataSources() {
+        if (mSensorGateway != null) {
+            mSensorGateway.enableUpdates();
+        }
+
+        mTimeGateway.enableUpdates();
+    }
+
+    /** Disables sending updates on sensor and time. */
+    @UiThread
+    public void disablePlatformDataSources() {
+        if (mSensorGateway != null) {
+            mSensorGateway.disableUpdates();
+        }
+
+        mTimeGateway.disableUpdates();
+    }
+
+    /**
+     * Closes existing time gateway.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void close() {
+        try {
+            mTimeGateway.close();
+        } catch (RuntimeException ex) {
+            Log.e(TAG, "Error while cleaning up time gateway", ex);
+        }
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeValueReceiver.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeValueReceiver.java
new file mode 100644
index 0000000..f421d27
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeValueReceiver.java
@@ -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.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.UiThread;
+
+/**
+ * Callback for an evaluation result. This is intended to support two-step updates; first a
+ * notification will be sent that the evaluation result item will be updated, then the new
+ * evaluation result will be delivered. This allows downstream consumers to properly synchronize
+ * their updates if they depend on two or more evaluation result items, rather than updating
+ * multiple times (with potentially invalid states).
+ *
+ * <p>It is guaranteed that for any given batch evaluation result, {@link #onPreUpdate()} will be
+ * called on all listeners before any {@link #onData} calls are fired.
+ *
+ * @param <T> Data type.
+ */
+public interface DynamicTypeValueReceiver<T> {
+    /**
+     * Called when evaluation result for the expression that this callback was registered for is
+     * about to be updated. This allows a downstream consumer to properly synchronize updates if it
+     * depends on two or more evaluation result items. In that case, it should use this call to
+     * figure out how many of its dependencies are going to be updated, and wait for all of them to
+     * be updated (via {@link DynamicTypeValueReceiver#onData(T)}) before acting on the change.
+     *
+     * @hide
+     */
+    @UiThread
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    void onPreUpdate();
+
+    /**
+     * Called when the expression that this callback was registered for has a new evaluation result.
+     *
+     * @see DynamicTypeValueReceiver#onPreUpdate()
+     */
+    @UiThread
+    void onData(@NonNull T newData);
+
+    /** Called when the expression that this callback was registered for has an invalid result. */
+    @UiThread
+    void onInvalidated();
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java
new file mode 100644
index 0000000..73727de
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java
@@ -0,0 +1,240 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import static androidx.wear.protolayout.expression.pipeline.AnimationsHelper.applyAnimationSpecToAnimator;
+
+import android.animation.ValueAnimator;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
+import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableFixedFloat;
+import androidx.wear.protolayout.expression.proto.DynamicProto.ArithmeticFloatOp;
+import androidx.wear.protolayout.expression.proto.FixedProto.FixedFloat;
+
+/** Dynamic data nodes which yield floats. */
+class FloatNodes {
+
+    private FloatNodes() {}
+
+    /** Dynamic float node that has a fixed value. */
+    static class FixedFloatNode implements DynamicDataSourceNode<Float> {
+        private final float mValue;
+        private final DynamicTypeValueReceiver<Float> mDownstream;
+
+        FixedFloatNode(FixedFloat protoNode, DynamicTypeValueReceiver<Float> downstream) {
+            this.mValue = protoNode.getValue();
+            this.mDownstream = downstream;
+        }
+
+        @Override
+        @UiThread
+        public void preInit() {
+            mDownstream.onPreUpdate();
+        }
+
+        @Override
+        @UiThread
+        public void init() {
+            mDownstream.onData(mValue);
+        }
+
+        @Override
+        @UiThread
+        public void destroy() {}
+    }
+
+    /** Dynamic float node that gets value from the state. */
+    static class StateFloatNode extends StateSourceNode<Float> {
+        StateFloatNode(
+                ObservableStateStore observableStateStore,
+                String bindKey,
+                DynamicTypeValueReceiver<Float> downstream) {
+            super(observableStateStore, bindKey, se -> se.getFloatVal().getValue(), downstream);
+        }
+    }
+
+    /** Dynamic float node that supports arithmetic operations. */
+    static class ArithmeticFloatNode extends DynamicDataBiTransformNode<Float, Float, Float> {
+        private static final String TAG = "ArithmeticFloatNode";
+
+        ArithmeticFloatNode(
+                ArithmeticFloatOp protoNode, DynamicTypeValueReceiver<Float> downstream) {
+            super(
+                    downstream,
+                    (lhs, rhs) -> {
+                        try {
+                            switch (protoNode.getOperationType()) {
+                                case ARITHMETIC_OP_TYPE_UNDEFINED:
+                                case UNRECOGNIZED:
+                                    Log.e(TAG, "Unknown operation type in ArithmeticFloatNode");
+                                    return Float.NaN;
+                                case ARITHMETIC_OP_TYPE_ADD:
+                                    return lhs + rhs;
+                                case ARITHMETIC_OP_TYPE_SUBTRACT:
+                                    return lhs - rhs;
+                                case ARITHMETIC_OP_TYPE_MULTIPLY:
+                                    return lhs * rhs;
+                                case ARITHMETIC_OP_TYPE_DIVIDE:
+                                    return lhs / rhs;
+                                case ARITHMETIC_OP_TYPE_MODULO:
+                                    return lhs % rhs;
+                            }
+                        } catch (ArithmeticException ex) {
+                            Log.e(TAG, "ArithmeticException in ArithmeticFloatNode", ex);
+                            return Float.NaN;
+                        }
+
+                        Log.e(TAG, "Unknown operation type in ArithmeticFloatNode");
+                        return Float.NaN;
+                    });
+        }
+    }
+
+    /** Dynamic float node that gets value from INTEGER. */
+    static class Int32ToFloatNode extends DynamicDataTransformNode<Integer, Float> {
+
+        Int32ToFloatNode(DynamicTypeValueReceiver<Float> downstream) {
+            super(downstream, i -> (float) i);
+        }
+    }
+
+    /** Dynamic float node that gets animatable value from fixed source. */
+    static class AnimatableFixedFloatNode extends AnimatableNode
+            implements DynamicDataSourceNode<Float> {
+
+        private final AnimatableFixedFloat mProtoNode;
+        private final DynamicTypeValueReceiver<Float> mDownstream;
+
+        AnimatableFixedFloatNode(
+                AnimatableFixedFloat protoNode,
+                DynamicTypeValueReceiver<Float> downstream,
+                QuotaManager quotaManager) {
+            super(quotaManager);
+            this.mProtoNode = protoNode;
+            this.mDownstream = downstream;
+        }
+
+        @Override
+        @UiThread
+        public void preInit() {
+            mDownstream.onPreUpdate();
+        }
+
+        @Override
+        @UiThread
+        public void init() {
+            ValueAnimator animator =
+                    ValueAnimator.ofFloat(mProtoNode.getFromValue(), mProtoNode.getToValue());
+            animator.addUpdateListener(a -> mDownstream.onData((float) a.getAnimatedValue()));
+
+            applyAnimationSpecToAnimator(animator, mProtoNode.getSpec());
+
+            mQuotaAwareAnimator.updateAnimator(animator);
+            startOrSkipAnimator();
+        }
+
+        @Override
+        @UiThread
+        public void destroy() {
+            mQuotaAwareAnimator.stopAnimator();
+        }
+    }
+
+    /** Dynamic float node that gets animatable value from dynamic source. */
+    static class DynamicAnimatedFloatNode extends AnimatableNode implements DynamicDataNode<Float> {
+
+        final DynamicTypeValueReceiver<Float> mDownstream;
+        private final DynamicTypeValueReceiver<Float> mInputCallback;
+
+        @Nullable Float mCurrentValue = null;
+        int mPendingCalls = 0;
+
+        // Static analysis complains about calling methods of parent class AnimatableNode under
+        // initialization but mInputCallback is only used after the constructor is finished.
+        @SuppressWarnings("method.invocation.invalid")
+        DynamicAnimatedFloatNode(
+                DynamicTypeValueReceiver<Float> downstream,
+                @NonNull AnimationSpec spec,
+                QuotaManager quotaManager) {
+            super(quotaManager);
+            this.mDownstream = downstream;
+            this.mInputCallback =
+                    new DynamicTypeValueReceiver<Float>() {
+                        @Override
+                        public void onPreUpdate() {
+                            mPendingCalls++;
+
+                            if (mPendingCalls == 1) {
+                                mDownstream.onPreUpdate();
+
+                                mQuotaAwareAnimator.resetAnimator();
+                            }
+                        }
+
+                        @Override
+                        public void onData(@NonNull Float newData) {
+                            if (mPendingCalls > 0) {
+                                mPendingCalls--;
+                            }
+
+                            if (mPendingCalls == 0) {
+                                if (mCurrentValue == null) {
+                                    mCurrentValue = newData;
+                                    mDownstream.onData(mCurrentValue);
+                                } else {
+                                    ValueAnimator animator =
+                                            ValueAnimator.ofFloat(mCurrentValue, newData);
+
+                                    applyAnimationSpecToAnimator(animator, spec);
+
+                                    animator.addUpdateListener(
+                                            a -> {
+                                                if (mPendingCalls == 0) {
+                                                    mCurrentValue = (float) a.getAnimatedValue();
+                                                    mDownstream.onData(mCurrentValue);
+                                                }
+                                            });
+
+                                    mQuotaAwareAnimator.updateAnimator(animator);
+                                    startOrSkipAnimator();
+                                }
+                            }
+                        }
+
+                        @Override
+                        public void onInvalidated() {
+                            if (mPendingCalls > 0) {
+                                mPendingCalls--;
+                            }
+
+                            if (mPendingCalls == 0) {
+                                mCurrentValue = null;
+                                mDownstream.onInvalidated();
+                            }
+                        }
+                    };
+        }
+
+        public DynamicTypeValueReceiver<Float> getInputCallback() {
+            return mInputCallback;
+        }
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
new file mode 100644
index 0000000..53a7968
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
@@ -0,0 +1,206 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.wear.protolayout.expression.pipeline.PlatformDataSources.EpochTimePlatformDataSource;
+import androidx.wear.protolayout.expression.pipeline.PlatformDataSources.PlatformDataSource;
+import androidx.wear.protolayout.expression.pipeline.PlatformDataSources.SensorGatewayPlatformDataSource;
+import androidx.wear.protolayout.expression.proto.DynamicProto.ArithmeticInt32Op;
+import androidx.wear.protolayout.expression.proto.DynamicProto.FloatToInt32Op;
+import androidx.wear.protolayout.expression.proto.DynamicProto.PlatformInt32Source;
+import androidx.wear.protolayout.expression.proto.DynamicProto.PlatformInt32SourceType;
+import androidx.wear.protolayout.expression.proto.DynamicProto.StateInt32Source;
+import androidx.wear.protolayout.expression.proto.FixedProto.FixedInt32;
+
+/** Dynamic data nodes which yield integers. */
+class Int32Nodes {
+    private Int32Nodes() {}
+
+    /** Dynamic integer node that has a fixed value. */
+    static class FixedInt32Node implements DynamicDataSourceNode<Integer> {
+        private final int mValue;
+        private final DynamicTypeValueReceiver<Integer> mDownstream;
+
+        FixedInt32Node(FixedInt32 protoNode, DynamicTypeValueReceiver<Integer> downstream) {
+            this.mValue = protoNode.getValue();
+            this.mDownstream = downstream;
+        }
+
+        @Override
+        @UiThread
+        public void preInit() {
+            mDownstream.onPreUpdate();
+        }
+
+        @Override
+        @UiThread
+        public void init() {
+            mDownstream.onData(mValue);
+        }
+
+        @Override
+        @UiThread
+        public void destroy() {}
+    }
+
+    /** Dynamic integer node that gets value from the platform source. */
+    static class PlatformInt32SourceNode implements DynamicDataSourceNode<Integer> {
+        private static final String TAG = "PlatformInt32SourceNode";
+
+        @Nullable private final SensorGatewayPlatformDataSource mSensorGatewaySource;
+        @Nullable private final EpochTimePlatformDataSource mEpochTimePlatformDataSource;
+        private final PlatformInt32Source mProtoNode;
+        private final DynamicTypeValueReceiver<Integer> mDownstream;
+
+        PlatformInt32SourceNode(
+                PlatformInt32Source protoNode,
+                @Nullable EpochTimePlatformDataSource epochTimePlatformDataSource,
+                @Nullable SensorGatewayPlatformDataSource sensorGatewaySource,
+                DynamicTypeValueReceiver<Integer> downstream) {
+            this.mProtoNode = protoNode;
+            this.mEpochTimePlatformDataSource = epochTimePlatformDataSource;
+            this.mSensorGatewaySource = sensorGatewaySource;
+            this.mDownstream = downstream;
+        }
+
+        @Override
+        @UiThread
+        public void preInit() {
+            if (platformInt32SourceTypeToPlatformDataSource(mProtoNode.getSourceType()) != null) {
+                mDownstream.onPreUpdate();
+            }
+        }
+
+        @Override
+        @UiThread
+        public void init() {
+            PlatformDataSource dataSource =
+                    platformInt32SourceTypeToPlatformDataSource(mProtoNode.getSourceType());
+            if (dataSource != null) {
+                dataSource.registerForData(mProtoNode.getSourceType(), mDownstream);
+            } else {
+                mDownstream.onInvalidated();
+            }
+        }
+
+        @Override
+        @UiThread
+        public void destroy() {
+            PlatformDataSource dataSource =
+                    platformInt32SourceTypeToPlatformDataSource(mProtoNode.getSourceType());
+            if (dataSource != null) {
+                dataSource.unregisterForData(mProtoNode.getSourceType(), mDownstream);
+            }
+        }
+
+        @Nullable
+        private PlatformDataSource platformInt32SourceTypeToPlatformDataSource(
+                PlatformInt32SourceType sourceType) {
+            switch (sourceType) {
+                case UNRECOGNIZED:
+                case PLATFORM_INT32_SOURCE_TYPE_UNDEFINED:
+                    Log.w(TAG, "Unknown PlatformInt32SourceType");
+                    return null;
+                case PLATFORM_INT32_SOURCE_TYPE_CURRENT_HEART_RATE:
+                case PLATFORM_INT32_SOURCE_TYPE_DAILY_STEP_COUNT:
+                    return mSensorGatewaySource;
+                case PLATFORM_INT32_SOURCE_TYPE_EPOCH_TIME_SECONDS:
+                    return mEpochTimePlatformDataSource;
+            }
+            Log.w(TAG, "Unknown PlatformInt32SourceType");
+            return null;
+        }
+    }
+
+    /** Dynamic integer node that supports arithmetic operations. */
+    static class ArithmeticInt32Node extends DynamicDataBiTransformNode<Integer, Integer, Integer> {
+        private static final String TAG = "ArithmeticInt32Node";
+
+        ArithmeticInt32Node(
+                ArithmeticInt32Op protoNode, DynamicTypeValueReceiver<Integer> downstream) {
+            super(
+                    downstream,
+                    (lhs, rhs) -> {
+                        try {
+                            switch (protoNode.getOperationType()) {
+                                case ARITHMETIC_OP_TYPE_UNDEFINED:
+                                case UNRECOGNIZED:
+                                    Log.e(TAG, "Unknown operation type in ArithmeticInt32Node");
+                                    return 0;
+                                case ARITHMETIC_OP_TYPE_ADD:
+                                    return lhs + rhs;
+                                case ARITHMETIC_OP_TYPE_SUBTRACT:
+                                    return lhs - rhs;
+                                case ARITHMETIC_OP_TYPE_MULTIPLY:
+                                    return lhs * rhs;
+                                case ARITHMETIC_OP_TYPE_DIVIDE:
+                                    return lhs / rhs;
+                                case ARITHMETIC_OP_TYPE_MODULO:
+                                    return lhs % rhs;
+                            }
+                        } catch (ArithmeticException ex) {
+                            Log.e(TAG, "ArithmeticException in ArithmeticInt32Node", ex);
+                            return 0;
+                        }
+
+                        Log.e(TAG, "Unknown operation type in ArithmeticInt32Node");
+                        return 0;
+                    });
+        }
+    }
+
+    /** Dynamic integer node that gets value from the state. */
+    static class StateInt32SourceNode extends StateSourceNode<Integer> {
+
+        StateInt32SourceNode(
+                ObservableStateStore observableStateStore,
+                StateInt32Source protoNode,
+                DynamicTypeValueReceiver<Integer> downstream) {
+            super(
+                    observableStateStore,
+                    protoNode.getSourceKey(),
+                    se -> se.getInt32Val().getValue(),
+                    downstream);
+        }
+    }
+
+    /** Dynamic integer node that gets value from float. */
+    static class FloatToInt32Node extends DynamicDataTransformNode<Float, Integer> {
+
+        FloatToInt32Node(FloatToInt32Op protoNode, DynamicTypeValueReceiver<Integer> downstream) {
+            super(
+                    downstream,
+                    x -> {
+                        switch (protoNode.getRoundMode()) {
+                            case ROUND_MODE_UNDEFINED:
+                            case ROUND_MODE_FLOOR:
+                                return (int) Math.floor(x);
+                            case ROUND_MODE_ROUND:
+                                return Math.round(x);
+                            case ROUND_MODE_CEILING:
+                                return (int) Math.ceil(x);
+                            default:
+                                throw new IllegalArgumentException("Unknown rounding mode");
+                        }
+                    });
+        }
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/MainThreadExecutor.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/MainThreadExecutor.java
new file mode 100644
index 0000000..600bb618
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/MainThreadExecutor.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.expression.pipeline;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.concurrent.Executor;
+
+/** Implements an Executor that runs on the main thread. */
+class MainThreadExecutor implements Executor {
+    private final Handler mHandler;
+
+    MainThreadExecutor() {
+        this(new Handler(Looper.getMainLooper()));
+    }
+
+    MainThreadExecutor(Handler handler) {
+        this.mHandler = handler;
+    }
+
+    @Override
+    public void execute(Runnable r) {
+        mHandler.post(r);
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/NumberFormatter.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/NumberFormatter.java
new file mode 100644
index 0000000..d024a9e
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/NumberFormatter.java
@@ -0,0 +1,184 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import static java.lang.Math.max;
+
+import android.icu.number.IntegerWidth;
+import android.icu.number.LocalizedNumberFormatter;
+import android.icu.number.NumberFormatter.GroupingStrategy;
+import android.icu.number.Precision;
+import android.icu.text.DecimalFormat;
+import android.icu.text.NumberFormat;
+import android.icu.util.ULocale;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.wear.protolayout.expression.proto.DynamicProto.FloatFormatOp;
+import androidx.wear.protolayout.expression.proto.DynamicProto.Int32FormatOp;
+
+/** Utility to number formatting. */
+class NumberFormatter {
+
+    Formatter mFormatter;
+    private static final int DEFAULT_MIN_INTEGER_DIGITS = 1;
+    private static final int DEFAULT_MAX_FRACTION_DIGITS = 3;
+
+    private interface Formatter {
+        String format(int value);
+
+        String format(float value);
+    }
+
+    NumberFormatter(FloatFormatOp floatFormatOp, ULocale currentLocale) {
+        int minIntegerDigits =
+                floatFormatOp.hasMinIntegerDigits()
+                        ? floatFormatOp.getMinIntegerDigits()
+                        : DEFAULT_MIN_INTEGER_DIGITS;
+        int maxFractionDigits =
+                max(
+                        floatFormatOp.hasMaxFractionDigits()
+                                ? floatFormatOp.getMaxFractionDigits()
+                                : DEFAULT_MAX_FRACTION_DIGITS,
+                        floatFormatOp.getMinFractionDigits());
+        mFormatter =
+                buildFormatter(
+                        minIntegerDigits,
+                        floatFormatOp.getMinFractionDigits(),
+                        maxFractionDigits,
+                        floatFormatOp.getGroupingUsed(),
+                        currentLocale);
+    }
+
+    NumberFormatter(Int32FormatOp int32FormatOp, ULocale currentLocale) {
+        int minIntegerDigits =
+                int32FormatOp.hasMinIntegerDigits()
+                        ? int32FormatOp.getMinIntegerDigits()
+                        : DEFAULT_MIN_INTEGER_DIGITS;
+        mFormatter =
+                buildFormatter(
+                        minIntegerDigits,
+                        /* minFractionDigits= */ 0,
+                        /* maxFractionDigits= */ 0,
+                        int32FormatOp.getGroupingUsed(),
+                        currentLocale);
+    }
+
+    String format(float value) {
+        return mFormatter.format(value);
+    }
+
+    String format(int value) {
+        return mFormatter.format(value);
+    }
+
+    @RequiresApi(VERSION_CODES.R)
+    private static class Api30Impl {
+        @NonNull
+        @DoNotInline
+        static String callFormatToString(LocalizedNumberFormatter mFmt, int value) {
+            return mFmt.format(value).toString();
+        }
+
+        @NonNull
+        @DoNotInline
+        static String callFormatToString(LocalizedNumberFormatter mFmt, float value) {
+            return mFmt.format(value).toString();
+        }
+
+        @NonNull
+        @DoNotInline
+        static LocalizedNumberFormatter buildLocalizedNumberFormatter(
+                int minIntegerDigits,
+                int minFractionDigits,
+                int maxFractionDigits,
+                boolean groupingUsed,
+                ULocale currentLocale) {
+            return android.icu.number.NumberFormatter.withLocale(currentLocale)
+                    .grouping(groupingUsed ? GroupingStrategy.AUTO : GroupingStrategy.OFF)
+                    .integerWidth(IntegerWidth.zeroFillTo(minIntegerDigits))
+                    .precision(Precision.minMaxFraction(minFractionDigits, maxFractionDigits));
+        }
+    }
+
+    private static Formatter buildFormatter(
+            int minIntegerDigits,
+            int minFractionDigits,
+            int maxFractionDigits,
+            boolean groupingUsed,
+            ULocale currentLocale) {
+        if (VERSION.SDK_INT >= VERSION_CODES.R) {
+            return new Formatter() {
+                final LocalizedNumberFormatter mFmt =
+                        Api30Impl.buildLocalizedNumberFormatter(
+                                minIntegerDigits,
+                                minFractionDigits,
+                                maxFractionDigits,
+                                groupingUsed,
+                                currentLocale);
+
+                @Override
+                public String format(int value) {
+                    return Api30Impl.callFormatToString(mFmt, value);
+                }
+
+                @Override
+                public String format(float value) {
+                    return Api30Impl.callFormatToString(mFmt, value);
+                }
+            };
+
+        } else {
+            return new Formatter() {
+                final DecimalFormat mFmt =
+                        buildDecimalFormat(
+                                minIntegerDigits,
+                                minFractionDigits,
+                                maxFractionDigits,
+                                groupingUsed,
+                                currentLocale);
+
+                @Override
+                public String format(int value) {
+                    return mFmt.format(value);
+                }
+
+                @Override
+                public String format(float value) {
+                    return mFmt.format(value);
+                }
+            };
+        }
+    }
+
+    static DecimalFormat buildDecimalFormat(
+            int minIntegerDigits,
+            int minFractionDigits,
+            int maxFractionDigits,
+            boolean groupingUsed,
+            ULocale currentLocale) {
+        DecimalFormat decimalFormat = (DecimalFormat) NumberFormat.getInstance(currentLocale);
+        decimalFormat.setMinimumIntegerDigits(minIntegerDigits);
+        decimalFormat.setGroupingUsed(groupingUsed);
+        decimalFormat.setMaximumFractionDigits(maxFractionDigits);
+        decimalFormat.setMinimumFractionDigits(minFractionDigits);
+        return decimalFormat;
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ObservableStateStore.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ObservableStateStore.java
new file mode 100644
index 0000000..8155cb2
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ObservableStateStore.java
@@ -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.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.UiThread;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+import androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * State storage for ProtoLayout, which also supports sending callback when data items change.
+ *
+ * <p>Note that this class is **not** thread-safe. Since ProtoLayout inflation currently happens on
+ * the main thread, and because updates will eventually affect the main thread, this whole class
+ * must only be used from the UI thread.
+ */
+public class ObservableStateStore {
+    @NonNull private final Map<String, StateEntryValue> mCurrentState = new ArrayMap<>();
+
+    @NonNull
+    private final Map<String, Set<DynamicTypeValueReceiver<StateEntryValue>>> mRegisteredCallbacks =
+            new ArrayMap<>();
+
+    public ObservableStateStore(@NonNull Map<String, StateEntryValue> initialState) {
+        mCurrentState.putAll(initialState);
+    }
+
+    /**
+     * Sets the given state into a storage. It replaces the current state with the new map and
+     * informs the registered listeners for changed values.
+     */
+    @UiThread
+    public void setStateEntryValues(@NonNull Map<String, StateEntryValue> newState) {
+        // Figure out which nodes have actually changed.
+        List<String> changedKeys = new ArrayList<>();
+        for (Entry<String, StateEntryValue> newEntry : newState.entrySet()) {
+            StateEntryValue currentEntry = mCurrentState.get(newEntry.getKey());
+            if (currentEntry == null || !currentEntry.equals(newEntry.getValue())) {
+                changedKeys.add(newEntry.getKey());
+            }
+        }
+
+        for (String key : changedKeys) {
+            for (DynamicTypeValueReceiver<StateEntryValue> callback :
+                    mRegisteredCallbacks.getOrDefault(key, Collections.emptySet())) {
+                callback.onPreUpdate();
+            }
+        }
+
+        mCurrentState.clear();
+        mCurrentState.putAll(newState);
+
+        for (String key : changedKeys) {
+            for (DynamicTypeValueReceiver<StateEntryValue> callback :
+                    mRegisteredCallbacks.getOrDefault(key, Collections.emptySet())) {
+                if (newState.containsKey(key)) {
+                    // The keys come from newState, so this should never be null.
+                    callback.onData(newState.get(key));
+                }
+            }
+        }
+    }
+
+    /**
+     * Gets state with the given key.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @UiThread
+    @Nullable
+    public StateEntryValue getStateEntryValues(@NonNull String key) {
+        return mCurrentState.get(key);
+    }
+
+    /**
+     * Registers the given callback for updates to the state for the given key.
+     *
+     * <p>Note that the callback will be executed on the UI thread.
+     */
+    @UiThread
+    void registerCallback(
+            @NonNull String key, @NonNull DynamicTypeValueReceiver<StateEntryValue> callback) {
+        new MainThreadExecutor();
+        mRegisteredCallbacks.computeIfAbsent(key, k -> new ArraySet<>()).add(callback);
+    }
+
+    /** Unregisters from receiving the updates. */
+    @UiThread
+    void unregisterCallback(
+            @NonNull String key, @NonNull DynamicTypeValueReceiver<StateEntryValue> callback) {
+        Set<DynamicTypeValueReceiver<StateEntryValue>> callbackSet = mRegisteredCallbacks.get(key);
+        if (callbackSet != null) {
+            callbackSet.remove(callback);
+        }
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/PlatformDataSources.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/PlatformDataSources.java
new file mode 100644
index 0000000..d2c2a6b
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/PlatformDataSources.java
@@ -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.wear.protolayout.expression.pipeline;
+
+import androidx.collection.ArrayMap;
+import androidx.collection.SimpleArrayMap;
+import androidx.wear.protolayout.expression.pipeline.TimeGateway.TimeCallback;
+import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway;
+import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.SensorDataType;
+import androidx.wear.protolayout.expression.proto.DynamicProto.PlatformInt32SourceType;
+
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/** Utility for various platform data sources. */
+class PlatformDataSources {
+    private PlatformDataSources() {}
+
+    interface PlatformDataSource {
+        void registerForData(
+                PlatformInt32SourceType sourceType, DynamicTypeValueReceiver<Integer> consumer);
+
+        void unregisterForData(
+                PlatformInt32SourceType sourceType, DynamicTypeValueReceiver<Integer> consumer);
+    }
+
+    /** Utility for time data source. */
+    static class EpochTimePlatformDataSource implements PlatformDataSource {
+        private final Executor mUiExecutor;
+        private final TimeGateway mGateway;
+        private final SimpleArrayMap<DynamicTypeValueReceiver<Integer>, TimeCallback>
+                mConsumerToTimeCallback = new SimpleArrayMap<>();
+
+        EpochTimePlatformDataSource(Executor uiExecutor, TimeGateway gateway) {
+            mUiExecutor = uiExecutor;
+            mGateway = gateway;
+        }
+
+        @Override
+        public void registerForData(
+                PlatformInt32SourceType sourceType, DynamicTypeValueReceiver<Integer> consumer) {
+            TimeCallback timeCallback =
+                    new TimeCallback() {
+                        @Override
+                        public void onPreUpdate() {
+                            consumer.onPreUpdate();
+                        }
+
+                        @Override
+                        public void onData() {
+                            long currentEpochTimeSeconds = System.currentTimeMillis() / 1000;
+                            consumer.onData((int) currentEpochTimeSeconds);
+                        }
+                    };
+            mGateway.registerForUpdates(mUiExecutor, timeCallback);
+            mConsumerToTimeCallback.put(consumer, timeCallback);
+        }
+
+        @Override
+        public void unregisterForData(
+                PlatformInt32SourceType sourceType, DynamicTypeValueReceiver<Integer> consumer) {
+            TimeCallback timeCallback = mConsumerToTimeCallback.remove(consumer);
+            if (timeCallback != null) {
+                mGateway.unregisterForUpdates(timeCallback);
+            }
+        }
+    }
+
+    /** Utility for sensor data source. */
+    static class SensorGatewayPlatformDataSource implements PlatformDataSource {
+        private static final String TAG = "SensorGtwPltDataSource";
+        final Executor mUiExecutor;
+        private final SensorGateway mSensorGateway;
+        private final Map<DynamicTypeValueReceiver<Integer>, SensorGateway.Consumer>
+                mCallbackToRegisteredSensorConsumer = new ArrayMap<>();
+
+        SensorGatewayPlatformDataSource(Executor uiExecutor, SensorGateway sensorGateway) {
+            this.mUiExecutor = uiExecutor;
+            this.mSensorGateway = sensorGateway;
+        }
+
+        @SensorDataType
+        private int mapSensorPlatformSource(PlatformInt32SourceType platformSource) {
+            switch (platformSource) {
+                case PLATFORM_INT32_SOURCE_TYPE_CURRENT_HEART_RATE:
+                    return SensorGateway.SENSOR_DATA_TYPE_HEART_RATE;
+                case PLATFORM_INT32_SOURCE_TYPE_DAILY_STEP_COUNT:
+                    return SensorGateway.SENSOR_DATA_TYPE_DAILY_STEP_COUNT;
+                default:
+                    throw new IllegalArgumentException("Unknown PlatformSourceType");
+            }
+        }
+
+        @Override
+        @SuppressWarnings("ExecutorTaskName")
+        public void registerForData(
+                PlatformInt32SourceType sourceType, DynamicTypeValueReceiver<Integer> callback) {
+            @SensorDataType int sensorDataType = mapSensorPlatformSource(sourceType);
+            SensorGateway.Consumer sensorConsumer =
+                    new SensorGateway.Consumer() {
+                        @Override
+                        public void onData(double value) {
+                            mUiExecutor.execute(() -> callback.onData((int) value));
+                        }
+
+                        @Override
+                        @SensorDataType
+                        public int getRequestedDataType() {
+                            return sensorDataType;
+                        }
+                    };
+            mCallbackToRegisteredSensorConsumer.put(callback, sensorConsumer);
+            mSensorGateway.registerSensorGatewayConsumer(sensorConsumer);
+        }
+
+        @Override
+        public void unregisterForData(
+                PlatformInt32SourceType sourceType, DynamicTypeValueReceiver<Integer> consumer) {
+            SensorGateway.Consumer sensorConsumer =
+                    mCallbackToRegisteredSensorConsumer.get(consumer);
+            if (sensorConsumer != null) {
+                mSensorGateway.unregisterSensorGatewayConsumer(sensorConsumer);
+            }
+        }
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/QuotaAwareAnimator.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/QuotaAwareAnimator.java
new file mode 100644
index 0000000..413b885
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/QuotaAwareAnimator.java
@@ -0,0 +1,206 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+/**
+ * Wrapper for Animator that is aware of quota. Animator's animations will be played only if given
+ * quota manager allows. If not, non infinite animation will jump to an end.
+ */
+class QuotaAwareAnimator {
+    @Nullable private ValueAnimator mAnimator;
+
+    @NonNull private final QuotaManager mQuotaManager;
+    @NonNull private final QuotaReleasingAnimatorListener mListener;
+
+    QuotaAwareAnimator(@Nullable ValueAnimator animator, @NonNull QuotaManager quotaManager) {
+        this.mAnimator = animator;
+        this.mQuotaManager = quotaManager;
+        this.mListener = new QuotaReleasingAnimatorListener(quotaManager);
+
+        if (this.mAnimator != null) {
+            this.mAnimator.addListener(mListener);
+        }
+    }
+
+    /**
+     * Sets the new animator with {link @QuotaReleasingListener} added. Previous animator will be
+     * canceled.
+     */
+    void updateAnimator(@NonNull ValueAnimator animator) {
+        cancelAnimator();
+
+        this.mAnimator = animator;
+        this.mAnimator.addListener(mListener);
+    }
+
+    /** Resets the animator to null. Previous animator will be canceled. */
+    void resetAnimator() {
+        cancelAnimator();
+
+        mAnimator = null;
+    }
+
+    /**
+     * Tries to start animation. Returns true if quota allows the animator to start. Otherwise, it
+     * returns false.
+     */
+    @UiThread
+    boolean tryStartAnimation() {
+        if (mAnimator == null) {
+            return false;
+        }
+        ValueAnimator localAnimator = mAnimator;
+        if (mQuotaManager.tryAcquireQuota(1)) {
+            startAnimator(localAnimator);
+            return true;
+        } else {
+            if (!isInfiniteAnimator()) {
+                localAnimator.end();
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Tries to start/resume infinite animation. Returns true if quota allows the animator to
+     * start/resume. Otherwise, it returns false.
+     */
+    @UiThread
+    boolean tryStartOrResumeAnimator() {
+        if (mAnimator == null) {
+            return false;
+        }
+        ValueAnimator localAnimator = mAnimator;
+        if (localAnimator.isPaused() && mQuotaManager.tryAcquireQuota(1)) {
+            resumeAnimator(localAnimator);
+        } else if (isInfiniteAnimator() && mQuotaManager.tryAcquireQuota(1)) {
+            // Infinite animators created when this node was invisible have not started yet.
+            startAnimator(localAnimator);
+        }
+        // No need to jump to an end of animation if it can't be played as they are infinite.
+        return false;
+    }
+
+    private void resumeAnimator(ValueAnimator localAnimator) {
+        localAnimator.resume();
+        mListener.mIsUsingQuota = true;
+    }
+
+    private void startAnimator(ValueAnimator localAnimator) {
+        localAnimator.start();
+        mListener.mIsUsingQuota = true;
+    }
+
+    /**
+     * Stops or pauses the animator, depending on it's state. If stopped, it will assign the end
+     * value.
+     */
+    @UiThread
+    void stopOrPauseAnimator() {
+        if (mAnimator == null) {
+            return;
+        }
+        ValueAnimator localAnimator = mAnimator;
+        if (isInfiniteAnimator()) {
+            localAnimator.pause();
+        } else {
+            // This causes the animation to assign the end value of the property being animated.
+            stopAnimator();
+        }
+    }
+
+    /** Stops the animator, which will cause it to assign the end value. */
+    @UiThread
+    void stopAnimator() {
+        if (mAnimator == null) {
+            return;
+        }
+        mAnimator.end();
+    }
+
+    /** Cancels the animator, which will stop in its tracks. */
+    @UiThread
+    void cancelAnimator() {
+        if (mAnimator == null) {
+            return;
+        }
+        // This calls both onCancel and onEnd methods from listener.
+        mAnimator.cancel();
+        mAnimator.removeListener(mListener);
+    }
+
+    /** Returns whether the animator in this class has an infinite duration. */
+    protected boolean isInfiniteAnimator() {
+        return mAnimator != null && mAnimator.getTotalDuration() == Animator.DURATION_INFINITE;
+    }
+
+    /** Returns whether this node has a running animation. */
+    boolean hasRunningAnimation() {
+        return mAnimator != null && mAnimator.isRunning();
+    }
+
+    /**
+     * The listener used for animatable nodes to release quota when the animation is finished or
+     * paused.
+     */
+    private static final class QuotaReleasingAnimatorListener extends AnimatorListenerAdapter {
+        @NonNull private final QuotaManager mQuotaManager;
+
+        // We need to keep track of whether the animation has started because pipeline has initiated
+        // and it has received quota, or onAnimationStart listener has been called because of the
+        // inner ValueAnimator implementation (i.e., when calling end() on animator to assign it end
+        // value, ValueAnimator will call start first if animation is not running to get it to the
+        // end state.
+        boolean mIsUsingQuota = false;
+
+        QuotaReleasingAnimatorListener(@NonNull QuotaManager quotaManager) {
+            this.mQuotaManager = quotaManager;
+        }
+
+        @Override
+        public void onAnimationStart(Animator animation) {}
+
+        @Override
+        public void onAnimationResume(Animator animation) {}
+
+        @Override
+        @UiThread
+        public void onAnimationEnd(Animator animation) {
+            if (mIsUsingQuota) {
+                mQuotaManager.releaseQuota(1);
+                mIsUsingQuota = false;
+            }
+        }
+
+        @Override
+        @UiThread
+        public void onAnimationPause(Animator animation) {
+            if (mIsUsingQuota) {
+                mQuotaManager.releaseQuota(1);
+                mIsUsingQuota = false;
+            }
+        }
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/QuotaManager.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/QuotaManager.java
new file mode 100644
index 0000000..fa8d642
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/QuotaManager.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.expression.pipeline;
+
+
+/**
+ * Interface responsible for managing quota. Before initiating some action (e.g. starting an
+ * animation) that uses a limited resource, {@link #tryAcquireQuota} should be called to see if the
+ * target quota cap has been reached and if not, it will be updated. When action/resource is
+ * finished, it should be release with {@link #releaseQuota}.
+ *
+ * <p>For example, this can be used for limiting the number of concurrently running animations.
+ * Every time new animations is due to be played, it should request quota from {@link QuotaManager}
+ * in amount that is equal to the number of animations that should be played (e.g. playing fade in
+ * and slide in animation on one object should request amount of 2 quota.
+ *
+ * <p>It is callers responsibility to release acquired quota after limited resource has been
+ * finished. For example, when animation is running, but surface became invisible, caller should
+ * return acquired quota.
+ */
+public interface QuotaManager {
+
+    /**
+     * Tries to acquire the given amount of quota and returns true if successful. Otherwise, returns
+     * false meaning that quota cap has already been reached and the quota won't be acquired.
+     *
+     * <p>It is callers responsibility to release acquired quota after limited resource has been
+     * finished. For example, when animation is running, but surface became invisible, caller should
+     * return acquired quota.
+     */
+    boolean tryAcquireQuota(int quota);
+
+    /**
+     * Releases the given amount of quota.
+     *
+     * @throws IllegalArgumentException if the given quota amount exceeds the amount of acquired
+     *     quota.
+     */
+    void releaseQuota(int quota);
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateSourceNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateSourceNode.java
new file mode 100644
index 0000000..8d14ff8
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateSourceNode.java
@@ -0,0 +1,83 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue;
+
+import java.util.function.Function;
+
+class StateSourceNode<T>
+        implements DynamicDataSourceNode<T>, DynamicTypeValueReceiver<StateEntryValue> {
+    private final ObservableStateStore mObservableStateStore;
+    private final String mBindKey;
+    private final Function<StateEntryValue, T> mStateExtractor;
+    private final DynamicTypeValueReceiver<T> mDownstream;
+
+    StateSourceNode(
+            ObservableStateStore observableStateStore,
+            String bindKey,
+            Function<StateEntryValue, T> stateExtractor,
+            DynamicTypeValueReceiver<T> downstream) {
+        this.mObservableStateStore = observableStateStore;
+        this.mBindKey = bindKey;
+        this.mStateExtractor = stateExtractor;
+        this.mDownstream = downstream;
+    }
+
+    @Override
+    @UiThread
+    public void preInit() {
+        mDownstream.onPreUpdate();
+    }
+
+    @Override
+    @UiThread
+    public void init() {
+        mObservableStateStore.registerCallback(mBindKey, this);
+        StateEntryValue item = mObservableStateStore.getStateEntryValues(mBindKey);
+
+        if (item != null) {
+            this.onData(item);
+        } else {
+            this.onInvalidated();
+        }
+    }
+
+    @Override
+    @UiThread
+    public void destroy() {
+        mObservableStateStore.unregisterCallback(mBindKey, this);
+    }
+
+    @Override
+    public void onPreUpdate() {
+        mDownstream.onPreUpdate();
+    }
+
+    @Override
+    public void onData(@Nullable StateEntryValue newData) {
+        T actualValue = mStateExtractor.apply(newData);
+        mDownstream.onData(actualValue);
+    }
+
+    @Override
+    public void onInvalidated() {
+        mDownstream.onInvalidated();
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StringNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StringNodes.java
new file mode 100644
index 0000000..1c224d4
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StringNodes.java
@@ -0,0 +1,89 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.UiThread;
+import androidx.wear.protolayout.expression.proto.DynamicProto.StateStringSource;
+import androidx.wear.protolayout.expression.proto.FixedProto.FixedString;
+
+/** Dynamic data nodes which yield Strings. */
+class StringNodes {
+    private StringNodes() {}
+
+    /** Dynamic string node that has a fixed value. */
+    static class FixedStringNode implements DynamicDataSourceNode<String> {
+        private final String mValue;
+        private final DynamicTypeValueReceiver<String> mDownstream;
+
+        FixedStringNode(FixedString protoNode, DynamicTypeValueReceiver<String> downstream) {
+            this.mValue = protoNode.getValue();
+            this.mDownstream = downstream;
+        }
+
+        @Override
+        @UiThread
+        public void preInit() {
+            mDownstream.onPreUpdate();
+        }
+
+        @Override
+        @UiThread
+        public void init() {
+            mDownstream.onData(mValue);
+        }
+
+        @Override
+        @UiThread
+        public void destroy() {}
+    }
+
+    /** Dynamic string node that gets a value from integer. */
+    static class Int32FormatNode extends DynamicDataTransformNode<Integer, String> {
+        Int32FormatNode(NumberFormatter formatter, DynamicTypeValueReceiver<String> downstream) {
+            super(downstream, formatter::format);
+        }
+    }
+
+    /** Dynamic string node that gets a value from the other strings. */
+    static class StringConcatOpNode extends DynamicDataBiTransformNode<String, String, String> {
+        StringConcatOpNode(DynamicTypeValueReceiver<String> downstream) {
+            super(downstream, String::concat);
+        }
+    }
+
+    /** Dynamic string node that gets a value from float. */
+    static class FloatFormatNode extends DynamicDataTransformNode<Float, String> {
+
+        FloatFormatNode(NumberFormatter formatter, DynamicTypeValueReceiver<String> downstream) {
+            super(downstream, formatter::format);
+        }
+    }
+
+    /** Dynamic string node that gets a value from the state. */
+    static class StateStringNode extends StateSourceNode<String> {
+        StateStringNode(
+                ObservableStateStore observableStateStore,
+                StateStringSource protoNode,
+                DynamicTypeValueReceiver<String> downstream) {
+            super(
+                    observableStateStore,
+                    protoNode.getSourceKey(),
+                    se -> se.getStringVal().getValue(),
+                    downstream);
+        }
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGateway.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGateway.java
new file mode 100644
index 0000000..786e1a2
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGateway.java
@@ -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.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.NonNull;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A gateway to Time data sources. This should call any provided callbacks every second.
+ *
+ * <p>Implementations of this class should track a few things:
+ *
+ * <ul>
+ *   <li>Surface lifecycle. Implementations should keep track of registered consumers, and
+ *       deregister them all when the surface is no longer available.
+ *   <li>Device state. Implementations should react to device state (i.e. ambient mode), and
+ *       activity state (i.e. is the surface in the foreground), and enable/disable updates
+ *       accordingly.
+ * </ul>
+ */
+interface TimeGateway {
+    /** Callback for time notifications. */
+    interface TimeCallback {
+        /**
+         * Called just before an update happens. All onPreUpdate calls will be made before any
+         * onUpdate calls fire.
+         *
+         * <p>Will be called on the same executor passed to {@link TimeGateway#registerForUpdates}.
+         */
+        void onPreUpdate();
+
+        /**
+         * Notifies that the current time has changed.
+         *
+         * <p>Will be called on the same executor passed to {@link TimeGateway#registerForUpdates}.
+         */
+        void onData();
+    }
+
+    /** Register for time updates. All callbacks will be called on the provided executor. */
+    void registerForUpdates(@NonNull Executor executor, @NonNull TimeCallback callback);
+
+    /** Unregister for time updates. */
+    void unregisterForUpdates(@NonNull TimeCallback callback);
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java
new file mode 100644
index 0000000..4c6a3e5
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java
@@ -0,0 +1,128 @@
+package androidx.wear.protolayout.expression.pipeline;
+
+import android.os.Handler;
+import android.os.SystemClock;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.UiThread;
+import androidx.collection.ArrayMap;
+
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Default implementation of {@link TimeGateway} using Android's clock.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public class TimeGatewayImpl implements TimeGateway, AutoCloseable {
+    private final Handler uiHandler;
+    private final Map<TimeCallback, Executor> registeredCallbacks = new ArrayMap<>();
+    private boolean updatesEnabled;
+    private final Runnable onTick;
+
+    private long lastScheduleTimeMillis = 0;
+
+    // Suppress warning on "onTick = this::notifyNextSecond". This happens because notifyNextSecond
+    // is @UnderInitialization here, but onTick needs to be @Initialized. This is safe though;  the
+    // only time that onTick can be invoked is in the other methods on this class, which can only be
+    // called after initialization is complete. This class is also final, so those methods cannot
+    // be called from a sub-constructor either.
+    @SuppressWarnings("methodref.receiver.bound")
+    public TimeGatewayImpl(@NonNull Handler uiHandler, boolean updatesEnabled) {
+        this.uiHandler = uiHandler;
+        this.updatesEnabled = updatesEnabled;
+
+        this.onTick = this::notifyNextSecond;
+    }
+
+    /** See {@link TimeGateway#registerForUpdates(Executor, TimeCallback)}. */
+    @Override
+    public void registerForUpdates(@NonNull Executor executor, @NonNull TimeCallback callback) {
+        registeredCallbacks.put(callback, executor);
+
+        // If this was the first registration, _and_ we're enabled, then schedule the message on the
+        // Handler (otherwise, another call has already scheduled the call).
+        if (registeredCallbacks.size() == 1 && this.updatesEnabled) {
+            lastScheduleTimeMillis = SystemClock.uptimeMillis() + 1000;
+            uiHandler.postAtTime(this.onTick, this, lastScheduleTimeMillis);
+        }
+    }
+
+    /** See {@link TimeGateway#unregisterForUpdates(TimeCallback)}. */
+    @Override
+    public void unregisterForUpdates(@NonNull TimeCallback callback) {
+        registeredCallbacks.remove(callback);
+
+        // If there are no more registered callbacks, stop the periodic call.
+        if (registeredCallbacks.isEmpty() && this.updatesEnabled) {
+            uiHandler.removeCallbacks(this.onTick, this);
+        }
+    }
+
+    @UiThread
+    public void enableUpdates() {
+        setUpdatesEnabled(true);
+    }
+
+    @UiThread
+    public void disableUpdates() {
+        setUpdatesEnabled(false);
+    }
+
+    private void setUpdatesEnabled(boolean updatesEnabled) {
+        if (updatesEnabled == this.updatesEnabled) {
+            return;
+        }
+
+        this.updatesEnabled = updatesEnabled;
+
+        if (!updatesEnabled) {
+            uiHandler.removeCallbacks(this.onTick, this);
+        } else if (!registeredCallbacks.isEmpty()) {
+            lastScheduleTimeMillis = SystemClock.uptimeMillis() + 1000;
+
+            uiHandler.postAtTime(this.onTick, this, lastScheduleTimeMillis);
+        }
+    }
+
+    @SuppressWarnings("ExecutorTaskName")
+    private void notifyNextSecond() {
+        if (!this.updatesEnabled) {
+            return;
+        }
+
+        for (Map.Entry<TimeCallback, Executor> callback : registeredCallbacks.entrySet()) {
+            callback.getValue().execute(callback.getKey()::onPreUpdate);
+        }
+
+        for (Map.Entry<TimeCallback, Executor> callback : registeredCallbacks.entrySet()) {
+            callback.getValue().execute(callback.getKey()::onData);
+        }
+
+        lastScheduleTimeMillis += 1000;
+
+        // Ensure that the new time is actually in the future. If a call from uiHandler gets
+        // significantly delayed for any reason, then without this, we'll reschedule immediately
+        // (potentially multiple times), compounding the situation further.
+        if (lastScheduleTimeMillis < SystemClock.uptimeMillis()) {
+            // Skip the failed updates...
+            long missedTime = SystemClock.uptimeMillis() - lastScheduleTimeMillis;
+
+            // Round up to the nearest second...
+            missedTime = ((missedTime / 1000) + 1) * 1000;
+            lastScheduleTimeMillis += missedTime;
+        }
+
+        uiHandler.postAtTime(this.onTick, this, lastScheduleTimeMillis);
+    }
+
+    @Override
+    public void close() {
+        setUpdatesEnabled(false);
+        registeredCallbacks.clear();
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/sensor/SensorGateway.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/sensor/SensorGateway.java
new file mode 100644
index 0000000..513f38d
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/sensor/SensorGateway.java
@@ -0,0 +1,203 @@
+/*
+ * 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.wear.protolayout.expression.pipeline.sensor;
+
+import android.Manifest;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresPermission;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.UiThread;
+
+import java.io.Closeable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
+
+/**
+ * Gateway for proto layout expression library to be able to access sensor data, e.g. health data.
+ *
+ * <p>Implementations of this class should track a few things:
+ *
+ * <ul>
+ *   <li>Surface lifecycle. Implementations should keep track of the surface provider, registered
+ *       consumers, and deregister them all when the surface is not longer available.
+ *   <li>Device state. Implementations should react to device state (i.e. ambient mode), and
+ *       activity state (i.e. surface being in the foreground), and appropriately set the sampling
+ *       rate of the sensor (e.g. high rate when surface is in the foreground, otherwise low-rate or
+ *       off).
+ * </ul>
+ */
+public interface SensorGateway extends Closeable {
+
+    /**
+     * Sensor data types that can be subscribed to from {@link SensorGateway}.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+        SENSOR_DATA_TYPE_INVALID,
+        SENSOR_DATA_TYPE_HEART_RATE,
+        SENSOR_DATA_TYPE_DAILY_STEP_COUNT
+    })
+    public @interface SensorDataType {};
+
+    /** Invalid data type. Used to return error states. */
+    int SENSOR_DATA_TYPE_INVALID = -1;
+
+    /**
+     * The user's current heart rate. This is an instantaneous reading from the last time it was
+     * sampled. Note that this means that apps which subscribe to passive heart rate data may not
+     * receive exact heart rate data; it will be batched to a given period.
+     */
+    @RequiresPermission(Manifest.permission.BODY_SENSORS)
+    int SENSOR_DATA_TYPE_HEART_RATE = 0;
+
+    /**
+     * The user's current daily step count. Note that this data type will reset to zero at midnight.
+     * each day, and any subscriptions to this data type will log the number of steps the user has
+     * done since 12:00AM local time.
+     */
+    @RequiresPermission(Manifest.permission.ACTIVITY_RECOGNITION)
+    int SENSOR_DATA_TYPE_DAILY_STEP_COUNT = 1;
+
+    /**
+     * Consumer for sensor data.
+     *
+     * <p>If Consumer is relying on multiple sources or upstream nodes, it should be responsible for
+     * data coordination between pending updates and received data.
+     *
+     * <p>For example, this Consumer listens to two upstream sources:
+     *
+     * <pre>{@code
+     * class MyConsumer implements Consumer {
+     *  int pending = 0;
+     *
+     * @Override
+     * public void onPreUpdate(){
+     *      pending++;
+     * }
+     *
+     *  @Override
+     *  public void onData(double value){
+     *   // store the value internally
+     *   pending--;
+     *   if (pending == 0) {
+     *     // We've received data from every changed upstream source
+     *     consumeTheChangedDataValues()
+     *   }
+     *  }
+     * }
+     * }</pre>
+     */
+    interface Consumer {
+        /**
+         * Called when a new batch of data has arrived. The actual data will be delivered in {@link
+         * #onData(double)} after this method is called on all registered consumers.
+         */
+        @AnyThread
+        default void onPreUpdate() {}
+
+        /**
+         * Called when a new data for the requested data type is received. This will be run on a
+         * single background thread.
+         *
+         * <p>Note that there is no notification when a daily sensor data item resets to zero; this
+         * will simply emit an item of sensor data with value 0.0 when the rollover happens.
+         */
+        @AnyThread
+        void onData(double value);
+
+        /**
+         * Notify that the current data for the registered data type has been invalidated. This
+         * could be, for example, that the current heart rate is no longer valid as the user is not
+         * wearing the device.
+         */
+        @AnyThread
+        default void onInvalidated() {}
+
+        /** The sensor data type to be consumed. */
+        @SensorDataType
+        int getRequestedDataType();
+    }
+
+    /**
+     * Enables/unpauses sending updates to the consumers. All cached updates (while updates were
+     * paused) for data types will be delivered by sending the latest data.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP_PREFIX)
+    void enableUpdates();
+
+    /**
+     * Disables/pauses sending updates to the consumers. While paused, updates will be cached to be
+     * delivered after unpausing.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP_PREFIX)
+    void disableUpdates();
+
+    /**
+     * Register for updates for {@link Consumer#getRequestedDataType()} data type. This may cause
+     * {@link Consumer} to immediately fire if there is suitable cached data, otherwise {@link
+     * Consumer} will fire when there is appropriate updates to the requested sensor data.
+     *
+     * <p>Implementations should check if the provider has permission to provide the requested data
+     * type.
+     *
+     * <p>Note that the callback will be executed on the single background thread (implementation
+     * dependent). To specify the execution thread, use {@link
+     * #registerSensorGatewayConsumer(Executor, Consumer)}.
+     *
+     * @throws SecurityException if the provider does not have permission to provide requested data
+     *     type.
+     */
+    @UiThread
+    void registerSensorGatewayConsumer(@NonNull Consumer consumer);
+
+    /**
+     * Register for updates for {@link Consumer#getRequestedDataType()} data type. This may cause
+     * {@link Consumer} to immediately fire if there is suitable cached data, otherwise {@link
+     * Consumer} will fire when there is appropriate updates to the requested sensor data.
+     *
+     * <p>Implementations should check if the provider has permission to provide the requested data
+     * type.
+     *
+     * <p>The callback will be executed on the provided {@link Executor}.
+     *
+     * @throws SecurityException if the provider does not have permission to provide requested data
+     *     type.
+     */
+    @UiThread
+    void registerSensorGatewayConsumer(
+            @NonNull /* @CallbackExecutor */ Executor executor, @NonNull Consumer consumer);
+
+    /** Unregister for updates for {@link Consumer#getRequestedDataType()} data type. */
+    @UiThread
+    void unregisterSensorGatewayConsumer(@NonNull Consumer consumer);
+
+    /** See {@link Closeable#close()}. */
+    @Override
+    void close();
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/AnimatableNodeTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/AnimatableNodeTest.java
new file mode 100644
index 0000000..e0f76fd
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/AnimatableNodeTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.expression.pipeline;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.animation.ValueAnimator;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class AnimatableNodeTest {
+
+    @Test
+    public void infiniteAnimator_onlyStartsWhenNodeIsVisible() {
+        ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 10.0f);
+        QuotaManager quotaManager = new UnlimitedQuotaManager();
+        TestQuotaAwareAnimator quotaAwareAnimator =
+                new TestQuotaAwareAnimator(animator, quotaManager);
+        TestAnimatableNode animNode = new TestAnimatableNode(quotaAwareAnimator);
+
+        quotaAwareAnimator.isInfiniteAnimator = true;
+
+        assertThat(animator.isRunning()).isFalse();
+
+        animNode.startOrSkipAnimator();
+        assertThat(animator.isRunning()).isFalse();
+
+        animNode.setVisibility(true);
+        assertThat(animator.isRunning()).isTrue();
+    }
+
+    @Test
+    public void infiniteAnimator_pausesWhenNodeIsInvisible() {
+        ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 10.0f);
+        QuotaManager quotaManager = new UnlimitedQuotaManager();
+        TestQuotaAwareAnimator quotaAwareAnimator =
+                new TestQuotaAwareAnimator(animator, quotaManager);
+        TestAnimatableNode animNode = new TestAnimatableNode(quotaAwareAnimator);
+
+        quotaAwareAnimator.isInfiniteAnimator = true;
+
+        animNode.setVisibility(true);
+        assertThat(animator.isRunning()).isTrue();
+
+        animNode.setVisibility(false);
+        assertThat(animator.isPaused()).isTrue();
+
+        animNode.setVisibility(true);
+        assertThat(animator.isRunning()).isTrue();
+    }
+
+    @Test
+    public void animator_noQuota_notPlayed() {
+        ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 10.0f);
+        QuotaManager quotaManager = new TestNoQuotaManagerImpl();
+        TestQuotaAwareAnimator quotaAwareAnimator =
+                new TestQuotaAwareAnimator(animator, quotaManager);
+        TestAnimatableNode animNode = new TestAnimatableNode(quotaAwareAnimator);
+
+        // Check that animator hasn't started because there is no quota.
+        animNode.setVisibility(true);
+        assertThat(animator.isStarted()).isFalse();
+        assertThat(animator.isRunning()).isFalse();
+    }
+
+    static class TestAnimatableNode extends AnimatableNode {
+
+        TestAnimatableNode(QuotaAwareAnimator quotaAwareAnimator) {
+            super(quotaAwareAnimator);
+        }
+    }
+
+    static class TestQuotaAwareAnimator extends QuotaAwareAnimator {
+        public boolean isInfiniteAnimator = false;
+
+        TestQuotaAwareAnimator(
+                @Nullable ValueAnimator animator, @NonNull QuotaManager mQuotaManager) {
+            super(animator, mQuotaManager);
+        }
+
+        @Override
+        protected boolean isInfiniteAnimator() {
+            return isInfiniteAnimator;
+        }
+    }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/TestNoQuotaManagerImpl.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/TestNoQuotaManagerImpl.java
new file mode 100644
index 0000000..520e2cdc
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/TestNoQuotaManagerImpl.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.expression.pipeline;
+
+/** QuotaManager that doesn't allow any quota. */
+public class TestNoQuotaManagerImpl implements QuotaManager {
+
+    @Override
+    public boolean tryAcquireQuota(int quota) {
+        return false;
+    }
+
+    @Override
+    public void releaseQuota(int quota) {}
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/UnlimitedQuotaManager.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/UnlimitedQuotaManager.java
new file mode 100644
index 0000000..e4c814a
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/UnlimitedQuotaManager.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.expression.pipeline;
+
+/** Default, unlimited quota manager implementation that always returns true. */
+public class UnlimitedQuotaManager implements QuotaManager {
+    private int mQuotaCounter = 0;
+
+    /**
+     * @see QuotaManager#tryAcquireQuota
+     *     <p>Note that this method is not thread safe.
+     */
+    @Override
+    public boolean tryAcquireQuota(int quota) {
+        mQuotaCounter += quota;
+        return true;
+    }
+
+    /**
+     * @see QuotaManager#releaseQuota
+     *     <p>Note that this method is not thread safe.
+     */
+    @Override
+    public void releaseQuota(int quota) {
+        mQuotaCounter -= quota;
+    }
+
+    /** Returns true if all quota has been released. */
+    public boolean isAllQuotaReleased() {
+        return mQuotaCounter == 0;
+    }
+}
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
index 62c253c..d9949ab 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 {
@@ -107,9 +113,39 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat animate();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 asInt();
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat constant(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(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 androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(float);
+    method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat!,java.lang.Float!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default byte[] toDynamicFloatByteArray();
   }
 
   public static class DynamicBuilders.DynamicFloat.FloatFormatter {
@@ -123,9 +159,44 @@
   public static interface DynamicBuilders.DynamicInt32 extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat asFloat();
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 constant(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 div(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(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 androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 minus(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(int);
+    method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32!,java.lang.Integer!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 plus(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 rem(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 times(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(float);
+    method public default byte[] toDynamicInt32ByteArray();
   }
 
   public static class DynamicBuilders.DynamicInt32.IntFormatter {
@@ -137,8 +208,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..be278c8 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 {
@@ -107,9 +113,39 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat animate();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 asInt();
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat constant(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(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 androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(float);
+    method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat!,java.lang.Float!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default byte[] toDynamicFloatByteArray();
   }
 
   public static class DynamicBuilders.DynamicFloat.FloatFormatter {
@@ -123,9 +159,44 @@
   public static interface DynamicBuilders.DynamicInt32 extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat asFloat();
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 constant(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 div(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(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 androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 minus(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(int);
+    method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32!,java.lang.Integer!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 plus(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 rem(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 times(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(float);
+    method public default byte[] toDynamicInt32ByteArray();
   }
 
   public static class DynamicBuilders.DynamicInt32.IntFormatter {
@@ -137,8 +208,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..d9949ab 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 {
@@ -107,9 +113,39 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat animate();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 asInt();
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat constant(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(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 androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(float);
+    method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat!,java.lang.Float!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default byte[] toDynamicFloatByteArray();
   }
 
   public static class DynamicBuilders.DynamicFloat.FloatFormatter {
@@ -123,9 +159,44 @@
   public static interface DynamicBuilders.DynamicInt32 extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat asFloat();
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 constant(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 div(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(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 androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 minus(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(int);
+    method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32!,java.lang.Integer!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 plus(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 rem(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(float);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 times(int);
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(float);
+    method public default byte[] toDynamicInt32ByteArray();
   }
 
   public static class DynamicBuilders.DynamicInt32.IntFormatter {
@@ -137,8 +208,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/build.gradle b/wear/protolayout/protolayout-expression/build.gradle
index 0666bcd..eb396f8 100644
--- a/wear/protolayout/protolayout-expression/build.gradle
+++ b/wear/protolayout/protolayout-expression/build.gradle
@@ -26,7 +26,7 @@
     api("androidx.annotation:annotation:1.2.0")
 
     implementation("androidx.annotation:annotation-experimental:1.2.0")
-    implementation(project(":wear:protolayout:protolayout-proto"))
+    implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
 
     compileOnly(libs.kotlinStdlib) // For annotation-experimental
 
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..9b6c389 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;
 
@@ -56,7 +58,7 @@
 
     /**
      * Incoming elements are animated using deceleration easing, which starts a transition at peak
-     * velocity (the fastest point of an element’s movement) and ends at rest.
+     * velocity (the fastest point of an element's movement) and ends at rest.
      *
      * <p>This is equivalent to the Compose {@code LinearOutSlowInEasing}.
      */
@@ -140,7 +142,7 @@
     }
 
     /**
-     * Gets the easing to be used for adjusting an animation’s fraction.
+     * Gets the easing to be used for adjusting an animation's fraction.
      *
      * @since 1.2
      */
@@ -202,6 +204,21 @@
       return mImpl;
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "AnimationSpec{"
+          + "durationMillis="
+          + getDurationMillis()
+          + ", delayMillis="
+          + getDelayMillis()
+          + ", easing="
+          + getEasing()
+          + ", repeatable="
+          + getRepeatable()
+          + "}";
+    }
+
     /** Builder for {@link AnimationSpec} */
     public static final class Builder {
       private final AnimationParameterProto.AnimationSpec.Builder mImpl =
@@ -235,7 +252,7 @@
       }
 
       /**
-       * Sets the easing to be used for adjusting an animation’s fraction. If not set, defaults to
+       * Sets the easing to be used for adjusting an animation's fraction. If not set, defaults to
        * Linear Interpolator.
        *
        * @since 1.2
@@ -280,7 +297,7 @@
   }
 
   /**
-   * Interface defining the easing to be used for adjusting an animation’s fraction. This allows
+   * Interface defining the easing to be used for adjusting an animation's fraction. This allows
    * animation to speed up and slow down, rather than moving at a constant rate. If not set,
    * defaults to Linear Interpolator.
    *
@@ -296,6 +313,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 +340,8 @@
     @Nullable
     Fingerprint getFingerprint();
 
-    /** Builder to create {@link Easing} objects.
+    /**
+     * Builder to create {@link Easing} objects.
      *
      * @hide
      */
@@ -327,7 +363,7 @@
   }
 
   /**
-   * The cubic polynomial easing that implements third-order Bézier curves. This is equivalent to
+   * The cubic polynomial easing that implements third-order Bezier curves. This is equivalent to
    * the Android PathInterpolator.
    *
    * @since 1.2
@@ -408,6 +444,21 @@
       return AnimationParameterProto.Easing.newBuilder().setCubicBezier(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "CubicBezierEasing{"
+          + "x1="
+          + getX1()
+          + ", y1="
+          + getY1()
+          + ", x2="
+          + getX2()
+          + ", y2="
+          + getY2()
+          + "}";
+    }
+
     /** Builder for {@link CubicBezierEasing}. */
     public static final class Builder implements Easing.Builder {
       private final AnimationParameterProto.CubicBezierEasing.Builder mImpl =
@@ -542,6 +593,17 @@
       return iteration < 1;
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "Repeatable{"
+          + "iterations="
+          + getIterations()
+          + ", repeatMode="
+          + getRepeatMode()
+          + "}";
+    }
+
     /** Builder for {@link Repeatable} */
     public static final class Builder {
       private final AnimationParameterProto.Repeatable.Builder mImpl =
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..dfecd528 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;
 
@@ -43,6 +46,69 @@
   private DynamicBuilders() {}
 
   /**
+   * The type of arithmetic operation used in {@link ArithmeticInt32Op} and {@link
+   * ArithmeticFloatOp}.
+   *
+   * @hide
+   * @since 1.2
+   */
+  @RestrictTo(RestrictTo.Scope.LIBRARY)
+  @IntDef({
+      ARITHMETIC_OP_TYPE_UNDEFINED,
+      ARITHMETIC_OP_TYPE_ADD,
+      ARITHMETIC_OP_TYPE_SUBTRACT,
+      ARITHMETIC_OP_TYPE_MULTIPLY,
+      ARITHMETIC_OP_TYPE_DIVIDE,
+      ARITHMETIC_OP_TYPE_MODULO
+  })
+  @Retention(RetentionPolicy.SOURCE)
+  @interface ArithmeticOpType {
+
+  }
+
+  /**
+   * Undefined operation type.
+   *
+   * @since 1.2
+   */
+  static final int ARITHMETIC_OP_TYPE_UNDEFINED = 0;
+
+  /**
+   * Addition.
+   *
+   * @since 1.2
+   */
+  static final int ARITHMETIC_OP_TYPE_ADD = 1;
+
+  /**
+   * Subtraction.
+   *
+   * @since 1.2
+   */
+  static final int ARITHMETIC_OP_TYPE_SUBTRACT = 2;
+
+  /**
+   * Multiplication.
+   *
+   * @since 1.2
+   */
+  static final int ARITHMETIC_OP_TYPE_MULTIPLY = 3;
+
+  /**
+   * Division.
+   *
+   * @since 1.2
+   */
+  static final int ARITHMETIC_OP_TYPE_DIVIDE = 4;
+
+  /**
+   * Modulus.
+   *
+   * @since 1.2
+   */
+  static final int ARITHMETIC_OP_TYPE_MODULO = 5;
+
+  /**
    * Rounding mode to use when converting a float to an int32.
    *
    * @since 1.2
@@ -89,13 +155,13 @@
    */
   @RestrictTo(RestrictTo.Scope.LIBRARY)
   @IntDef({
-    COMPARISON_OP_TYPE_UNDEFINED,
-    COMPARISON_OP_TYPE_EQUALS,
-    COMPARISON_OP_TYPE_NOT_EQUALS,
-    COMPARISON_OP_TYPE_LESS_THAN,
-    COMPARISON_OP_TYPE_LESS_THAN_OR_EQUAL_TO,
-    COMPARISON_OP_TYPE_GREATER_THAN,
-    COMPARISON_OP_TYPE_GREATER_THAN_OR_EQUAL_TO
+      COMPARISON_OP_TYPE_UNDEFINED,
+      COMPARISON_OP_TYPE_EQUALS,
+      COMPARISON_OP_TYPE_NOT_EQUALS,
+      COMPARISON_OP_TYPE_LESS_THAN,
+      COMPARISON_OP_TYPE_LESS_THAN_OR_EQUAL_TO,
+      COMPARISON_OP_TYPE_GREATER_THAN,
+      COMPARISON_OP_TYPE_GREATER_THAN_OR_EQUAL_TO
   })
   @Retention(RetentionPolicy.SOURCE)
   @interface ComparisonOpType {}
@@ -183,6 +249,150 @@
   static final int LOGICAL_OP_TYPE_OR = 2;
 
   /**
+   * An arithmetic operation, operating on two Int32 instances. This implements simple binary
+   * operations of the form "result = LHS <op> RHS", where the available operation types are
+   * described in {@code ArithmeticOpType}.
+   *
+   * @since 1.2
+   */
+  static final class ArithmeticInt32Op implements DynamicInt32 {
+
+    private final DynamicProto.ArithmeticInt32Op mImpl;
+    @Nullable
+    private final Fingerprint mFingerprint;
+
+    ArithmeticInt32Op(DynamicProto.ArithmeticInt32Op impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets left hand side of the arithmetic operation.
+     *
+     * @since 1.2
+     */
+    @Nullable
+    public DynamicInt32 getInputLhs() {
+      if (mImpl.hasInputLhs()) {
+        return DynamicBuilders.dynamicInt32FromProto(mImpl.getInputLhs());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets right hand side of the arithmetic operation.
+     *
+     * @since 1.2
+     */
+    @Nullable
+    public DynamicInt32 getInputRhs() {
+      if (mImpl.hasInputRhs()) {
+        return DynamicBuilders.dynamicInt32FromProto(mImpl.getInputRhs());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the type of operation to carry out.
+     *
+     * @since 1.2
+     */
+    @ArithmeticOpType
+    public int getOperationType() {
+      return mImpl.getOperationType().getNumber();
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static ArithmeticInt32Op fromProto(@NonNull DynamicProto.ArithmeticInt32Op proto) {
+      return new ArithmeticInt32Op(proto, null);
+    }
+
+    @NonNull
+    DynamicProto.ArithmeticInt32Op toProto() {
+      return mImpl;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public DynamicProto.DynamicInt32 toDynamicInt32Proto() {
+      return DynamicProto.DynamicInt32.newBuilder().setArithmeticOperation(mImpl).build();
+    }
+
+    /**
+     * Builder for {@link ArithmeticInt32Op}.
+     */
+    public static final class Builder implements DynamicInt32.Builder {
+
+      private final DynamicProto.ArithmeticInt32Op.Builder mImpl =
+          DynamicProto.ArithmeticInt32Op.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-2012727925);
+
+      public Builder() {
+      }
+
+      /**
+       * Sets left hand side of the arithmetic operation.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setInputLhs(@NonNull DynamicInt32 inputLhs) {
+        mImpl.setInputLhs(inputLhs.toDynamicInt32Proto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(inputLhs.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets right hand side of the arithmetic operation.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setInputRhs(@NonNull DynamicInt32 inputRhs) {
+        mImpl.setInputRhs(inputRhs.toDynamicInt32Proto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(inputRhs.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the type of operation to carry out.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setOperationType(@ArithmeticOpType int operationType) {
+        mImpl.setOperationType(DynamicProto.ArithmeticOpType.forNumber(operationType));
+        mFingerprint.recordPropertyUpdate(3, operationType);
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public ArithmeticInt32Op build() {
+        return new ArithmeticInt32Op(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
    * A dynamic Int32 which sources its data from the tile's state.
    *
    * @since 1.2
@@ -197,7 +407,7 @@
     }
 
     /**
-     * Gets the key in the state to bind to. Intended for testing purposes only.
+     * Gets the key in the state to bind to.
      *
      * @since 1.2
      */
@@ -232,6 +442,12 @@
       return DynamicProto.DynamicInt32.newBuilder().setStateSource(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "StateInt32Source{" + "sourceKey=" + getSourceKey() + "}";
+    }
+
     /** Builder for {@link StateInt32Source}. */
     public static final class Builder implements DynamicInt32.Builder {
       private final DynamicProto.StateInt32Source.Builder mImpl =
@@ -261,6 +477,302 @@
   }
 
   /**
+   * A conditional operator which yields an integer depending on the boolean operand. This
+   * implements "int result = condition ? value_if_true : value_if_false".
+   *
+   * @since 1.2
+   */
+  static final class ConditionalInt32Op implements DynamicInt32 {
+
+    private final DynamicProto.ConditionalInt32Op mImpl;
+    @Nullable
+    private final Fingerprint mFingerprint;
+
+    ConditionalInt32Op(DynamicProto.ConditionalInt32Op impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the condition to use.
+     *
+     * @since 1.2
+     */
+    @Nullable
+    public DynamicBool getCondition() {
+      if (mImpl.hasCondition()) {
+        return DynamicBuilders.dynamicBoolFromProto(mImpl.getCondition());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the integer to yield if condition is true.
+     *
+     * @since 1.2
+     */
+    @Nullable
+    public DynamicInt32 getValueIfTrue() {
+      if (mImpl.hasValueIfTrue()) {
+        return DynamicBuilders.dynamicInt32FromProto(mImpl.getValueIfTrue());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the integer to yield if condition is false.
+     *
+     * @since 1.2
+     */
+    @Nullable
+    public DynamicInt32 getValueIfFalse() {
+      if (mImpl.hasValueIfFalse()) {
+        return DynamicBuilders.dynamicInt32FromProto(mImpl.getValueIfFalse());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static ConditionalInt32Op fromProto(@NonNull DynamicProto.ConditionalInt32Op proto) {
+      return new ConditionalInt32Op(proto, null);
+    }
+
+    @NonNull
+    DynamicProto.ConditionalInt32Op toProto() {
+      return mImpl;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public DynamicProto.DynamicInt32 toDynamicInt32Proto() {
+      return DynamicProto.DynamicInt32.newBuilder().setConditionalOp(mImpl).build();
+    }
+
+    /**
+     * Builder for {@link ConditionalInt32Op}.
+     */
+    public static final class Builder implements DynamicInt32.Builder {
+
+      private final DynamicProto.ConditionalInt32Op.Builder mImpl =
+          DynamicProto.ConditionalInt32Op.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(1444834226);
+
+      public Builder() {
+      }
+
+      /**
+       * Sets the condition to use.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setCondition(@NonNull DynamicBool condition) {
+        mImpl.setCondition(condition.toDynamicBoolProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(condition.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the integer to yield if condition is true.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setValueIfTrue(@NonNull DynamicInt32 valueIfTrue) {
+        mImpl.setValueIfTrue(valueIfTrue.toDynamicInt32Proto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(valueIfTrue.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the integer to yield if condition is false.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setValueIfFalse(@NonNull DynamicInt32 valueIfFalse) {
+        mImpl.setValueIfFalse(valueIfFalse.toDynamicInt32Proto());
+        mFingerprint.recordPropertyUpdate(
+            3, checkNotNull(valueIfFalse.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public ConditionalInt32Op build() {
+        return new ConditionalInt32Op(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * A conditional operator which yields a float depending on the boolean operand. This implements
+   * "float result = condition ? value_if_true : value_if_false".
+   *
+   * @since 1.2
+   */
+  static final class ConditionalFloatOp implements DynamicFloat {
+
+    private final DynamicProto.ConditionalFloatOp mImpl;
+    @Nullable
+    private final Fingerprint mFingerprint;
+
+    ConditionalFloatOp(DynamicProto.ConditionalFloatOp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the condition to use.
+     *
+     * @since 1.2
+     */
+    @Nullable
+    public DynamicBool getCondition() {
+      if (mImpl.hasCondition()) {
+        return DynamicBuilders.dynamicBoolFromProto(mImpl.getCondition());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the float to yield if condition is true.
+     *
+     * @since 1.2
+     */
+    @Nullable
+    public DynamicFloat getValueIfTrue() {
+      if (mImpl.hasValueIfTrue()) {
+        return DynamicBuilders.dynamicFloatFromProto(mImpl.getValueIfTrue());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the float to yield if condition is false.
+     *
+     * @since 1.2
+     */
+    @Nullable
+    public DynamicFloat getValueIfFalse() {
+      if (mImpl.hasValueIfFalse()) {
+        return DynamicBuilders.dynamicFloatFromProto(mImpl.getValueIfFalse());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static ConditionalFloatOp fromProto(@NonNull DynamicProto.ConditionalFloatOp proto) {
+      return new ConditionalFloatOp(proto, null);
+    }
+
+    @NonNull
+    DynamicProto.ConditionalFloatOp toProto() {
+      return mImpl;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public DynamicProto.DynamicFloat toDynamicFloatProto() {
+      return DynamicProto.DynamicFloat.newBuilder().setConditionalOp(mImpl).build();
+    }
+
+    /**
+     * Builder for {@link ConditionalFloatOp}.
+     */
+    public static final class Builder implements DynamicFloat.Builder {
+
+      private final DynamicProto.ConditionalFloatOp.Builder mImpl =
+          DynamicProto.ConditionalFloatOp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(1968171153);
+
+      public Builder() {
+      }
+
+      /**
+       * Sets the condition to use.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setCondition(@NonNull DynamicBool condition) {
+        mImpl.setCondition(condition.toDynamicBoolProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(condition.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the float to yield if condition is true.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setValueIfTrue(@NonNull DynamicFloat valueIfTrue) {
+        mImpl.setValueIfTrue(valueIfTrue.toDynamicFloatProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(valueIfTrue.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the float to yield if condition is false.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setValueIfFalse(@NonNull DynamicFloat valueIfFalse) {
+        mImpl.setValueIfFalse(valueIfFalse.toDynamicFloatProto());
+        mFingerprint.recordPropertyUpdate(
+            3, checkNotNull(valueIfFalse.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public ConditionalFloatOp build() {
+        return new ConditionalFloatOp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
    * Converts a Float to an Int32, with a customizable rounding mode.
    *
    * @since 1.2
@@ -275,7 +787,7 @@
     }
 
     /**
-     * Gets the float to round. Intended for testing purposes only.
+     * Gets the float to round.
      *
      * @since 1.2
      */
@@ -289,8 +801,7 @@
     }
 
     /**
-     * Gets the rounding mode to use. Defaults to ROUND_MODE_FLOOR if not specified. Intended for
-     * testing purposes only.
+     * Gets the rounding mode to use. Defaults to ROUND_MODE_FLOOR if not specified.
      *
      * @since 1.2
      */
@@ -325,6 +836,12 @@
       return DynamicProto.DynamicInt32.newBuilder().setFloatToInt(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "FloatToInt32Op{" + "input=" + getInput() + ", roundMode=" + getRoundMode() + "}";
+    }
+
     /** Builder for {@link FloatToInt32Op}. */
     public static final class Builder implements DynamicInt32.Builder {
       private final DynamicProto.FloatToInt32Op.Builder mImpl =
@@ -369,6 +886,26 @@
   /**
    * Interface defining a dynamic int32 type.
    *
+   * <p>It offers a set of helper methods for creating arithmetic and logical expressions, e.g.
+   * {@link #plus(int)}, {@link #times(int)}, {@link #eq(int)}, etc. These helper methods produce
+   * expression trees based on the order in which they were called in an expression. Thus, no
+   * operator precedence rules are applied.
+   *
+   * <p>For example the following expression is equivalent to {@code result = ((a + b)*c)/d }:
+   *
+   * <pre>
+   * a.plus(b).times(c).div(d);
+   * </pre>
+   *
+   * More complex expressions can be created by nesting expressions. For example the following
+   * expression is equivalent to {@code result = (a + b)*(c - d) }:
+   *
+   * <pre>
+   * (a.plus(b)).times(c.minus(d));
+   * </pre>
+   *
+   * .
+   *
    * @since 1.2
    */
   public interface DynamicInt32 extends DynamicType {
@@ -381,6 +918,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) {
@@ -405,6 +963,670 @@
     }
 
     /**
+     * Bind the value of this {@link DynamicInt32} to the result of a conditional expression. This
+     * will use the value given in either {@link ConditionScope#use} or {@link
+     * ConditionScopes.IfTrueScope#elseUse} depending on the value yielded from {@code condition}.
+     */
+    @NonNull
+    static ConditionScope<DynamicInt32, Integer> onCondition(@NonNull DynamicBool condition) {
+      return new ConditionScopes.ConditionScope<>(
+          (trueValue, falseValue) ->
+              new ConditionalInt32Op.Builder()
+                  .setCondition(condition)
+                  .setValueIfTrue(trueValue)
+                  .setValueIfFalse(falseValue)
+                  .build(),
+          DynamicInt32::constant);
+    }
+
+    /**
+     * Creates a {@link DynamicInt32} containing the result of adding another {@link DynamicInt32}
+     * to this {@link DynamicInt32}; As an example, the following is equal to {@code
+     * DynamicInt32.constant(13)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).plus(DynamicInt32.constant(6));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicInt32 plus(@NonNull DynamicInt32 other) {
+      return new ArithmeticInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_ADD)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFlaot} containing the result of adding a {@link DynamicFloat} to this
+     * {@link DynamicInt32}; As an example, the following is equal to {@code
+     * DynamicFloat.constant(13.5f)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).plus(DynamicFloat.constant(6.5f));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat plus(@NonNull DynamicFloat other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this.asFloat())
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_ADD)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicInt32} containing the result of adding an integer to this {@link
+     * DynamicInt32}; As an example, the following is equal to {@code DynamicInt32.constant(13)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).plus(6);
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicInt32 plus(int other) {
+      return new ArithmeticInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_ADD)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFlaot} containing the result of adding a float to this {@link
+     * DynamicInt32}; As an example, the following is equal to {@code DynamicFloat.constant(13.5f)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).plus(6.5f);
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat plus(float other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this.asFloat())
+          .setInputRhs(DynamicFloat.constant(other))
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_ADD)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicInt32} containing the result of subtracting another {@link
+     * DynamicInt32} from this {@link DynamicInt32}; As an example, the following is equal to {@code
+     * DynamicInt32.constant(2)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).minus(DynamicInt32.constant(5));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicInt32 minus(@NonNull DynamicInt32 other) {
+      return new ArithmeticInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_SUBTRACT)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the result of subtracting a {@link DynamicFloat}
+     * from this {@link DynamicInt32}; As an example, the following is equal to {@code
+     * DynamicFloat.constant(1.5f)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).minus(DynamicFloat.constant(5.5f));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat minus(@NonNull DynamicFloat other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this.asFloat())
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_SUBTRACT)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicInt32} containing the result of subtracting an integer from this
+     * {@link DynamicInt32}; As an example, the following is equal to {@code
+     * DynamicInt32.constant(2)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).minus(5);
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicInt32 minus(int other) {
+      return new ArithmeticInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_SUBTRACT)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the result of subtracting a float from this {@link
+     * DynamicInt32}; As an example, the following is equal to {@code DynamicFloat.constant(1.5f)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).minus(5.5f);
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat minus(float other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this.asFloat())
+          .setInputRhs(DynamicFloat.constant(other))
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_SUBTRACT)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicInt32} containing the result of multiplying this {@link DynamicInt32}
+     * by another {@link DynamicInt32}; As an example, the following is equal to {@code
+     * DynamicInt32.constant(35)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).times(DynamicInt32.constant(5));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicInt32 times(@NonNull DynamicInt32 other) {
+      return new ArithmeticInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MULTIPLY)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the result of multiplying this {@link DynamicInt32}
+     * by a {@link DynamicFloat}; As an example, the following is equal to {@code
+     * DynamicFloat.constant(38.5f)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).times(DynamicFloat.constant(5.5f));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat times(@NonNull DynamicFloat other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this.asFloat())
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MULTIPLY)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicInt32} containing the result of multiplying this {@link DynamicInt32}
+     * by an integer; As an example, the following is equal to {@code DynamicInt32.constant(35)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).times(5);
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicInt32 times(int other) {
+      return new ArithmeticInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MULTIPLY)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the result of multiplying this {@link DynamicInt32}
+     * by a float; As an example, the following is equal to {@code DynamicFloat.constant(38.5f)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).times(5.5f);
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat times(float other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this.asFloat())
+          .setInputRhs(DynamicFloat.constant(other))
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MULTIPLY)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicInt32} containing the result of dividing this {@link DynamicInt32} by
+     * another {@link DynamicInt32}; As an example, the following is equal to {@code
+     * DynamicInt32.constant(1)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).div(DynamicInt32.constant(5));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicInt32 div(@NonNull DynamicInt32 other) {
+      return new ArithmeticInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_DIVIDE)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the result of dividing this {@link DynamicInt32} by
+     * a {@link DynamicFloat}; As an example, the following is equal to {@code
+     * DynamicFloat.constant(1.4f)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).div(DynamicFloat.constant(5f));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat div(@NonNull DynamicFloat other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this.asFloat())
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_DIVIDE)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicInt32} containing the result of dividing this {@link DynamicInt32} by
+     * an integer; As an example, the following is equal to {@code DynamicInt32.constant(1)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).div(5);
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicInt32 div(int other) {
+      return new ArithmeticInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_DIVIDE)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the result of dividing this {@link DynamicInt32} by
+     * a float; As an example, the following is equal to {@code DynamicFloat.constant(1.4f)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).div(5f);
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat div(float other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this.asFloat())
+          .setInputRhs(DynamicFloat.constant(other))
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_DIVIDE)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicInt32} containing the reminder of dividing this {@link DynamicInt32}
+     * by another {@link DynamicInt32}; As an example, the following is equal to {@code
+     * DynamicInt32.constant(2)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).rem(DynamicInt32.constant(5));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicInt32 rem(@NonNull DynamicInt32 other) {
+      return new ArithmeticInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MODULO)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the reminder of dividing this {@link DynamicInt32}
+     * by a {@link DynamicFloat}; As an example, the following is equal to {@code
+     * DynamicFloat.constant(1.5f)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).rem(DynamicInt32.constant(5.5f));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat rem(@NonNull DynamicFloat other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this.asFloat())
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MODULO)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicInt32} containing the reminder of dividing this {@link DynamicInt32}
+     * by an integer; As an example, the following is equal to {@code DynamicInt32.constant(2)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).rem(5);
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicInt32 rem(int other) {
+      return new ArithmeticInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MODULO)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicInt32} containing the reminder of dividing this {@link DynamicInt32}
+     * by a float; As an example, the following is equal to {@code DynamicFloat.constant(1.5f)}
+     *
+     * <pre>
+     *   DynamicInt32.constant(7).rem(5.5f);
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat rem(float other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs( this.asFloat())
+          .setInputRhs(DynamicFloat.constant(other))
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MODULO)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} and
+     * {@code other} are equal, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool eq(@NonNull DynamicInt32 other) {
+      return new ComparisonInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_EQUALS)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} and
+     * {@code other} are equal, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool eq(int other) {
+      return new ComparisonInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_EQUALS)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} and
+     * {@code other} are not equal, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool ne(@NonNull DynamicInt32 other) {
+      return new ComparisonInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_NOT_EQUALS)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} and
+     * {@code other} are not equal, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool ne(int other) {
+      return new ComparisonInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_NOT_EQUALS)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} is less
+     * than {@code other}, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool lt(@NonNull DynamicInt32 other) {
+      return new ComparisonInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_LESS_THAN)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} is less
+     * than {@code other}, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool lt(int other) {
+      return new ComparisonInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_LESS_THAN)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} is less
+     * than or equal to {@code other}, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool lte(@NonNull DynamicInt32 other) {
+      return new ComparisonInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_LESS_THAN_OR_EQUAL_TO)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} is less
+     * than or equal to {@code other}, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool lte(int other) {
+      return new ComparisonInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_LESS_THAN_OR_EQUAL_TO)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} is
+     * greater than {@code other}, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool gt(@NonNull DynamicInt32 other) {
+      return new ComparisonInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_GREATER_THAN)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} is
+     * greater than {@code other}, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool gt(int other) {
+      return new ComparisonInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_GREATER_THAN)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} is
+     * greater than or equal to {@code other}, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool gte(@NonNull DynamicInt32 other) {
+      return new ComparisonInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_GREATER_THAN_OR_EQUAL_TO)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} is
+     * greater than or equal to {@code other}, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool gte(int other) {
+      return new ComparisonInt32Op.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_GREATER_THAN_OR_EQUAL_TO)
+          .build();
+    }
+
+    /**
      * Returns a {@link DynamicString} that contains the formatted value of this {@link
      * DynamicInt32} (with default formatting parameters). As an example, in the English locale, the
      * following is equal to {@code DynamicString.constant("12")}
@@ -481,7 +1703,8 @@
     @Nullable
     Fingerprint getFingerprint();
 
-    /** Builder to create {@link DynamicInt32} objects.
+    /**
+     * Builder to create {@link DynamicInt32} objects.
      *
      * @hide
      */
@@ -506,9 +1729,15 @@
     if (proto.hasFixed()) {
       return FixedInt32.fromProto(proto.getFixed());
     }
+    if (proto.hasArithmeticOperation()) {
+      return ArithmeticInt32Op.fromProto(proto.getArithmeticOperation());
+    }
     if (proto.hasStateSource()) {
       return StateInt32Source.fromProto(proto.getStateSource());
     }
+    if (proto.hasConditionalOp()) {
+      return ConditionalInt32Op.fromProto(proto.getConditionalOp());
+    }
     if (proto.hasFloatToInt()) {
       return FloatToInt32Op.fromProto(proto.getFloatToInt());
     }
@@ -530,7 +1759,7 @@
     }
 
     /**
-     * Gets the source of Int32 data to convert to a string. Intended for testing purposes only.
+     * Gets the source of Int32 data to convert to a string.
      *
      * @since 1.2
      */
@@ -546,8 +1775,7 @@
     /**
      * Gets minimum integer digits. Sign and grouping characters are not considered when applying
      * minIntegerDigits constraint. If not defined, defaults to one. For example,in the English
-     * locale, applying minIntegerDigit=4 to 12 would yield "0012". Intended for testing purposes
-     * only.
+     * locale, applying minIntegerDigit=4 to 12 would yield "0012".
      *
      * @since 1.2
      */
@@ -559,7 +1787,7 @@
     /**
      * Gets digit grouping used. Grouping size and grouping character depend on the current locale.
      * If not defined, defaults to false. For example, in the English locale, using grouping with
-     * 1234 would yield "1,234". Intended for testing purposes only.
+     * 1234 would yield "1,234".
      *
      * @since 1.2
      */
@@ -593,6 +1821,19 @@
       return DynamicProto.DynamicString.newBuilder().setInt32FormatOp(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "Int32FormatOp{"
+          + "input="
+          + getInput()
+          + ", minIntegerDigits="
+          + getMinIntegerDigits()
+          + ", groupingUsed="
+          + getGroupingUsed()
+          + "}";
+    }
+
     /** Builder for {@link Int32FormatOp}. */
     public static final class Builder implements DynamicString.Builder {
       private final DynamicProto.Int32FormatOp.Builder mImpl =
@@ -666,7 +1907,7 @@
     }
 
     /**
-     * Gets the key in the state to bind to. Intended for testing purposes only.
+     * Gets the key in the state to bind to.
      *
      * @since 1.2
      */
@@ -701,6 +1942,12 @@
       return DynamicProto.DynamicString.newBuilder().setStateSource(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "StateStringSource{" + "sourceKey=" + getSourceKey() + "}";
+    }
+
     /** Builder for {@link StateStringSource}. */
     public static final class Builder implements DynamicString.Builder {
       private final DynamicProto.StateStringSource.Builder mImpl =
@@ -745,7 +1992,7 @@
     }
 
     /**
-     * Gets the condition to use. Intended for testing purposes only.
+     * Gets the condition to use.
      *
      * @since 1.2
      */
@@ -759,7 +2006,7 @@
     }
 
     /**
-     * Gets the string to yield if condition is true. Intended for testing purposes only.
+     * Gets the string to yield if condition is true.
      *
      * @since 1.2
      */
@@ -773,7 +2020,7 @@
     }
 
     /**
-     * Gets the string to yield if condition is false. Intended for testing purposes only.
+     * Gets the string to yield if condition is false.
      *
      * @since 1.2
      */
@@ -812,6 +2059,19 @@
       return DynamicProto.DynamicString.newBuilder().setConditionalOp(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "ConditionalStringOp{"
+          + "condition="
+          + getCondition()
+          + ", valueIfTrue="
+          + getValueIfTrue()
+          + ", valueIfFalse="
+          + getValueIfFalse()
+          + "}";
+    }
+
     /** Builder for {@link ConditionalStringOp}. */
     public static final class Builder implements DynamicString.Builder {
       private final DynamicProto.ConditionalStringOp.Builder mImpl =
@@ -873,6 +2133,7 @@
    * @since 1.2
    */
   static final class ConcatStringOp implements DynamicString {
+
     private final DynamicProto.ConcatStringOp mImpl;
     @Nullable private final Fingerprint mFingerprint;
 
@@ -882,7 +2143,7 @@
     }
 
     /**
-     * Gets left hand side of the concatenation operation. Intended for testing purposes only.
+     * Gets left hand side of the concatenation operation.
      *
      * @since 1.2
      */
@@ -896,7 +2157,7 @@
     }
 
     /**
-     * Gets right hand side of the concatenation operation. Intended for testing purposes only.
+     * Gets right hand side of the concatenation operation.
      *
      * @since 1.2
      */
@@ -935,6 +2196,12 @@
       return DynamicProto.DynamicString.newBuilder().setConcatOp(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "ConcatStringOp{" + "inputLhs=" + getInputLhs() + ", inputRhs=" + getInputRhs() + "}";
+    }
+
     /** Builder for {@link ConcatStringOp}. */
     public static final class Builder implements DynamicString.Builder {
       private final DynamicProto.ConcatStringOp.Builder mImpl =
@@ -992,7 +2259,7 @@
     }
 
     /**
-     * Gets the source of Float data to convert to a string. Intended for testing purposes only.
+     * Gets the source of Float data to convert to a string.
      *
      * @since 1.2
      */
@@ -1009,7 +2276,7 @@
      * Gets maximum fraction digits. Rounding will be applied if maxFractionDigits is smaller than
      * number of fraction digits. If not defined, defaults to three. minimumFractionDigits must be
      * <= maximumFractionDigits. If the condition is not satisfied, then minimumFractionDigits will
-     * be used for both fields. Intended for testing purposes only.
+     * be used for both fields.
      *
      * @since 1.2
      */
@@ -1022,7 +2289,6 @@
      * Gets minimum fraction digits. Zeros will be appended to the end to satisfy this constraint.
      * If not defined, defaults to zero. minimumFractionDigits must be <= maximumFractionDigits. If
      * the condition is not satisfied, then minimumFractionDigits will be used for both fields.
-     * Intended for testing purposes only.
      *
      * @since 1.2
      */
@@ -1034,8 +2300,7 @@
     /**
      * Gets minimum integer digits. Sign and grouping characters are not considered when applying
      * minIntegerDigits constraint. If not defined, defaults to one. For example, in the English
-     * locale, applying minIntegerDigit=4 to 12.34 would yield "0012.34". Intended for testing
-     * purposes only.
+     * locale, applying minIntegerDigit=4 to 12.34 would yield "0012.34".
      *
      * @since 1.2
      */
@@ -1047,7 +2312,7 @@
     /**
      * Gets digit grouping used. Grouping size and grouping character depend on the current locale.
      * If not defined, defaults to false. For example, in the English locale, using grouping with
-     * 1234.56 would yield "1,234.56". Intended for testing purposes only.
+     * 1234.56 would yield "1,234.56".
      *
      * @since 1.2
      */
@@ -1081,6 +2346,23 @@
       return DynamicProto.DynamicString.newBuilder().setFloatFormatOp(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "FloatFormatOp{"
+          + "input="
+          + getInput()
+          + ", maxFractionDigits="
+          + getMaxFractionDigits()
+          + ", minFractionDigits="
+          + getMinFractionDigits()
+          + ", minIntegerDigits="
+          + getMinIntegerDigits()
+          + ", groupingUsed="
+          + getGroupingUsed()
+          + "}";
+    }
+
     /** Builder for {@link FloatFormatOp}. */
     public static final class Builder implements DynamicString.Builder {
       private final DynamicProto.FloatFormatOp.Builder mImpl =
@@ -1183,6 +2465,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 +2545,8 @@
     @Nullable
     Fingerprint getFingerprint();
 
-    /** Builder to create {@link DynamicString} objects.
+    /**
+     * Builder to create {@link DynamicString} objects.
      *
      * @hide
      */
@@ -1286,6 +2590,150 @@
   }
 
   /**
+   * An arithmetic operation, operating on two Float instances. This implements simple binary
+   * operations of the form "result = LHS <op> RHS", where the available operation types are
+   * described in {@code ArithmeticOpType}.
+   *
+   * @since 1.2
+   */
+  static final class ArithmeticFloatOp implements DynamicFloat {
+
+    private final DynamicProto.ArithmeticFloatOp mImpl;
+    @Nullable
+    private final Fingerprint mFingerprint;
+
+    ArithmeticFloatOp(DynamicProto.ArithmeticFloatOp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets left hand side of the arithmetic operation.
+     *
+     * @since 1.2
+     */
+    @Nullable
+    public DynamicFloat getInputLhs() {
+      if (mImpl.hasInputLhs()) {
+        return DynamicBuilders.dynamicFloatFromProto(mImpl.getInputLhs());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets right hand side of the arithmetic operation.
+     *
+     * @since 1.2
+     */
+    @Nullable
+    public DynamicFloat getInputRhs() {
+      if (mImpl.hasInputRhs()) {
+        return DynamicBuilders.dynamicFloatFromProto(mImpl.getInputRhs());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the type of operation to carry out.
+     *
+     * @since 1.2
+     */
+    @ArithmeticOpType
+    public int getOperationType() {
+      return mImpl.getOperationType().getNumber();
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static ArithmeticFloatOp fromProto(@NonNull DynamicProto.ArithmeticFloatOp proto) {
+      return new ArithmeticFloatOp(proto, null);
+    }
+
+    @NonNull
+    DynamicProto.ArithmeticFloatOp toProto() {
+      return mImpl;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public DynamicProto.DynamicFloat toDynamicFloatProto() {
+      return DynamicProto.DynamicFloat.newBuilder().setArithmeticOperation(mImpl).build();
+    }
+
+    /**
+     * Builder for {@link ArithmeticFloatOp}.
+     */
+    public static final class Builder implements DynamicFloat.Builder {
+
+      private final DynamicProto.ArithmeticFloatOp.Builder mImpl =
+          DynamicProto.ArithmeticFloatOp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-1818249334);
+
+      public Builder() {
+      }
+
+      /**
+       * Sets left hand side of the arithmetic operation.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setInputLhs(@NonNull DynamicFloat inputLhs) {
+        mImpl.setInputLhs(inputLhs.toDynamicFloatProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(inputLhs.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets right hand side of the arithmetic operation.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setInputRhs(@NonNull DynamicFloat inputRhs) {
+        mImpl.setInputRhs(inputRhs.toDynamicFloatProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(inputRhs.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the type of operation to carry out.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setOperationType(@ArithmeticOpType int operationType) {
+        mImpl.setOperationType(DynamicProto.ArithmeticOpType.forNumber(operationType));
+        mFingerprint.recordPropertyUpdate(3, operationType);
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public ArithmeticFloatOp build() {
+        return new ArithmeticFloatOp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
    * A dynamic Float which sources its data from the tile's state.
    *
    * @since 1.2
@@ -1300,7 +2748,7 @@
     }
 
     /**
-     * Gets the key in the state to bind to. Intended for testing purposes only.
+     * Gets the key in the state to bind to.
      *
      * @since 1.2
      */
@@ -1335,6 +2783,12 @@
       return DynamicProto.DynamicFloat.newBuilder().setStateSource(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "StateFloatSource{" + "sourceKey=" + getSourceKey() + "}";
+    }
+
     /** Builder for {@link StateFloatSource}. */
     public static final class Builder implements DynamicFloat.Builder {
       private final DynamicProto.StateFloatSource.Builder mImpl =
@@ -1378,7 +2832,7 @@
     }
 
     /**
-     * Gets the input Int32 to convert to a Float. Intended for testing purposes only.
+     * Gets the input Int32 to convert to a Float.
      *
      * @since 1.2
      */
@@ -1417,6 +2871,12 @@
       return DynamicProto.DynamicFloat.newBuilder().setInt32ToFloatOperation(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "Int32ToFloatOp{" + "input=" + getInput() + "}";
+    }
+
     /** Builder for {@link Int32ToFloatOp}. */
     public static final class Builder implements DynamicFloat.Builder {
       private final DynamicProto.Int32ToFloatOp.Builder mImpl =
@@ -1462,7 +2922,7 @@
     }
 
     /**
-     * Gets the number to start animating from. Intended for testing purposes only.
+     * Gets the number to start animating from.
      *
      * @since 1.2
      */
@@ -1471,7 +2931,7 @@
     }
 
     /**
-     * Gets the number to animate to. Intended for testing purposes only.
+     * Gets the number to animate to.
      *
      * @since 1.2
      */
@@ -1480,7 +2940,7 @@
     }
 
     /**
-     * Gets the animation parameters for duration, delay, etc. Intended for testing purposes only.
+     * Gets the animation parameters for duration, delay, etc.
      *
      * @since 1.2
      */
@@ -1519,6 +2979,19 @@
       return DynamicProto.DynamicFloat.newBuilder().setAnimatableFixed(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "AnimatableFixedFloat{"
+          + "fromValue="
+          + getFromValue()
+          + ", toValue="
+          + getToValue()
+          + ", spec="
+          + getSpec()
+          + "}";
+    }
+
     /** Builder for {@link AnimatableFixedFloat}. */
     public static final class Builder implements DynamicFloat.Builder {
       private final DynamicProto.AnimatableFixedFloat.Builder mImpl =
@@ -1594,7 +3067,7 @@
     }
 
     /**
-     * Gets the value to watch, and animate when it changes. Intended for testing purposes only.
+     * Gets the value to watch, and animate when it changes.
      *
      * @since 1.2
      */
@@ -1608,7 +3081,7 @@
     }
 
     /**
-     * Gets the animation parameters for duration, delay, etc. Intended for testing purposes only.
+     * Gets the animation parameters for duration, delay, etc.
      *
      * @since 1.2
      */
@@ -1647,6 +3120,12 @@
       return DynamicProto.DynamicFloat.newBuilder().setAnimatableDynamic(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "AnimatableDynamicFloat{" + "input=" + getInput() + ", spec=" + getSpec() + "}";
+    }
+
     /** Builder for {@link AnimatableDynamicFloat}. */
     public static final class Builder implements DynamicFloat.Builder {
       private final DynamicProto.AnimatableDynamicFloat.Builder mImpl =
@@ -1692,6 +3171,26 @@
   /**
    * Interface defining a dynamic float type.
    *
+   * <p>It offers a set of helper methods for creating arithmetic and logical expressions, e.g.
+   * {@link #plus(float)}, {@link #times(float)}, {@link #eq(float)}, etc. These helper methods
+   * produce expression trees based on the order in which they were called in an expression. Thus,
+   * no operator precedence rules are applied.
+   *
+   * <p>For example the following expression is equivalent to {@code result = ((a + b)*c)/d }:
+   *
+   * <pre>
+   * a.plus(b).times(c).div(d);
+   * </pre>
+   *
+   * More complex expressions can be created by nesting expressions. For example the following
+   * expression is equivalent to {@code result = (a + b)*(c - d) }:
+   *
+   * <pre>
+   * (a.plus(b)).times(c.minus(d));
+   * </pre>
+   *
+   * .
+   *
    * @since 1.2
    */
   public interface DynamicFloat extends DynamicType {
@@ -1704,6 +3203,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) {
@@ -1813,6 +3333,551 @@
     }
 
     /**
+     * Creates a {@link DynamicFloat} containing the result of adding another {@link DynamicFloat}
+     * to this {@link DynamicFloat}; As an example, the following is equal to {@code
+     * DynamicFloat.constant(13f)}
+     *
+     * <pre>
+     *   DynamicFloat.constant(7f).plus(DynamicFloat.constant(5f));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat plus(@NonNull DynamicFloat other) {
+
+      // overloaded operators.
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_ADD)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the result of adding a float to this {@link
+     * DynamicFloat}; As an example, the following is equal to {@code DynamicFloat.constant(13f)}
+     *
+     * <pre>
+     *   DynamicFloat.constant(7f).plus(5f);
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat plus(float other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_ADD)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the result of adding a {@link DynamicInt32} to this
+     * {@link DynamicFloat}; As an example, the following is equal to {@code
+     * DynamicFloat.constant(13f)}
+     *
+     * <pre>
+     *   DynamicFloat.constant(7f).plus(DynamicInt32.constant(5));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat plus(@NonNull DynamicInt32 other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other.asFloat())
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_ADD)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the result of subtracting another {@link
+     * DynamicFloat} from this {@link DynamicFloat}; As an example, the following is equal to {@code
+     * DynamicFloat.constant(2f)}
+     *
+     * <pre>
+     *   DynamicFloat.constant(7f).minus(DynamicFloat.constant(5f));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat minus(@NonNull DynamicFloat other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_SUBTRACT)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the result of subtracting a flaot from this {@link
+     * DynamicFloat}; As an example, the following is equal to {@code DynamicFloat.constant(2f)}
+     *
+     * <pre>
+     *   DynamicFloat.constant(7f).minus(5f);
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat minus(float other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_SUBTRACT)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the result of subtracting a {@link DynamicInt32}
+     * from this {@link DynamicFloat}; As an example, the following is equal to {@code
+     * DynamicFloat.constant(2f)}
+     *
+     * <pre>
+     *   DynamicFloat.constant(7f).minus(DynamicInt32.constant(5));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat minus(@NonNull DynamicInt32 other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other.asFloat())
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_SUBTRACT)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the result of multiplying this {@link DynamicFloat}
+     * by another {@link DynamicFloat}; As an example, the following is equal to {@code
+     * DynamicFloat.constant(35f)}
+     *
+     * <pre>
+     *   DynamicFloat.constant(7f).times(DynamicFloat.constant(5f));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat times(@NonNull DynamicFloat other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MULTIPLY)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the result of multiplying this {@link DynamicFloat}
+     * by a flaot; As an example, the following is equal to {@code DynamicFloat.constant(35f)}
+     *
+     * <pre>
+     *   DynamicFloat.constant(7f).times(5f);
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat times(float other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MULTIPLY)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the result of multiplying this {@link DynamicFloat}
+     * by a {@link DynamicInt32}; As an example, the following is equal to {@code
+     * DynamicFloat.constant(35f)}
+     *
+     * <pre>
+     *   DynamicFloat.constant(7f).times(DynamicInt32.constant(5));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat times(@NonNull DynamicInt32 other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other.asFloat())
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MULTIPLY)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the result of dividing this {@link DynamicFloat} by
+     * another {@link DynamicFloat}; As an example, the following is equal to {@code
+     * DynamicFloat.constant(1.4f)}
+     *
+     * <pre>
+     *   DynamicFloat.constant(7f).div(DynamicFloat.constant(5f));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat div(@NonNull DynamicFloat other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_DIVIDE)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the result of dividing this {@link DynamicFloat} by
+     * a float; As an example, the following is equal to {@code DynamicFloat.constant(1.4f)}
+     *
+     * <pre>
+     *   DynamicFloat.constant(7f).div(5f);
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat div(float other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_DIVIDE)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the result of dividing this {@link DynamicFloat} by
+     * a {@link DynamicInt32}; As an example, the following is equal to {@code
+     * DynamicFloat.constant(1.4f)}
+     *
+     * <pre>
+     *   DynamicFloat.constant(7f).div(DynamicInt32.constant(5));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat div(@NonNull DynamicInt32 other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other.asFloat())
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_DIVIDE)
+          .build();
+    }
+
+    /**
+     * Creates a {@link DynamicFloat} containing the reminder of dividing this {@link DynamicFloat}
+     * by another {@link DynamicFloat}; As an example, the following is equal to {@code
+     * DynamicFloat.constant(1.5f)}
+     *
+     * <pre>
+     *   DynamicFloat.constant(7f).rem(DynamicFloat.constant(5.5f));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat rem(@NonNull DynamicFloat other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MODULO)
+          .build();
+    }
+
+    /**
+     * reates a {@link DynamicFloat} containing the reminder of dividing this {@link DynamicFloat}
+     * by a float; As an example, the following is equal to {@code DynamicFloat.constant(1.5f)}
+     *
+     * <pre>
+     *   DynamicFloat.constant(7f).rem(5.5f);
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat rem(float other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MODULO)
+          .build();
+    }
+
+    /**
+     * reates a {@link DynamicFloat} containing the reminder of dividing this {@link DynamicFloat}
+     * by a {@link DynamicInt32}; As an example, the following is equal to {@code
+     * DynamicFloat.constant(2f)}
+     *
+     * <pre>
+     *   DynamicFloat.constant(7f).rem(DynamicInt32.constant(5));
+     * </pre>
+     *
+     * The operation's evaluation order depends only on its position in the expression; no operator
+     * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+     * evaluation order.
+     *
+     * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+     */
+    @SuppressWarnings("KotlinOperator")
+    @NonNull
+    default DynamicFloat rem(@NonNull DynamicInt32 other) {
+      return new ArithmeticFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other.asFloat())
+          .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MODULO)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} and
+     * {@code other} are equal, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool eq(@NonNull DynamicFloat other) {
+      return new ComparisonFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_EQUALS)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} and
+     * {@code other} are equal, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool eq(float other) {
+      return new ComparisonFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_EQUALS)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} and
+     * {@code other} are not equal, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool ne(@NonNull DynamicFloat other) {
+      return new ComparisonFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_NOT_EQUALS)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} and
+     * {@code other} are not equal, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool ne(float other) {
+      return new ComparisonFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_NOT_EQUALS)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} is less
+     * than {@code other}, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool lt(@NonNull DynamicFloat other) {
+      return new ComparisonFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_LESS_THAN)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} is less
+     * than {@code other}, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool lt(float other) {
+      return new ComparisonFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_LESS_THAN)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} is less
+     * than or equal to {@code other}, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool lte(@NonNull DynamicFloat other) {
+      return new ComparisonFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_LESS_THAN_OR_EQUAL_TO)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} is less
+     * than or equal to {@code other}, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool lte(float other) {
+      return new ComparisonFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_LESS_THAN_OR_EQUAL_TO)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} is
+     * greater than {@code other}, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool gt(@NonNull DynamicFloat other) {
+      return new ComparisonFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_GREATER_THAN)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} is
+     * greater than {@code other}, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool gt(float other) {
+      return new ComparisonFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_GREATER_THAN)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} is
+     * greater than or equal to {@code other}, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool gte(@NonNull DynamicFloat other) {
+      return new ComparisonFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(other)
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_GREATER_THAN_OR_EQUAL_TO)
+          .build();
+    }
+
+    /**
+     * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} is
+     * greater than or equal to {@code other}, otherwise it's false.
+     */
+    @NonNull
+    default DynamicBool gte(float other) {
+      return new ComparisonFloatOp.Builder()
+          .setInputLhs(this)
+          .setInputRhs(constant(other))
+          .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_GREATER_THAN_OR_EQUAL_TO)
+          .build();
+    }
+
+    /**
+     * Bind the value of this {@link DynamicFloat} to the result of a conditional expression. This
+     * will use the value given in either {@link ConditionScope#use} or {@link
+     * ConditionScopes.IfTrueScope#elseUse} depending on the value yielded from {@code condition}.
+     */
+    @NonNull
+    static ConditionScope<DynamicFloat, Float> onCondition(@NonNull DynamicBool condition) {
+      return new ConditionScopes.ConditionScope<>(
+          (trueValue, falseValue) ->
+              new ConditionalFloatOp.Builder()
+                  .setCondition(condition)
+                  .setValueIfTrue(trueValue)
+                  .setValueIfFalse(falseValue)
+                  .build(),
+          DynamicFloat::constant);
+    }
+
+    /**
      * Returns a {@link DynamicString} that contains the formatted value of this {@link
      * DynamicFloat} (with default formatting parameters). As an example, in the English locale, the
      * following is equal to {@code DynamicString.constant("12.346")}
@@ -1911,7 +3976,8 @@
     @Nullable
     Fingerprint getFingerprint();
 
-    /** Builder to create {@link DynamicFloat} objects.
+    /**
+     * Builder to create {@link DynamicFloat} objects.
      *
      * @hide
      */
@@ -1936,12 +4002,18 @@
     if (proto.hasFixed()) {
       return FixedFloat.fromProto(proto.getFixed());
     }
+    if (proto.hasArithmeticOperation()) {
+      return ArithmeticFloatOp.fromProto(proto.getArithmeticOperation());
+    }
     if (proto.hasInt32ToFloatOperation()) {
       return Int32ToFloatOp.fromProto(proto.getInt32ToFloatOperation());
     }
     if (proto.hasStateSource()) {
       return StateFloatSource.fromProto(proto.getStateSource());
     }
+    if (proto.hasConditionalOp()) {
+      return ConditionalFloatOp.fromProto(proto.getConditionalOp());
+    }
     if (proto.hasAnimatableFixed()) {
       return AnimatableFixedFloat.fromProto(proto.getAnimatableFixed());
     }
@@ -1966,7 +4038,7 @@
     }
 
     /**
-     * Gets the key in the state to bind to. Intended for testing purposes only.
+     * Gets the key in the state to bind to.
      *
      * @since 1.2
      */
@@ -2001,8 +4073,15 @@
       return DynamicProto.DynamicBool.newBuilder().setStateSource(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "StateBoolSource{" + "sourceKey=" + getSourceKey() + "}";
+    }
+
     /** Builder for {@link StateBoolSource}. */
     public static final class Builder implements DynamicBool.Builder {
+
       private final DynamicProto.StateBoolSource.Builder mImpl =
           DynamicProto.StateBoolSource.newBuilder();
       private final Fingerprint mFingerprint = new Fingerprint(1818702779);
@@ -2030,6 +4109,294 @@
   }
 
   /**
+   * A comparison operation, operating on two Int32 instances. This implements various comparison
+   * operations of the form "boolean result = LHS <op> RHS", where the available operation types are
+   * described in {@code ComparisonOpType}.
+   *
+   * @since 1.2
+   */
+  static final class ComparisonInt32Op implements DynamicBool {
+
+    private final DynamicProto.ComparisonInt32Op mImpl;
+    @Nullable
+    private final Fingerprint mFingerprint;
+
+    ComparisonInt32Op(DynamicProto.ComparisonInt32Op impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the left hand side of the comparison operation.
+     *
+     * @since 1.2
+     */
+    @Nullable
+    public DynamicInt32 getInputLhs() {
+      if (mImpl.hasInputLhs()) {
+        return DynamicBuilders.dynamicInt32FromProto(mImpl.getInputLhs());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the right hand side of the comparison operation.
+     *
+     * @since 1.2
+     */
+    @Nullable
+    public DynamicInt32 getInputRhs() {
+      if (mImpl.hasInputRhs()) {
+        return DynamicBuilders.dynamicInt32FromProto(mImpl.getInputRhs());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the type of the operation.
+     *
+     * @since 1.2
+     */
+    @ComparisonOpType
+    public int getOperationType() {
+      return mImpl.getOperationType().getNumber();
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static ComparisonInt32Op fromProto(@NonNull DynamicProto.ComparisonInt32Op proto) {
+      return new ComparisonInt32Op(proto, null);
+    }
+
+    @NonNull
+    DynamicProto.ComparisonInt32Op toProto() {
+      return mImpl;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public DynamicProto.DynamicBool toDynamicBoolProto() {
+      return DynamicProto.DynamicBool.newBuilder().setInt32Comparison(mImpl).build();
+    }
+
+    /**
+     * Builder for {@link ComparisonInt32Op}.
+     */
+    public static final class Builder implements DynamicBool.Builder {
+
+      private final DynamicProto.ComparisonInt32Op.Builder mImpl =
+          DynamicProto.ComparisonInt32Op.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-1112207999);
+
+      public Builder() {
+      }
+
+      /**
+       * Sets the left hand side of the comparison operation.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setInputLhs(@NonNull DynamicInt32 inputLhs) {
+        mImpl.setInputLhs(inputLhs.toDynamicInt32Proto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(inputLhs.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the right hand side of the comparison operation.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setInputRhs(@NonNull DynamicInt32 inputRhs) {
+        mImpl.setInputRhs(inputRhs.toDynamicInt32Proto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(inputRhs.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the type of the operation.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setOperationType(@ComparisonOpType int operationType) {
+        mImpl.setOperationType(DynamicProto.ComparisonOpType.forNumber(operationType));
+        mFingerprint.recordPropertyUpdate(3, operationType);
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public ComparisonInt32Op build() {
+        return new ComparisonInt32Op(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * A comparison operation, operating on two Float instances. This implements various comparison
+   * operations of the form "boolean result = LHS <op> RHS", where the available operation types are
+   * described in {@code ComparisonOpType}.
+   *
+   * @since 1.2
+   */
+  static final class ComparisonFloatOp implements DynamicBool {
+
+    private final DynamicProto.ComparisonFloatOp mImpl;
+    @Nullable
+    private final Fingerprint mFingerprint;
+
+    ComparisonFloatOp(DynamicProto.ComparisonFloatOp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the left hand side of the comparison operation.
+     *
+     * @since 1.2
+     */
+    @Nullable
+    public DynamicFloat getInputLhs() {
+      if (mImpl.hasInputLhs()) {
+        return DynamicBuilders.dynamicFloatFromProto(mImpl.getInputLhs());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the right hand side of the comparison operation.
+     *
+     * @since 1.2
+     */
+    @Nullable
+    public DynamicFloat getInputRhs() {
+      if (mImpl.hasInputRhs()) {
+        return DynamicBuilders.dynamicFloatFromProto(mImpl.getInputRhs());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the type of the operation.
+     *
+     * @since 1.2
+     */
+    @ComparisonOpType
+    public int getOperationType() {
+      return mImpl.getOperationType().getNumber();
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static ComparisonFloatOp fromProto(@NonNull DynamicProto.ComparisonFloatOp proto) {
+      return new ComparisonFloatOp(proto, null);
+    }
+
+    @NonNull
+    DynamicProto.ComparisonFloatOp toProto() {
+      return mImpl;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public DynamicProto.DynamicBool toDynamicBoolProto() {
+      return DynamicProto.DynamicBool.newBuilder().setFloatComparison(mImpl).build();
+    }
+
+    /**
+     * Builder for {@link ComparisonFloatOp}.
+     */
+    public static final class Builder implements DynamicBool.Builder {
+
+      private final DynamicProto.ComparisonFloatOp.Builder mImpl =
+          DynamicProto.ComparisonFloatOp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-1679565270);
+
+      public Builder() {
+      }
+
+      /**
+       * Sets the left hand side of the comparison operation.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setInputLhs(@NonNull DynamicFloat inputLhs) {
+        mImpl.setInputLhs(inputLhs.toDynamicFloatProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(inputLhs.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the right hand side of the comparison operation.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setInputRhs(@NonNull DynamicFloat inputRhs) {
+        mImpl.setInputRhs(inputRhs.toDynamicFloatProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(inputRhs.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the type of the operation.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setOperationType(@ComparisonOpType int operationType) {
+        mImpl.setOperationType(DynamicProto.ComparisonOpType.forNumber(operationType));
+        mFingerprint.recordPropertyUpdate(3, operationType);
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public ComparisonFloatOp build() {
+        return new ComparisonFloatOp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
    * A boolean operation which implements a "NOT" operator, i.e. "boolean result = !input".
    *
    * @since 1.2
@@ -2044,7 +4411,7 @@
     }
 
     /**
-     * Gets the input, whose value to negate. Intended for testing purposes only.
+     * Gets the input, whose value to negate.
      *
      * @since 1.2
      */
@@ -2083,6 +4450,12 @@
       return DynamicProto.DynamicBool.newBuilder().setNotOp(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "NotBoolOp{" + "input=" + getInput() + "}";
+    }
+
     /** Builder for {@link NotBoolOp}. */
     public static final class Builder implements DynamicBool.Builder {
       private final DynamicProto.NotBoolOp.Builder mImpl = DynamicProto.NotBoolOp.newBuilder();
@@ -2127,7 +4500,7 @@
     }
 
     /**
-     * Gets the left hand side of the logical operation. Intended for testing purposes only.
+     * Gets the left hand side of the logical operation.
      *
      * @since 1.2
      */
@@ -2141,7 +4514,7 @@
     }
 
     /**
-     * Gets the right hand side of the logical operation. Intended for testing purposes only.
+     * Gets the right hand side of the logical operation.
      *
      * @since 1.2
      */
@@ -2155,7 +4528,7 @@
     }
 
     /**
-     * Gets the operation type to apply to LHS/RHS. Intended for testing purposes only.
+     * Gets the operation type to apply to LHS/RHS.
      *
      * @since 1.2
      */
@@ -2190,6 +4563,19 @@
       return DynamicProto.DynamicBool.newBuilder().setLogicalOp(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "LogicalBoolOp{"
+          + "inputLhs="
+          + getInputLhs()
+          + ", inputRhs="
+          + getInputRhs()
+          + ", operationType="
+          + getOperationType()
+          + "}";
+    }
+
     /** Builder for {@link LogicalBoolOp}. */
     public static final class Builder implements DynamicBool.Builder {
       private final DynamicProto.LogicalBoolOp.Builder mImpl =
@@ -2259,6 +4645,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 +4737,8 @@
     @Nullable
     Fingerprint getFingerprint();
 
-    /** Builder to create {@link DynamicBool} objects.
+    /**
+     * Builder to create {@link DynamicBool} objects.
      *
      * @hide
      */
@@ -2358,12 +4766,18 @@
     if (proto.hasStateSource()) {
       return StateBoolSource.fromProto(proto.getStateSource());
     }
+    if (proto.hasInt32Comparison()) {
+      return ComparisonInt32Op.fromProto(proto.getInt32Comparison());
+    }
     if (proto.hasNotOp()) {
       return NotBoolOp.fromProto(proto.getNotOp());
     }
     if (proto.hasLogicalOp()) {
       return LogicalBoolOp.fromProto(proto.getLogicalOp());
     }
+    if (proto.hasFloatComparison()) {
+      return ComparisonFloatOp.fromProto(proto.getFloatComparison());
+    }
     throw new IllegalStateException("Proto was not a recognised instance of DynamicBool");
   }
 
@@ -2382,7 +4796,7 @@
     }
 
     /**
-     * Gets the key in the state to bind to. Intended for testing purposes only.
+     * Gets the key in the state to bind to.
      *
      * @since 1.2
      */
@@ -2417,6 +4831,12 @@
       return DynamicProto.DynamicColor.newBuilder().setStateSource(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "StateColorSource{" + "sourceKey=" + getSourceKey() + "}";
+    }
+
     /** Builder for {@link StateColorSource}. */
     public static final class Builder implements DynamicColor.Builder {
       private final DynamicProto.StateColorSource.Builder mImpl =
@@ -2461,8 +4881,7 @@
     }
 
     /**
-     * Gets the color value (in ARGB format) to start animating from. Intended for testing purposes
-     * only.
+     * Gets the color value (in ARGB format) to start animating from.
      *
      * @since 1.2
      */
@@ -2472,7 +4891,7 @@
     }
 
     /**
-     * Gets the color value (in ARGB format) to animate to. Intended for testing purposes only.
+     * Gets the color value (in ARGB format) to animate to.
      *
      * @since 1.2
      */
@@ -2482,7 +4901,7 @@
     }
 
     /**
-     * Gets the animation parameters for duration, delay, etc. Intended for testing purposes only.
+     * Gets the animation parameters for duration, delay, etc.
      *
      * @since 1.2
      */
@@ -2521,6 +4940,19 @@
       return DynamicProto.DynamicColor.newBuilder().setAnimatableFixed(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "AnimatableFixedColor{"
+          + "fromArgb="
+          + getFromArgb()
+          + ", toArgb="
+          + getToArgb()
+          + ", spec="
+          + getSpec()
+          + "}";
+    }
+
     /** Builder for {@link AnimatableFixedColor}. */
     public static final class Builder implements DynamicColor.Builder {
       private final DynamicProto.AnimatableFixedColor.Builder mImpl =
@@ -2596,7 +5028,7 @@
     }
 
     /**
-     * Gets the value to watch, and animate when it changes. Intended for testing purposes only.
+     * Gets the value to watch, and animate when it changes.
      *
      * @since 1.2
      */
@@ -2610,7 +5042,7 @@
     }
 
     /**
-     * Gets the animation parameters for duration, delay, etc. Intended for testing purposes only.
+     * Gets the animation parameters for duration, delay, etc.
      *
      * @since 1.2
      */
@@ -2649,6 +5081,12 @@
       return DynamicProto.DynamicColor.newBuilder().setAnimatableDynamic(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "AnimatableDynamicColor{" + "input=" + getInput() + ", spec=" + getSpec() + "}";
+    }
+
     /** Builder for {@link AnimatableDynamicColor}. */
     public static final class Builder implements DynamicColor.Builder {
       private final DynamicProto.AnimatableDynamicColor.Builder mImpl =
@@ -2706,6 +5144,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 +5271,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..4ca0ed2 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() {}
@@ -90,6 +91,12 @@
       return StateEntryProto.StateEntryValue.newBuilder().setInt32Val(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "FixedInt32{" + "value=" + getValue() + "}";
+    }
+
     /** Builder for {@link FixedInt32}. */
     public static final class Builder
         implements DynamicBuilders.DynamicInt32.Builder,
@@ -178,6 +185,12 @@
       return StateEntryProto.StateEntryValue.newBuilder().setStringVal(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "FixedString{" + "value=" + getValue() + "}";
+    }
+
     /** Builder for {@link FixedString}. */
     public static final class Builder
         implements DynamicBuilders.DynamicString.Builder,
@@ -265,6 +278,12 @@
       return StateEntryProto.StateEntryValue.newBuilder().setFloatVal(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "FixedFloat{" + "value=" + getValue() + "}";
+    }
+
     /** Builder for {@link FixedFloat}. */
     public static final class Builder
         implements DynamicBuilders.DynamicFloat.Builder,
@@ -352,6 +371,12 @@
       return StateEntryProto.StateEntryValue.newBuilder().setBoolVal(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "FixedBool{" + "value=" + getValue() + "}";
+    }
+
     /** Builder for {@link FixedBool}. */
     public static final class Builder
         implements DynamicBuilders.DynamicBool.Builder, StateEntryBuilders.StateEntryValue.Builder {
@@ -440,6 +465,12 @@
       return StateEntryProto.StateEntryValue.newBuilder().setColorVal(mImpl).build();
     }
 
+    @Override
+    @NonNull
+    public String toString() {
+      return "FixedColor{" + "argb=" + getArgb() + "}";
+    }
+
     /** Builder for {@link FixedColor}. */
     public static final class Builder
         implements DynamicBuilders.DynamicColor.Builder,
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/AnimationSpecTest.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/AnimationSpecTest.java
new file mode 100644
index 0000000..7f2ec39
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/AnimationSpecTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.expression;
+
+import static androidx.wear.protolayout.expression.AnimationParameterBuilders.REPEAT_MODE_RESTART;
+import static androidx.wear.protolayout.expression.AnimationParameterBuilders.REPEAT_MODE_REVERSE;
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec;
+import androidx.wear.protolayout.expression.AnimationParameterBuilders.CubicBezierEasing;
+import androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class AnimationSpecTest {
+  @Test
+  public void animationSpecToString() {
+    assertThat(
+            new AnimationSpec.Builder()
+                .setDurationMillis(1)
+                .setDelayMillis(2)
+                .setEasing(new CubicBezierEasing.Builder().setX1(3f).build())
+                .setRepeatable(new Repeatable.Builder().setIterations(4).build())
+                .build()
+                .toString())
+        .isEqualTo(
+            "AnimationSpec{durationMillis=1, delayMillis=2, "
+                + "easing=CubicBezierEasing{x1=3.0, y1=0.0, x2=0.0, y2=0.0}, "
+                + "repeatable=Repeatable{iterations=4, repeatMode=0}}");
+  }
+
+  @Test
+  public void cubicBezierEasingToString() {
+    assertThat(
+            new CubicBezierEasing.Builder()
+                .setX1(1f)
+                .setY1(2f)
+                .setX2(3f)
+                .setY2(4f)
+                .build()
+                .toString())
+        .isEqualTo("CubicBezierEasing{x1=1.0, y1=2.0, x2=3.0, y2=4.0}");
+  }
+
+  @Test
+  public void repeatableToString() {
+    assertThat(
+            new Repeatable.Builder()
+                .setIterations(10)
+                .setRepeatMode(REPEAT_MODE_RESTART)
+                .build()
+                .toString())
+        .isEqualTo("Repeatable{iterations=10, repeatMode=1}");
+  }
+}
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..7c51d3e 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,107 @@
 
 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 constantToString() {
+    assertThat(DynamicBool.constant(true).toString()).isEqualTo("FixedBool{value=true}");
+  }
 
-        assertThat(stateBool.toDynamicBoolProto().getStateSource().getSourceKey()).isEqualTo(
-                STATE_KEY);
-    }
+  @Test
+  public void stateEntryValueBool() {
+    DynamicBool stateBool = DynamicBool.fromState(STATE_KEY);
 
-    @Test
-    public void andOpBool() {
-        DynamicBool firstBool = DynamicBool.constant(false);
-        DynamicBool secondBool = DynamicBool.constant(true);
+    assertThat(stateBool.toDynamicBoolProto().getStateSource().getSourceKey()).isEqualTo(STATE_KEY);
+  }
 
-        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 stateToString() {
+    assertThat(DynamicBool.fromState("key").toString()).isEqualTo("StateBoolSource{sourceKey=key}");
+  }
 
-    @Test
-    public void orOpBool() {
-        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.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.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());
+  }
 
-    public void negateOpBool() {
-        DynamicBool firstBool = DynamicBool.constant(true);
+  @Test
+  public void orOpBool() {
+    DynamicBool firstBool = DynamicBool.constant(false);
+    DynamicBool secondBool = DynamicBool.constant(true);
 
-        assertThat(firstBool.isTrue().toDynamicBoolProto()).isEqualTo(firstBool);
-        assertThat(firstBool.toDynamicBoolProto().getNotOp().getInput())
-                .isEqualTo(firstBool);
-    }
+    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());
+  }
+
+  @Test
+  public void logicalOpToString() {
+    assertThat(DynamicBool.constant(true).and(DynamicBool.constant(false)).toString())
+        .isEqualTo(
+            "LogicalBoolOp{"
+                + "inputLhs=FixedBool{value=true}, "
+                + "inputRhs=FixedBool{value=false}, "
+                + "operationType=1}");
+  }
+
+  @Test
+  public void negateOpBool() {
+    DynamicBool firstBool = DynamicBool.constant(true);
+
+    assertThat(firstBool.isTrue().toDynamicBoolProto()).isEqualTo(firstBool.toDynamicBoolProto());
+    assertThat(firstBool.isFalse().toDynamicBoolProto().getNotOp().getInput())
+        .isEqualTo(firstBool.toDynamicBoolProto());
+  }
+
+  @Test
+  public void logicalToString() {
+    assertThat(DynamicBool.constant(true).isFalse().toString())
+        .isEqualTo("NotBoolOp{input=FixedBool{value=true}}");
+  }
+
+  @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..b3d3eda 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,124 @@
 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 constantToString() {
+    assertThat(DynamicColor.constant(0x00000001).toString()).isEqualTo("FixedColor{argb=1}");
+  }
 
-        assertThat(stateColor.toDynamicColorProto().getStateSource().getSourceKey()).isEqualTo(
-                STATE_KEY);
-    }
+  @Test
+  public void stateEntryValueColor() {
+    DynamicColor stateColor = DynamicColor.fromState(STATE_KEY);
 
-    @Test
-    public void rangeAnimatedColor() {
-        int startColor = 0xff00ff00;
-        int endColor = 0xff00ffff;
+    assertThat(stateColor.toDynamicColorProto().getStateSource().getSourceKey())
+        .isEqualTo(STATE_KEY);
+  }
 
-        DynamicColor animatedColor = DynamicColor.animate(startColor, endColor);
-        DynamicColor animatedColorWithSpec = DynamicColor.animate(startColor, endColor, SPEC);
+  @Test
+  public void stateToString() {
+    assertThat(DynamicColor.fromState("key").toString())
+        .isEqualTo("StateColorSource{sourceKey=key}");
+  }
 
-        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 rangeAnimatedColor() {
+    int startColor = 0xff00ff00;
+    int endColor = 0xff00ffff;
 
-    @Test
-    public void stateAnimatedColor() {
-        DynamicColor stateColor = DynamicColor.fromState(STATE_KEY);
+    DynamicColor animatedColor = DynamicColor.animate(startColor, endColor);
+    DynamicColor animatedColorWithSpec = DynamicColor.animate(startColor, endColor, SPEC);
 
-        DynamicColor animatedColor = DynamicColor.animate(STATE_KEY);
-        DynamicColor animatedColorWithSpec = DynamicColor.animate(STATE_KEY, 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().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 rangeAnimatedToString() {
+    assertThat(
+            DynamicColor.animate(
+                    /* start= */ 0x00000001,
+                    /* end= */ 0x00000002,
+                    new AnimationSpec.Builder().setDelayMillis(0).build())
+                .toString())
+        .isEqualTo(
+            "AnimatableFixedColor{"
+                + "fromArgb=1, toArgb=2, spec=AnimationSpec{"
+                + "durationMillis=0, delayMillis=0, easing=null, repeatable=null}}");
+  }
+
+  @Test
+  public void stateAnimatedColor() {
+    DynamicColor stateColor = DynamicColor.fromState(STATE_KEY);
+
+    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());
+  }
+
+  @Test
+  public void stateAnimatedToString() {
+    assertThat(
+            DynamicColor.animate(
+                    /* stateKey= */ "key", new AnimationSpec.Builder().setDelayMillis(1).build())
+                .toString())
+        .isEqualTo(
+            "AnimatableDynamicColor{"
+                + "input=StateColorSource{sourceKey=key}, spec=AnimationSpec{"
+                + "durationMillis=0, delayMillis=1, easing=null, repeatable=null}}");
+  }
+
+  @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..731406e 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,202 @@
 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 constantToString() {
+    assertThat(DynamicFloat.constant(1f).toString()).isEqualTo("FixedFloat{value=1.0}");
+  }
 
-        assertThat(stateFloat.toDynamicFloatProto().getStateSource().getSourceKey()).isEqualTo(
-                STATE_KEY);
-    }
+  @Test
+  public void stateEntryValueFloat() {
+    DynamicFloat stateFloat = DynamicFloat.fromState(STATE_KEY);
 
-    @Test
-    public void constantFloat_asInt() {
-        DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
+    assertThat(stateFloat.toDynamicFloatProto().getStateSource().getSourceKey())
+        .isEqualTo(STATE_KEY);
+  }
 
-        DynamicInt32 dynamicInt32 = constantFloat.asInt();
+  @Test
+  public void stateToString() {
+    assertThat(DynamicFloat.fromState("key").toString())
+        .isEqualTo("StateFloatSource{sourceKey=key}");
+  }
 
-        assertThat(dynamicInt32.toDynamicInt32Proto().getFloatToInt()
-                .getInput().getFixed().getValue()).isWithin(0.0001f).of(CONSTANT_VALUE);
-    }
+  @Test
+  public void constantFloat_asInt() {
+    DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
 
-    @Test
-    public void formatFloat_defaultParameters() {
-        DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
+    DynamicInt32 dynamicInt32 = constantFloat.asInt();
 
-        DynamicString defaultFormat = constantFloat.format();
+    assertThat(dynamicInt32.toDynamicInt32Proto().getFloatToInt().getInput().getFixed().getValue())
+        .isWithin(0.0001f)
+        .of(CONSTANT_VALUE);
+  }
 
-        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);
+  @Test
+  public void constantFloat_asIntToString() {
+    assertThat(DynamicFloat.constant(1f).asInt().toString())
+        .isEqualTo("FloatToInt32Op{input=FixedFloat{value=1.0}, roundMode=1}");
+  }
 
-        DynamicString customFormat = constantFloat.format(floatFormatter);
+  @Test
+  public void formatFloat_defaultParameters() {
+    DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
 
-        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 defaultFormat = constantFloat.format();
 
-    @Test
-    public void rangeAnimatedFloat() {
-        float startFloat = 100f;
-        float endFloat = 200f;
+    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();
+  }
 
-        DynamicFloat animatedFloat = DynamicFloat.animate(startFloat,
-                endFloat);
-        DynamicFloat animatedFloatWithSpec = DynamicFloat.animate(startFloat, endFloat, SPEC);
+  @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);
 
-        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());
-    }
+    DynamicString customFormat = constantFloat.format(floatFormatter);
 
-    @Test
-    public void stateAnimatedFloat() {
-        DynamicFloat stateFloat = DynamicFloat.fromState(STATE_KEY);
+    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 animatedColor = DynamicFloat.animate(STATE_KEY);
-        DynamicFloat animatedColorWithSpec = DynamicFloat.animate(STATE_KEY, SPEC);
+  @Test
+  public void formatToString() {
+    assertThat(
+            DynamicFloat.constant(1f)
+                .format(
+                    DynamicFloat.FloatFormatter.with()
+                        .maxFractionDigits(2)
+                        .minFractionDigits(3)
+                        .minIntegerDigits(4)
+                        .groupingUsed(true))
+                .toString())
+        .isEqualTo(
+            "FloatFormatOp{input=FixedFloat{value=1.0}, maxFractionDigits=2, "
+                + "minFractionDigits=3, minIntegerDigits=4, groupingUsed=true}");
+  }
 
-        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());
-    }
+  @Test
+  public void rangeAnimatedFloat() {
+    float startFloat = 100f;
+    float endFloat = 200f;
+
+    DynamicFloat animatedFloat = DynamicFloat.animate(startFloat, endFloat);
+    DynamicFloat animatedFloatWithSpec = DynamicFloat.animate(startFloat, endFloat, SPEC);
+
+    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());
+  }
+
+  @Test
+  public void rangeAnimatedToString() {
+    assertThat(
+            DynamicFloat.animate(
+                    /* start= */ 1f,
+                    /* end= */ 2f,
+                    new AnimationSpec.Builder().setDelayMillis(0).build())
+                .toString())
+        .isEqualTo(
+            "AnimatableFixedFloat{fromValue=1.0, toValue=2.0, spec=AnimationSpec{"
+                + "durationMillis=0, delayMillis=0, easing=null, repeatable=null}}");
+  }
+
+  @Test
+  public void stateAnimatedFloat() {
+    DynamicFloat stateFloat = DynamicFloat.fromState(STATE_KEY);
+
+    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 stateAnimatedToString() {
+    assertThat(
+            DynamicFloat.animate(
+                    /* stateKey= */ "key", new AnimationSpec.Builder().setDelayMillis(1).build())
+                .toString())
+        .isEqualTo(
+            "AnimatableDynamicFloat{"
+                + "input=StateFloatSource{sourceKey=key}, spec=AnimationSpec{"
+                + "durationMillis=0, delayMillis=1, easing=null, repeatable=null}}");
+  }
+
+  @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..416e35b 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,120 @@
 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 constantToString() {
+    assertThat(DynamicInt32.constant(1).toString()).isEqualTo("FixedInt32{value=1}");
+  }
 
-        assertThat(stateInt32.toDynamicInt32Proto().getStateSource().getSourceKey()).isEqualTo(
-                STATE_KEY);
-    }
+  @Test
+  public void stateEntryValueInt32() {
+    DynamicInt32 stateInt32 = DynamicInt32.fromState(STATE_KEY);
 
-    @Test
-    public void constantInt32_asFloat() {
-        DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
+    assertThat(stateInt32.toDynamicInt32Proto().getStateSource().getSourceKey())
+        .isEqualTo(STATE_KEY);
+  }
 
-        DynamicFloat dynamicFloat = constantInt32.asFloat();
+  @Test
+  public void stateToString() {
+    assertThat(DynamicInt32.fromState("key").toString())
+        .isEqualTo("StateInt32Source{sourceKey=key}");
+  }
 
-        assertThat(dynamicFloat.toDynamicFloatProto().getInt32ToFloatOperation()
-                .getInput().getFixed().getValue()).isEqualTo(CONSTANT_VALUE);
-    }
+  @Test
+  public void constantInt32_asFloat() {
+    DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
 
-    public void formatInt32_defaultParameters() {
-        DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
+    DynamicFloat dynamicFloat = constantInt32.asFloat();
 
-        DynamicBuilders.DynamicString defaultFormat = constantInt32.format();
+    assertThat(
+            dynamicFloat
+                .toDynamicFloatProto()
+                .getInt32ToFloatOperation()
+                .getInput()
+                .getFixed()
+                .getValue())
+        .isEqualTo(CONSTANT_VALUE);
+  }
 
-        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);
+  @Test
+  public void constantInt32_asFloatToString() {
+    assertThat(DynamicInt32.constant(1).asFloat().toString())
+        .isEqualTo("Int32ToFloatOp{input=FixedInt32{value=1}}");
+  }
 
-        DynamicBuilders.DynamicString customFormat = constantInt32.format(intFormatter);
+  @Test
+  public void formatInt32_defaultParameters() {
+    DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
 
-        DynamicProto.Int32FormatOp int32FormatOp =
-                customFormat.toDynamicStringProto().getInt32FormatOp();
-        assertThat(int32FormatOp.getInput()).isEqualTo(constantInt32.toDynamicInt32Proto());
-        assertThat(int32FormatOp.getGroupingUsed()).isEqualTo(groupingUsed);
-        assertThat(int32FormatOp.getMinIntegerDigits()).isEqualTo(minIntegerDigits);
-    }
+    DynamicBuilders.DynamicString defaultFormat = constantInt32.format();
+
+    DynamicProto.Int32FormatOp int32FormatOp =
+        defaultFormat.toDynamicStringProto().getInt32FormatOp();
+    assertThat(int32FormatOp.getInput()).isEqualTo(constantInt32.toDynamicInt32Proto());
+    assertThat(int32FormatOp.getGroupingUsed()).isFalse();
+    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);
+
+    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 formatToString() {
+    assertThat(
+            DynamicInt32.constant(1)
+                .format(DynamicInt32.IntFormatter.with().minIntegerDigits(2).groupingUsed(true))
+                .toString())
+        .isEqualTo(
+            "Int32FormatOp{input=FixedInt32{value=1}, minIntegerDigits=2, groupingUsed=true}");
+  }
+
+  @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..b0f2be1 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,118 @@
 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 constantToString() {
+    assertThat(DynamicString.constant("a").toString()).isEqualTo("FixedString{value=a}");
+  }
 
-        assertThat(stateString.toDynamicStringProto().getStateSource().getSourceKey()).isEqualTo(
-                STATE_KEY);
-    }
+  @Test
+  public void stateEntryValueString() {
+    DynamicString stateString = DynamicString.fromState(STATE_KEY);
 
-    @Test
-    public void constantString_concat() {
-        DynamicString firstString = DynamicString.constant(CONSTANT_VALUE);
-        DynamicString secondString = DynamicString.constant(CONSTANT_VALUE);
+    assertThat(stateString.toDynamicStringProto().getStateSource().getSourceKey())
+        .isEqualTo(STATE_KEY);
+  }
 
-        DynamicString resultString = firstString.concat(secondString);
+  @Test
+  public void stateToString() {
+    assertThat(DynamicString.fromState("key").toString())
+        .isEqualTo("StateStringSource{sourceKey=key}");
+  }
 
-        DynamicProto.ConcatStringOp concatOp = resultString.toDynamicStringProto().getConcatOp();
-        assertThat(concatOp.getInputLhs()).isEqualTo(firstString.toDynamicStringProto());
-        assertThat(concatOp.getInputRhs()).isEqualTo(secondString.toDynamicStringProto());
-    }
+  @Test
+  public void constantString_concat() {
+    DynamicString firstString = DynamicString.constant(CONSTANT_VALUE);
+    DynamicString secondString = DynamicString.constant(CONSTANT_VALUE);
 
-    @Test
-    public void constantString_conditional() {
-        DynamicBuilders.DynamicBool condition = DynamicBuilders.DynamicBool.constant(true);
-        DynamicString firstString = DynamicString.constant(CONSTANT_VALUE);
-        DynamicString secondString = DynamicString.constant(CONSTANT_VALUE);
+    DynamicString resultString = firstString.concat(secondString);
 
-        DynamicString resultString =
-                DynamicString.onCondition(condition).use(firstString).elseUse(secondString);
+    DynamicProto.ConcatStringOp concatOp = resultString.toDynamicStringProto().getConcatOp();
+    assertThat(concatOp.getInputLhs()).isEqualTo(firstString.toDynamicStringProto());
+    assertThat(concatOp.getInputRhs()).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 concatToString() {
+    assertThat(DynamicString.constant("a").concat(DynamicString.constant("b")).toString())
+        .isEqualTo("ConcatStringOp{inputLhs=FixedString{value=a}, inputRhs=FixedString{value=b}}");
+  }
 
-    @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 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().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()).isEqualTo(firstString.toDynamicStringProto());
+    assertThat(conditionalOp.getValueIfFalse()).isEqualTo(secondString.toDynamicStringProto());
+  }
+
+  @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);
+
+    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 conditionalToString() {
+    assertThat(
+            DynamicString.onCondition(DynamicBool.constant(true)).use("a").elseUse("b").toString())
+        .isEqualTo(
+            "ConditionalStringOp{"
+                + "condition=FixedBool{value=true}, "
+                + "valueIfTrue=FixedString{value=a}, "
+                + "valueIfFalse=FixedString{value=b}}");
+  }
+
+  @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/protolayout/protolayout-proto/src/main/proto/action.proto b/wear/protolayout/protolayout-proto/src/main/proto/action.proto
index 9406086..72e7e95 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/action.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/action.proto
@@ -4,6 +4,7 @@
 package androidx.wear.protolayout.proto;
 
 import "state.proto";
+import "state_entry.proto";
 
 option java_package = "androidx.wear.protolayout.proto";
 option java_outer_classname = "ActionProto";
@@ -72,10 +73,10 @@
   AndroidActivity android_activity = 1;
 }
 
-// An action used to load (or reload) the tile contents.
+// An action used to load (or reload) the layout contents.
 message LoadAction {
-  // The state to load the next tile with. This will be included in the
-  // TileRequest sent after this action is invoked by a Clickable.
+  // The state to load the next layout with. This will be included in the
+  // layout request sent after this action is invoked by a Clickable.
   State request_state = 1;
 }
 
@@ -86,3 +87,19 @@
     LoadAction load_action = 2;
   }
 }
+
+// An action that is handled internal to the current layout and won't cause a
+// layout refresh.
+message LocalAction {
+  oneof value {
+    SetStateAction set_state = 1;
+  }
+}
+
+// An action that sets a new value for a State
+message SetStateAction {
+  // The target key of the state item for this action.
+  string target_key = 1;
+  // The value to set the state item to, when this action is executed.
+  androidx.wear.protolayout.expression.proto.StateEntryValue value = 2;
+}
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/alignment.proto b/wear/protolayout/protolayout-proto/src/main/proto/alignment.proto
new file mode 100644
index 0000000..aaec628
--- /dev/null
+++ b/wear/protolayout/protolayout-proto/src/main/proto/alignment.proto
@@ -0,0 +1,150 @@
+// Alignment messages and enumerations, for use in other elements.
+syntax = "proto3";
+
+package androidx.wear.protolayout.proto;
+
+option java_package = "androidx.wear.protolayout.proto";
+option java_outer_classname = "AlignmentProto";
+
+// The horizontal alignment of an element within its container.
+enum HorizontalAlignment {
+  // Horizontal alignment is undefined.
+  HORIZONTAL_ALIGN_UNDEFINED = 0;
+
+  // Horizontally align to the left.
+  HORIZONTAL_ALIGN_LEFT = 1;
+
+  // Horizontally align to center.
+  HORIZONTAL_ALIGN_CENTER = 2;
+
+  // Horizontally align to the right.
+  HORIZONTAL_ALIGN_RIGHT = 3;
+
+  // Horizontally align to the content start (left in LTR layouts, right in RTL
+  // layouts).
+  HORIZONTAL_ALIGN_START = 4;
+
+  // Horizontally align to the content end (right in LTR layouts, left in RTL
+  // layouts).
+  HORIZONTAL_ALIGN_END = 5;
+}
+
+// An extensible HorizontalAlignment property.
+message HorizontalAlignmentProp {
+  // The value
+  HorizontalAlignment value = 1;
+}
+
+// The vertical alignment of an element within its container.
+enum VerticalAlignment {
+  // Vertical alignment is undefined.
+  VERTICAL_ALIGN_UNDEFINED = 0;
+
+  // Vertically align to the top.
+  VERTICAL_ALIGN_TOP = 1;
+
+  // Vertically align to center.
+  VERTICAL_ALIGN_CENTER = 2;
+
+  // Vertically align to the bottom.
+  VERTICAL_ALIGN_BOTTOM = 3;
+}
+
+// An extensible VerticalAlignment property.
+message VerticalAlignmentProp {
+  // The value.
+  VerticalAlignment value = 1;
+}
+
+// Alignment of a text element.
+enum TextAlignment {
+  // Alignment is undefined.
+  TEXT_ALIGN_UNDEFINED = 0;
+
+  // Align to the "start" of the Text element (left in LTR layouts, right in
+  // RTL layouts).
+  TEXT_ALIGN_START = 1;
+
+  // Align to the center of the Text element.
+  TEXT_ALIGN_CENTER = 2;
+
+  // Align to the "end" of the Text element (right in LTR layouts, left in RTL
+  // layouts).
+  TEXT_ALIGN_END = 3;
+}
+
+// An extensible TextAlignment property.
+message TextAlignmentProp {
+  // The value.
+  TextAlignment value = 1;
+}
+
+// The anchor position of an Arc's elements. This is used to specify how
+// elements added to an Arc should be laid out with respect to anchor_angle.
+//
+// As an example, assume that the following diagrams are wrapped to an arc, and
+// each represents an Arc element containing a single Text element. The Text
+// element's anchor_angle is "0" for all cases.
+//
+// ```
+// ARC_ANCHOR_START:
+// -180                                0                                    180
+//                                     Hello World!
+//
+//
+// ARC_ANCHOR_CENTER:
+// -180                                0                                    180
+//                                Hello World!
+//
+// ARC_ANCHOR_END:
+// -180                                0                                    180
+//                          Hello World!
+// ```
+enum ArcAnchorType {
+  // Anchor position is undefined.
+  ARC_ANCHOR_UNDEFINED = 0;
+
+  // Anchor at the start of the elements. This will cause elements added to an
+  // arc to begin at the given anchor_angle, and sweep around to the right.
+  ARC_ANCHOR_START = 1;
+
+  // Anchor at the center of the elements. This will cause the center of the
+  // whole set of elements added to an arc to be pinned at the given
+  // anchor_angle.
+  ARC_ANCHOR_CENTER = 2;
+
+  // Anchor at the end of the elements. This will cause the set of elements
+  // inside the arc to end at the specified anchor_angle, i.e. all elements
+  // should be to the left of anchor_angle.
+  ARC_ANCHOR_END = 3;
+}
+
+// An extensible ArcAnchorType property.
+message ArcAnchorTypeProp {
+  // The value.
+  ArcAnchorType value = 1;
+}
+
+// How to lay out components in a Arc context when they are smaller than their
+// container. This would be similar to HorizontalAlignment in a Box or Column.
+enum AngularAlignment {
+  // Angular alignment is undefined.
+  ANGULAR_ALIGNMENT_UNDEFINED = 0;
+
+  // Align to the start of the container. As an example, if the container
+  // starts at 90 degrees and has 180 degrees of sweep, the element within
+  // would draw from 90 degrees, clockwise.
+  ANGULAR_ALIGNMENT_START = 1;
+
+  // Align to the center of the container. As an example, if the container
+  // starts at 90 degrees, and has 180 degrees of sweep, and the contained
+  // element has 90 degrees of sweep, the element would draw between 135 and
+  // 225 degrees.
+  ANGULAR_ALIGNMENT_CENTER = 2;
+
+  // Align to the end of the container. As an example, if the container
+  // starts at 90 degrees and has 180 degrees of sweep, and the contained
+  // element has 90 degrees of sweep, the element would draw between 180 and 270
+  // degrees.
+  ANGULAR_ALIGNMENT_END = 3;
+}
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/animation_parameters.proto b/wear/protolayout/protolayout-proto/src/main/proto/animation_parameters.proto
index 094a273..aee13fe 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/animation_parameters.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/animation_parameters.proto
@@ -16,7 +16,7 @@
   // 0.
   uint32 delay_millis = 2;
 
-  // The easing to be used for adjusting an animation’s fraction. If not set,
+  // The easing to be used for adjusting an animation's fraction. If not set,
   // defaults to Linear Interpolator.
   Easing easing = 3;
 
@@ -25,17 +25,17 @@
   Repeatable repeatable = 5;
 }
 
-// The easing to be used for adjusting an animation’s fraction. This allows
+// The easing to be used for adjusting an animation's fraction. This allows
 // animation to speed up and slow down, rather than moving at a constant rate.
 // If not set, defaults to Linear Interpolator.
 message Easing {
   oneof inner {
-    // The cubic polynomial easing that implements third-order Bézier curves.
+    // The cubic polynomial easing that implements third-order Bezier curves.
     CubicBezierEasing cubic_bezier = 1;
   }
 }
 
-// The cubic polynomial easing that implements third-order Bézier curves. This
+// The cubic polynomial easing that implements third-order Bezier curves. This
 // is equivalent to the Android PathInterpolator.
 message CubicBezierEasing {
   // The x coordinate of the first control point. The line through the point (0,
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/color.proto b/wear/protolayout/protolayout-proto/src/main/proto/color.proto
index f719a06..f5535d9 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/color.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/color.proto
@@ -3,11 +3,16 @@
 
 package androidx.wear.protolayout.proto;
 
+import "dynamic.proto";
+
 option java_package = "androidx.wear.protolayout.proto";
 option java_outer_classname = "ColorProto";
 
 // A property defining a color.
 message ColorProp {
   // The color value, in ARGB format.
-  uint32 argb = 1;
+  optional uint32 argb = 1;
+
+  // The dynamic value.
+  androidx.wear.protolayout.expression.proto.DynamicColor dynamic_value = 2;
 }
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/device_parameters.proto b/wear/protolayout/protolayout-proto/src/main/proto/device_parameters.proto
index e7a5e2a..25c9e37 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/device_parameters.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/device_parameters.proto
@@ -1,12 +1,14 @@
-// Request messages used to fetch tiles and resources
+// Request messages used to fetch layouts and resources
 syntax = "proto3";
 
 package androidx.wear.protolayout.proto;
 
+import "version.proto";
+
 option java_package = "androidx.wear.protolayout.proto";
 option java_outer_classname = "DeviceParametersProto";
 
-// The platform of the device requesting a tile.
+// The platform of the device requesting a layout.
 enum DevicePlatform {
   // Device platform is undefined.
   DEVICE_PLATFORM_UNDEFINED = 0;
@@ -27,7 +29,7 @@
   SCREEN_SHAPE_RECT = 2;
 }
 
-// Parameters describing the device requesting a tile update. This contains
+// Parameters describing the device requesting a layout update. This contains
 // physical and logical characteristics about the device (e.g. screen size and
 // density, etc).
 message DeviceParameters {
@@ -41,9 +43,28 @@
   // Pixels (px = dp * density).
   float screen_density = 3;
 
+  // Current user preference for the scaling factor for fonts displayed on the
+  // display. This value is used to get from SP to DP (dp = sp * font_scale).
+  float font_scale = 7;
+
   // The platform of the device.
   DevicePlatform device_platform = 4;
 
   // The shape of the device's screen
   ScreenShape screen_shape = 5;
+
+  // The maximum schema version supported by the current renderer.
+  VersionInfo renderer_schema_version = 6;
+
+  // Renderer supported capabilities
+  Capabilities capabilities = 8;
+}
+
+// Capabilities describing the features that the renderer supports.
+message Capabilities {
+  // Current minimum freshness limit in milliseconds. This can change based on
+  // various factors. Any freshness request lower than the current limit will be
+  // replaced by that limit. A value of 0 here signifies that the minimum
+  // freshness limit in unknown.
+  uint64 minimum_freshness_limit_millis = 1;
 }
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/dimension.proto b/wear/protolayout/protolayout-proto/src/main/proto/dimension.proto
index a0a9989..6c69366 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/dimension.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/dimension.proto
@@ -3,13 +3,34 @@
 
 package androidx.wear.protolayout.proto;
 
+import "alignment.proto";
+import "dynamic.proto";
+import "types.proto";
+
 option java_package = "androidx.wear.protolayout.proto";
 option java_outer_classname = "DimensionProto";
 
 // A type for linear dimensions, measured in dp.
 message DpProp {
   // The value, in dp.
-  float value = 1;
+  optional float value = 1;
+
+  // The dynamic value, in dp.
+  androidx.wear.protolayout.expression.proto.DynamicFloat dynamic_value = 2;
+
+  float value_for_layout = 3;
+
+  oneof align {
+    // Vertical alignment of the actual content within the space reserved by
+    // value_for_layout. Only valid when this DpProp is used for vertical
+    // sizing. If not specified, defaults to center alignment.
+    VerticalAlignment vertical_alignment_for_layout = 4;
+
+    // Horizontal alignment of the actual content within the space reserved by
+    // value_for_layout. Only valid when this DpProp is used for horizontal
+    // sizing. If not specified, defaults to center alignment.
+    HorizontalAlignment horizontal_alignment_for_layout = 5;
+  }
 }
 
 // A type for font sizes, measured in sp.
@@ -26,19 +47,62 @@
   float value = 1;
 }
 
+// The length of an ArcLine.
+message ArcLineLength {
+  oneof inner {
+    DegreesProp degrees = 1;
+    ExpandedAngularDimensionProp expanded_angular_dimension = 2;
+  }
+}
+
+// The length of an ArcSpacer.
+message ArcSpacerLength {
+  oneof inner {
+    DegreesProp degrees = 1;
+    ExpandedAngularDimensionProp expanded_angular_dimension = 2;
+  }
+}
+
+// A type for an angular dimension that fills all the space it can
+// (i.e. MATCH_PARENT in Android parlance)
+message ExpandedAngularDimensionProp {
+  // The layout weight (a dimensionless scalar value) for this element. The
+  // angular dimension for this element will be layout_weight times the
+  // available space divided by the sum of the layout_weights. If not set this
+  // defaults to 1.
+  FloatProp layout_weight = 1;
+}
+
 // A type for angular dimensions, measured in degrees.
 message DegreesProp {
   // The value, in degrees.
-  float value = 1;
+  optional float value = 1;
+
+  // The dynamic value, in degrees.
+  androidx.wear.protolayout.expression.proto.DynamicFloat dynamic_value = 2;
+
+  float value_for_layout = 3;
+  AngularAlignment angular_alignment_for_layout = 4;
 }
 
 // A type for a dimension that fills all the space it can (i.e. MATCH_PARENT in
 // Android parlance)
-message ExpandedDimensionProp {}
+message ExpandedDimensionProp {
+  // The layout weight (a dimensionless scalar value) for this element. This
+  // will only affect the width of children of a Row or the height of children
+  // of a Column. By default, all children have equal weight. Where applicable,
+  // the width or height of the element is proportional to the sum of the
+  // weights of its siblings.
+  FloatProp layout_weight = 1;
+}
 
 // A type for a dimension that sizes itself to the size of its children (i.e.
 // WRAP_CONTENT in Android parlance)
-message WrappedDimensionProp {}
+message WrappedDimensionProp {
+  // The minimum size of this dimension. If not set, then there is no minimum
+  // size.
+  DpProp minimum_size = 1;
+}
 
 // A type for a dimension that scales itself proportionally to another dimension
 // such that the aspect ratio defined by the given width and height values is
@@ -79,6 +143,5 @@
 message SpacerDimension {
   oneof inner {
     DpProp linear_dimension = 1;
-    // TODO(b/169137847): Add ExpandedDimensionProp
   }
 }
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/fixed.proto b/wear/protolayout/protolayout-proto/src/main/proto/fixed.proto
index 7a2a878..f09c002 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/fixed.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/fixed.proto
@@ -1,4 +1,5 @@
-// Fixed primitive types used by layout elements.
+// Fixed value primitive types that can be used in dynamic expressions and in
+// for state state values.
 syntax = "proto3";
 
 package androidx.wear.protolayout.expression.proto;
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/layout.proto b/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
index bd896ba..5307e19 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
@@ -4,64 +4,16 @@
 
 package androidx.wear.protolayout.proto;
 
+import "alignment.proto";
 import "color.proto";
 import "dimension.proto";
+import "fingerprint.proto";
 import "modifiers.proto";
 import "types.proto";
 
 option java_package = "androidx.wear.protolayout.proto";
 option java_outer_classname = "LayoutElementProto";
 
-// The horizontal alignment of an element within its container.
-enum HorizontalAlignment {
-  // Horizontal alignment is undefined.
-  HORIZONTAL_ALIGN_UNDEFINED = 0;
-
-  // Horizontally align to the left.
-  HORIZONTAL_ALIGN_LEFT = 1;
-
-  // Horizontally align to center.
-  HORIZONTAL_ALIGN_CENTER = 2;
-
-  // Horizontally align to the right.
-  HORIZONTAL_ALIGN_RIGHT = 3;
-
-  // Horizontally align to the content start (left in LTR layouts, right in RTL
-  // layouts).
-  HORIZONTAL_ALIGN_START = 4;
-
-  // Horizontally align to the content end (right in LTR layouts, left in RTL
-  // layouts).
-  HORIZONTAL_ALIGN_END = 5;
-}
-
-// An extensible HorizontalAlignment property.
-message HorizontalAlignmentProp {
-  // The value
-  HorizontalAlignment value = 1;
-}
-
-// The vertical alignment of an element within its container.
-enum VerticalAlignment {
-  // Vertical alignment is undefined.
-  VERTICAL_ALIGN_UNDEFINED = 0;
-
-  // Vertically align to the top.
-  VERTICAL_ALIGN_TOP = 1;
-
-  // Vertically align to center.
-  VERTICAL_ALIGN_CENTER = 2;
-
-  // Vertically align to the bottom.
-  VERTICAL_ALIGN_BOTTOM = 3;
-}
-
-// An extensible VerticalAlignment property.
-message VerticalAlignmentProp {
-  // The value.
-  VerticalAlignment value = 1;
-}
-
 // The weight to be applied to the font.
 enum FontWeight {
   // Font weight is undefined.
@@ -159,29 +111,6 @@
   FontVariantProp variant = 7;
 }
 
-// Alignment of a text element.
-enum TextAlignment {
-  // Alignment is undefined.
-  TEXT_ALIGN_UNDEFINED = 0;
-
-  // Align to the "start" of the Text element (left in LTR layouts, right in
-  // RTL layouts).
-  TEXT_ALIGN_START = 1;
-
-  // Align to the center of the Text element.
-  TEXT_ALIGN_CENTER = 2;
-
-  // Align to the "end" of the Text element (right in LTR layouts, left in RTL
-  // layouts).
-  TEXT_ALIGN_END = 3;
-}
-
-// An extensible TextAlignment property.
-message TextAlignmentProp {
-  // The value.
-  TextAlignment value = 1;
-}
-
 // How text that will not fit inside the bounds of a Text element will be
 // handled.
 enum TextOverflow {
@@ -195,6 +124,13 @@
   // Truncate the text to fit in the Text element's bounds, but add an ellipsis
   // (i.e. ...) to the end of the text if it has been truncated.
   TEXT_OVERFLOW_ELLIPSIZE_END = 2;
+
+  // Enable marquee animation for texts that don't fit inside the Text element.
+  // This is only applicable for single line texts; if the text has multiple
+  // lines, the behavior is equivalent to TEXT_OVERFLOW_TRUNCATE.
+  //
+
+  TEXT_OVERFLOW_MARQUEE = 3;
 }
 
 // An extensible TextOverflow property.
@@ -203,50 +139,12 @@
   TextOverflow value = 1;
 }
 
-// The anchor position of an Arc's elements. This is used to specify how
-// elements added to an Arc should be laid out with respect to anchor_angle.
-//
-// As an example, assume that the following diagrams are wrapped to an arc, and
-// each represents an Arc element containing a single Text element. The Text
-// element's anchor_angle is "0" for all cases.
-//
-// ```
-// ARC_ANCHOR_START:
-// -180                                0                                    180
-//                                     Hello World!
-//
-//
-// ARC_ANCHOR_CENTER:
-// -180                                0                                    180
-//                                Hello World!
-//
-// ARC_ANCHOR_END:
-// -180                                0                                    180
-//                          Hello World!
-// ```
-enum ArcAnchorType {
-  // Anchor position is undefined.
-  ARC_ANCHOR_UNDEFINED = 0;
-
-  // Anchor at the start of the elements. This will cause elements added to an
-  // arc to begin at the given anchor_angle, and sweep around to the right.
-  ARC_ANCHOR_START = 1;
-
-  // Anchor at the center of the elements. This will cause the center of the
-  // whole set of elements added to an arc to be pinned at the given
-  // anchor_angle.
-  ARC_ANCHOR_CENTER = 2;
-
-  // Anchor at the end of the elements. This will cause the set of elements
-  // inside the arc to end at the specified anchor_angle, i.e. all elements
-  // should be to the left of anchor_angle.
-  ARC_ANCHOR_END = 3;
-}
-
-// An extensible ArcAnchorType property.
-message ArcAnchorTypeProp {
-  // The value.
-  ArcAnchorType value = 1;
+// An Android platform specific text style configuration options for styling and
+// compatibility.
+message AndroidTextStyle {
+  // Whether the Text excludes extra top and bottom padding above the normal
+  // ascent and descent. The default is false.
+  bool exclude_font_padding = 1;
 }
 
 // A text string.
@@ -283,6 +181,10 @@
   // vertical distance between subsequent baselines. If not specified, defaults
   // the font's recommended interline spacing.
   SpProp line_height = 7;
+
+  // An Android platform specific text style configuration options for styling
+  // and compatibility.
+  AndroidTextStyle android_text_style = 8;
 }
 
 // How content which does not match the dimensions of its bounds (e.g. an image
@@ -356,7 +258,7 @@
 
   // Filtering parameters for this image. If not specified, defaults to no
   // filtering.
-  ColorFilter filter = 6;
+  ColorFilter color_filter = 6;
 }
 
 // A simple spacer, typically used to provide padding between adjacent elements.
@@ -413,6 +315,10 @@
 
   // Modifiers for this element.
   SpanModifiers modifiers = 3;
+
+  // An Android platform specific text style configuration options for styling
+  // and compatibility.
+  AndroidTextStyle android_text_style = 4;
 }
 
 // An image which can be added to a Span.
@@ -445,11 +351,11 @@
   }
 }
 
-// A container of Span elements. Currently, this only supports Text elements,
-// where each individual Span can have different styling applied to it but the
-// resulting text will flow naturally. This allows sections of a paragraph of
-// text to have different styling applied to it, for example, making one or two
-// words bold or italic.
+// A container of Span elements. Currently, this supports SpanImage and SpanText
+// elements, where each individual Span can have different styling applied to it
+// but the resulting text will flow naturally. This allows sections of a
+// paragraph of text to have different styling applied to it, for example,
+// making one or two words bold or italic.
 message Spannable {
   // The Span elements that form this Spannable.
   repeated Span spans = 1;
@@ -579,6 +485,12 @@
 
   // Modifiers for this element.
   Modifiers modifiers = 5;
+
+  // The target angle that will be used by the layout when expanding child
+  // elements. NB a value of zero is interpreted to mean 360 degrees. This
+  // target may not be achievable if other non-expandable elements bring us past
+  // this value.
+  DegreesProp max_angle = 6;
 }
 
 // A text element that can be used in an Arc.
@@ -607,6 +519,31 @@
 
   // Modifiers for this element.
   ArcModifiers modifiers = 4;
+
+  // The length of this line. If not defined, defaults to 0 degrees.
+  ArcLineLength angular_length = 5;
+
+  // The line stroke cap. If not defined, defaults to STROKE_CAP_ROUND.
+  StrokeCapProp stroke_cap = 6;
+}
+
+// Styles to use for path endings
+enum StrokeCap {
+  // StrokeCap is undefined.
+  STROKE_CAP_UNDEFINED = 0;
+
+  // Begin and end contours with a flat edge and no extension.
+  STROKE_CAP_BUTT = 1;
+
+  // Begin and end contours with a semi-circle extension. The extension size is
+  // proportional to the thickness on the path.
+  STROKE_CAP_ROUND = 2;
+}
+
+// An extensible StrokeCap property.
+message StrokeCapProp {
+  // The value.
+  StrokeCap value = 1;
 }
 
 // A simple spacer used to provide padding between adjacent elements in an Arc.
@@ -619,6 +556,9 @@
 
   // Modifiers for this element.
   ArcModifiers modifiers = 3;
+
+  // The length of this spacer. If not defined, defaults to 0 degrees.
+  ArcSpacerLength angular_length = 4;
 }
 
 // A container that allows a standard LayoutElement to be added to an Arc.
@@ -664,6 +604,12 @@
 
 // A complete layout.
 message Layout {
+  // The layout message itself doesn't need a fingerprint. The "fingerprint"
+  // field below represents the tree defined by the "root" element.
+
   // The root element in the layout.
   LayoutElement root = 1;
+
+  // The fingerprint for the tree starting at "root".
+  TreeFingerprint fingerprint = 1000;
 }
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/modifiers.proto b/wear/protolayout/protolayout-proto/src/main/proto/modifiers.proto
index 4aa04db..d6f9586 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/modifiers.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/modifiers.proto
@@ -4,6 +4,7 @@
 package androidx.wear.protolayout.proto;
 
 import "action.proto";
+import "animation_parameters.proto";
 import "color.proto";
 import "dimension.proto";
 import "types.proto";
@@ -23,13 +24,52 @@
   Action on_click = 2;
 }
 
+// The type of user interface element. Accessibility services might use this to
+// describe the element or do customizations.
+enum SemanticsRole {
+  // Role is undefined. It may be automatically populated.
+  SEMANTICS_ROLE_NONE = 0;
+
+  // The element is an Image.
+  SEMANTICS_ROLE_IMAGE = 1;
+
+  // The element is a Button control.
+  SEMANTICS_ROLE_BUTTON = 2;
+
+  // The element is a Checkbox which is a component that represents two states
+  // (checked / unchecked).
+  SEMANTICS_ROLE_CHECKBOX = 3;
+
+  // The element is a Switch which is a two state toggleable component that
+  // provides on/off like options.
+  SEMANTICS_ROLE_SWITCH = 4;
+
+  // This element is a RadioButton which is a component to represent two states,
+  // selected and not selected.
+  SEMANTICS_ROLE_RADIOBUTTON = 5;
+}
+
 // A modifier for an element which has accessibility semantics associated with
 // it. This should generally be used sparingly, and in most cases should only be
 // applied to the top-level layout element or to Clickables.
 message Semantics {
   // The content description associated with this element. This will be dictated
   // when the element is focused by the screen reader.
-  string content_description = 1;
+  // @deprecated Use content_description instead.
+  string obsolete_content_description = 1;
+
+  // The content description associated with this element. This will be dictated
+  // when the element is focused by the screen reader.
+  StringProp content_description = 4;
+
+  // The type of user interface element. Accessibility services might use this
+  // to describe the element or do customizations.
+  SemanticsRole role = 2;
+
+  // The localized state description of the semantics node.
+  // For example: "on" or "off". This will be dictated when the element is
+  // focused by the screen reader.
+  StringProp state_description = 3;
 }
 
 // A modifier to apply padding around an element.
@@ -95,26 +135,192 @@
 // Padding or Background), or change their behaviour (e.g. Clickable, or
 // Semantics).
 message Modifiers {
-  // Allows its wrapped element to have actions associated with it, which will
-  // be executed when the element is tapped.
+  // The clickable property of the modified element. It allows its wrapped
+  // element to have actions associated with it, which will be executed when the
+  // element is tapped.
   Clickable clickable = 1;
 
-  // Adds metadata for the modified element, for example, screen reader content
-  // descriptions.
+  // The semantics of the modified element. This can be used to add metadata to
+  // the modified element (eg. screen reader content descriptions).
   Semantics semantics = 2;
 
-  // Adds padding to the modified element.
+  // The padding of the modified element.
   Padding padding = 3;
 
-  // Draws a border around the modified element.
+  // The border of the modified element.
   Border border = 4;
 
-  // Adds a background (with corner radius) to the modified element.
+  // The background (with optional corner radius) of the modified element.
   Background background = 5;
 
   // Metadata about an element. For use by libraries building higher-level
   // components only. This can be used to track component metadata
   ElementMetadata metadata = 6;
+
+  // The content transition of an element. Any update to the element or its
+  // children will trigger this animation for this element and everything
+  // underneath it.
+  AnimatedVisibility content_update_animation = 7;
+
+  // Whether the attached element is hidden, or visible. If the element is
+  // hidden, then it will still consume space in the layout, but will not render
+  // any contents, nor will any children render any contents.
+  //
+  // Note that a hidden element also cannot be clickable (i.e. a Clickable
+  // modifier would be ignored).
+  //
+  // Defaults to false (i.e. not hidden).
+  BoolProp hidden = 8;
+}
+
+// The content transition of an element. Any update to the element or its
+// children will trigger this animation for this element and everything
+// underneath it.
+message AnimatedVisibility {
+  // The content transition that is triggered when element enters the layout.
+  EnterTransition enter = 1;
+
+  // The content transition that is triggered when element exits the layout.
+  // Note that indefinite exit animations are ignored.
+  ExitTransition exit = 2;
+}
+
+// The content transition that is triggered when element enters the layout.
+message EnterTransition {
+  // The fading in animation for content transition of an element and its
+  // children happening when entering the layout.
+  FadeInTransition fade_in = 1;
+
+  // The sliding in animation for content transition of an element and its
+  // children happening when entering the layout.
+  SlideInTransition slide_in = 2;
+}
+
+// The fading animation for content transition of an element and its children,
+// from the specified starting alpha to fully visible.
+message FadeInTransition {
+  // The starting alpha of the fade in transition. It should be between 0 and 1.
+  // If not set, defaults to fully transparent, i.e. 0.
+  float initial_alpha = 1;
+
+  // The animation parameters for duration, delay, etc.
+  androidx.wear.protolayout.expression.proto.AnimationSpec animation_spec = 2;
+}
+
+// The sliding in animation for content transition of an element and its
+// children.
+message SlideInTransition {
+  // The slide direction used for slide animations on any element, from the
+  // specified point to its destination in the layout. If not set, defaults to
+  // horizontal from left to the right.
+  SlideDirection direction = 1;
+
+  // The initial offset for animation. By default the transition starts from the
+  // left parent boundary for horizontal orientation and from the top for
+  // vertical orientation. Note that sliding from the screen boundaries can only
+  // be achieved if all parent's sizes are big enough to accommodate it.
+  SlideBound initial_slide_bound = 2;
+
+  // The animation parameters for duration, delay, etc.
+  androidx.wear.protolayout.expression.proto.AnimationSpec animation_spec = 3;
+}
+
+// The content transition that is triggered when element exits the layout.
+message ExitTransition {
+  // The fading out animation for content transition of an element and its
+  // children happening when exiting the layout.
+  FadeOutTransition fade_out = 1;
+
+  // The sliding out animation for content transition of an element and its
+  // children happening when exiting the layout.
+  SlideOutTransition slide_out = 2;
+}
+
+// The fading animation for content transition of an element and its children,
+// from fully visible to the specified target alpha.
+message FadeOutTransition {
+  // The target alpha of the fade out transition. It should be between 0 and 1.
+  // If not set, defaults to fully invisible, i.e. 0.
+  float target_alpha = 1;
+
+  // The animation parameters for duration, delay, etc.
+  androidx.wear.protolayout.expression.proto.AnimationSpec animation_spec = 2;
+}
+
+// The sliding out animation for content transition of an element and its
+// children.
+message SlideOutTransition {
+  // The slide direction used for slide animations on any element, from its
+  // destination in the layout to the specified point. If not set, defaults to
+  // horizontal from right to the left.
+  SlideDirection direction = 1;
+
+  // The target offset for animation. By default the transition will end at the
+  // left parent boundary for horizontal orientation and at the top for
+  // vertical orientation. Note that sliding from the screen boundaries can only
+  // be achieved if all parent's sizes are big enough to accommodate it.
+  SlideBound target_slide_bound = 2;
+
+  // The animation parameters for duration, delay, etc.
+  androidx.wear.protolayout.expression.proto.AnimationSpec animation_spec = 3;
+}
+
+// The boundary that a Slide animation will use for start/end.
+message SlideBound {
+  oneof inner {
+    SlideParentBound parent_bound = 1;
+    SlideLinearBound linear_bound = 2;
+  }
+}
+
+// The slide animation will animate from/to the parent elements boundaries.
+message SlideParentBound {
+  // The snap options to use when sliding using parent boundaries. Defaults to
+  // SLIDE_PARENT_SNAP_TO_INSIDE if not specified.
+  SlideParentSnapOption snap_to = 1;
+}
+
+// The snap options to use when sliding using parent boundaries.
+enum SlideParentSnapOption {
+  // The undefined snapping option.
+  SLIDE_PARENT_SNAP_UNDEFINED = 0;
+
+  // The option that snaps insides of the element and its parent at start/end.
+  SLIDE_PARENT_SNAP_TO_INSIDE = 1;
+
+  // The option that snaps outsides of the element and its parent at start/end.
+  SLIDE_PARENT_SNAP_TO_OUTSIDE = 2;
+}
+
+// The slide animation will use the explicit offset for the animation boundary.
+message SlideLinearBound {
+  // The absolute delta linear distance to animate from/to. A direction will be
+  // defined by SlideDirection and this value should be a positive offset.
+  float offset_dp = 1;
+}
+
+// The slide direction used for slide animations on any element, from the
+// specified point to its destination in the layout for in animation or reverse
+// for out animation.
+enum SlideDirection {
+  // The undefined sliding orientation.
+  SLIDE_DIRECTION_UNDEFINED = 0;
+
+  // The sliding orientation that moves an element horizontally from left to the
+  // right.
+  SLIDE_DIRECTION_LEFT_TO_RIGHT = 1;
+
+  // The sliding orientation that moves an element horizontally from right to
+  // the left.
+  SLIDE_DIRECTION_RIGHT_TO_LEFT = 2;
+
+  // The sliding orientation that moves an element vertically from top to the
+  // bottom.
+  SLIDE_DIRECTION_TOP_TO_BOTTOM = 3;
+
+  // The sliding orientation that moves an element vertically from bottom to the
+  // top.
+  SLIDE_DIRECTION_BOTTOM_TO_TOP = 4;
 }
 
 // Modifiers that can be used with ArcLayoutElements. These may change the way
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/resources.proto b/wear/protolayout/protolayout-proto/src/main/proto/resources.proto
index f14a6a21..3284bee 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/resources.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/resources.proto
@@ -3,6 +3,9 @@
 
 package androidx.wear.protolayout.proto;
 
+import "dynamic.proto";
+import "trigger.proto";
+
 option java_package = "androidx.wear.protolayout.proto";
 option java_outer_classname = "ResourceProto";
 
@@ -44,6 +47,56 @@
   ImageFormat format = 4;
 }
 
+// An image to be loaded from a ContentProvider, by content URI.
+message AndroidImageResourceByContentUri {
+  // The content URI to use, on the format content://authority/resource.
+  string content_uri = 1;
+}
+
+// Format describing the contents of an animated image.
+enum AnimatedImageFormat {
+  // An undefined image format.
+  ANIMATED_IMAGE_FORMAT_UNDEFINED = 0;
+
+  // Android AnimatedVectorDrawable.
+  ANIMATED_IMAGE_FORMAT_AVD = 1;
+}
+
+// A non-seekable animated image resource that maps to an Android drawable by
+// resource ID. The animation is started with given trigger, fire and forget.
+message AndroidAnimatedImageResourceByResId {
+  // The format for the animated image.
+  AnimatedImageFormat format = 1;
+
+  // The Android resource ID, e.g. R.drawable.foo.
+  int32 resource_id = 2;
+
+  // The trigger to start the animation.
+  Trigger trigger = 3;
+}
+
+// A seekable animated image resource that maps to an Android drawable by
+// resource ID. The animation progress is bound to the provided dynamic float.
+message AndroidSeekableAnimatedImageResourceByResId {
+  // The format for the animated image.
+  AnimatedImageFormat format = 1;
+
+  // The Android resource ID, e.g. R.drawable.foo
+  int32 resource_id = 2;
+
+  // A dynamic float, normally transformed from certain states with the data
+  // binding pipeline, controls the progress of the animation. Its value is
+  // required to fall in the range of [0.0, 1.0], any values outside this range
+  // would be clamped.
+  //
+  // Typically, AnimatableFixedFloat or AnimatableDynamicFloat is used for this
+  // progress. With AnimatableFixedFloat, the animation is played from progress
+  // of its from_value to to_value; with AnimatableDynamicFloat, the animation
+  // is set from progress 0 to its first value once it is available, it then
+  // plays from current progress to the new value on subsequent updates.
+  androidx.wear.protolayout.expression.proto.DynamicFloat progress = 3;
+}
+
 // An image resource, which can be used by layouts. This holds multiple
 // underlying resource types, which the underlying runtime will pick according
 // to what it thinks is appropriate.
@@ -53,18 +106,30 @@
 
   // An image resource that contains the image data inline.
   InlineImageResource inline_resource = 2;
+
+  // An image which loads its drawable via an Android Content URI.
+  AndroidImageResourceByContentUri android_content_uri = 5;
+
+  // A non-seekable animated image resource that maps to an Android drawable by
+  // resource ID. The animation is started with given trigger, fire and forget.
+  AndroidAnimatedImageResourceByResId android_animated_resource_by_res_id = 6;
+
+  // A seekable animated image resource that maps to an Android drawable by
+  // resource ID. The animation progress is bound to the provided dynamic float.
+  AndroidSeekableAnimatedImageResourceByResId
+      android_seekable_animated_resource_by_res_id = 7;
 }
 
 // The resources for a layout.
 message Resources {
   // The version of this Resources instance.
   //
-  // Each tile specifies the version of resources it requires. After fetching a
-  // tile, the renderer will use the resources version specified by the tile
-  // to separately fetch the resources.
+  // Each layout specifies the version of resources it requires. After fetching
+  // a layout, the renderer will use the resources version specified by the
+  // layout to separately fetch the resources.
   //
-  // This value must match the version of the resources required by the tile
-  // for the tile to render successfully, and must match the resource version
+  // This value must match the version of the resources required by the layout
+  // for the layout to render successfully, and must match the resource version
   // specified in ResourcesRequest which triggered this request.
   string version = 1;
 
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/state.proto b/wear/protolayout/protolayout-proto/src/main/proto/state.proto
index 64d63468..162077e 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/state.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/state.proto
@@ -1,8 +1,10 @@
-// State of a tile.
+// State of a layout.
 syntax = "proto3";
 
 package androidx.wear.protolayout.proto;
 
+import "state_entry.proto";
+
 option java_package = "androidx.wear.protolayout.proto";
 option java_outer_classname = "StateProto";
 
@@ -10,4 +12,8 @@
 message State {
   // The ID of the clickable that was last clicked.
   string last_clickable_id = 1;
+
+  // Any shared state between the provider and renderer.
+  map<string, androidx.wear.protolayout.expression.proto.StateEntryValue>
+      id_to_value = 2;
 }
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/trigger.proto b/wear/protolayout/protolayout-proto/src/main/proto/trigger.proto
new file mode 100644
index 0000000..ef42469
--- /dev/null
+++ b/wear/protolayout/protolayout-proto/src/main/proto/trigger.proto
@@ -0,0 +1,37 @@
+// Triggers that can be used to start an animation.
+syntax = "proto3";
+
+package androidx.wear.protolayout.proto;
+
+import "dynamic.proto";
+
+option java_package = "androidx.wear.protolayout.proto";
+option java_outer_classname = "TriggerProto";
+
+// Triggers when the layout visibility state turns from invisible to fully
+// visible.
+message OnVisibleTrigger {}
+
+// Triggers only once when the layout visibility state turns from invisible to
+// fully visible for the first time.
+message OnVisibleOnceTrigger {}
+
+// Triggers immediately when the layout is loaded / reloaded.
+message OnLoadTrigger {}
+
+// Triggers *every time* the condition switches from false to true. If the
+// condition is true initially, that will fire the trigger on load.
+message OnConditionTrigger {
+  // Dynamic boolean used as trigger.
+  androidx.wear.protolayout.expression.proto.DynamicBool dynamic_bool = 1;
+}
+
+// The triggers that can be fired.
+message Trigger {
+  oneof inner {
+    OnVisibleTrigger on_visible_trigger = 1;
+    OnVisibleOnceTrigger on_visible_once_trigger = 2;
+    OnLoadTrigger on_load_trigger = 3;
+    OnConditionTrigger on_condition_trigger = 4;
+  }
+}
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/types.proto b/wear/protolayout/protolayout-proto/src/main/proto/types.proto
index 00591bc..c9ddacc 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/types.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/types.proto
@@ -3,6 +3,9 @@
 
 package androidx.wear.protolayout.proto;
 
+import "alignment.proto";
+import "dynamic.proto";
+
 option java_package = "androidx.wear.protolayout.proto";
 option java_outer_classname = "TypesProto";
 
@@ -10,22 +13,42 @@
 message Int32Prop {
   // The value.
   int32 value = 1;
+
+  // The dynamic value.
+  androidx.wear.protolayout.expression.proto.DynamicInt32 dynamic_value = 2;
 }
 
 // A string type.
 message StringProp {
   // The value.
   string value = 1;
+
+  // The dynamic value.
+  androidx.wear.protolayout.expression.proto.DynamicString dynamic_value = 2;
+
+  // When used as a layout-changing data bind, the string to measure, when
+  // considering how wide the element should be in the layout.
+  string value_for_layout = 3;
+
+  // Alignment alignment of the actual text within the space reserved by
+  // value_for_layout. If not specified, defaults to center alignment.
+  TextAlignment text_alignment_for_layout = 4;
 }
 
 // A float type.
 message FloatProp {
   // The value.
-  float value = 1;
+  optional float value = 1;
+
+  // The dynamic value.
+  androidx.wear.protolayout.expression.proto.DynamicFloat dynamic_value = 2;
 }
 
 // A boolean type.
 message BoolProp {
   // The value.
   bool value = 1;
+
+  // The dynamic value.
+  androidx.wear.protolayout.expression.proto.DynamicBool dynamic_value = 2;
 }
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/version.proto b/wear/protolayout/protolayout-proto/src/main/proto/version.proto
index 4a006bb..7c581e3 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/version.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/version.proto
@@ -1,4 +1,4 @@
-// The components of a tile that can be rendered by a tile renderer.
+// The schema version information of a layout.
 syntax = "proto3";
 
 package androidx.wear.protolayout.proto;
@@ -7,7 +7,7 @@
 option java_outer_classname = "VersionProto";
 
 // Version information. This is used to encode the schema version of a payload
-// (e.g. inside of Tile).
+// (e.g. inside of a layout).
 message VersionInfo {
   // Major version. Incremented on breaking changes (i.e. compatibility is not
   // guaranteed across major versions).
diff --git a/wear/protolayout/protolayout/api/current.txt b/wear/protolayout/protolayout/api/current.txt
index e6f50d0..2d0294f 100644
--- a/wear/protolayout/protolayout/api/current.txt
+++ b/wear/protolayout/protolayout/api/current.txt
@@ -1 +1,1005 @@
 // Signature format: 4.0
+package androidx.wear.protolayout {
+
+  public final class ActionBuilders {
+    method public static androidx.wear.protolayout.ActionBuilders.AndroidBooleanExtra booleanExtra(boolean);
+    method public static androidx.wear.protolayout.ActionBuilders.AndroidDoubleExtra doubleExtra(double);
+    method public static androidx.wear.protolayout.ActionBuilders.AndroidIntExtra intExtra(int);
+    method public static androidx.wear.protolayout.ActionBuilders.AndroidLongExtra longExtra(long);
+    method public static androidx.wear.protolayout.ActionBuilders.AndroidStringExtra stringExtra(String);
+  }
+
+  public static interface ActionBuilders.Action {
+  }
+
+  public static interface ActionBuilders.Action.Builder {
+    method public androidx.wear.protolayout.ActionBuilders.Action build();
+  }
+
+  public static final class ActionBuilders.AndroidActivity {
+    method public String getClassName();
+    method public java.util.Map<java.lang.String!,androidx.wear.protolayout.ActionBuilders.AndroidExtra!> getKeyToExtraMapping();
+    method public String getPackageName();
+  }
+
+  public static final class ActionBuilders.AndroidActivity.Builder {
+    ctor public ActionBuilders.AndroidActivity.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidActivity.Builder addKeyToExtraMapping(String, androidx.wear.protolayout.ActionBuilders.AndroidExtra);
+    method public androidx.wear.protolayout.ActionBuilders.AndroidActivity build();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidActivity.Builder setClassName(String);
+    method public androidx.wear.protolayout.ActionBuilders.AndroidActivity.Builder setPackageName(String);
+  }
+
+  public static final class ActionBuilders.AndroidBooleanExtra implements androidx.wear.protolayout.ActionBuilders.AndroidExtra {
+    method public boolean getValue();
+  }
+
+  public static final class ActionBuilders.AndroidBooleanExtra.Builder implements androidx.wear.protolayout.ActionBuilders.AndroidExtra.Builder {
+    ctor public ActionBuilders.AndroidBooleanExtra.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidBooleanExtra build();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidBooleanExtra.Builder setValue(boolean);
+  }
+
+  public static final class ActionBuilders.AndroidDoubleExtra implements androidx.wear.protolayout.ActionBuilders.AndroidExtra {
+    method public double getValue();
+  }
+
+  public static final class ActionBuilders.AndroidDoubleExtra.Builder implements androidx.wear.protolayout.ActionBuilders.AndroidExtra.Builder {
+    ctor public ActionBuilders.AndroidDoubleExtra.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidDoubleExtra build();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidDoubleExtra.Builder setValue(double);
+  }
+
+  public static interface ActionBuilders.AndroidExtra {
+  }
+
+  public static interface ActionBuilders.AndroidExtra.Builder {
+    method public androidx.wear.protolayout.ActionBuilders.AndroidExtra build();
+  }
+
+  public static final class ActionBuilders.AndroidIntExtra implements androidx.wear.protolayout.ActionBuilders.AndroidExtra {
+    method public int getValue();
+  }
+
+  public static final class ActionBuilders.AndroidIntExtra.Builder implements androidx.wear.protolayout.ActionBuilders.AndroidExtra.Builder {
+    ctor public ActionBuilders.AndroidIntExtra.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidIntExtra build();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidIntExtra.Builder setValue(int);
+  }
+
+  public static final class ActionBuilders.AndroidLongExtra implements androidx.wear.protolayout.ActionBuilders.AndroidExtra {
+    method public long getValue();
+  }
+
+  public static final class ActionBuilders.AndroidLongExtra.Builder implements androidx.wear.protolayout.ActionBuilders.AndroidExtra.Builder {
+    ctor public ActionBuilders.AndroidLongExtra.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidLongExtra build();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidLongExtra.Builder setValue(long);
+  }
+
+  public static final class ActionBuilders.AndroidStringExtra implements androidx.wear.protolayout.ActionBuilders.AndroidExtra {
+    method public String getValue();
+  }
+
+  public static final class ActionBuilders.AndroidStringExtra.Builder implements androidx.wear.protolayout.ActionBuilders.AndroidExtra.Builder {
+    ctor public ActionBuilders.AndroidStringExtra.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidStringExtra build();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidStringExtra.Builder setValue(String);
+  }
+
+  public static final class ActionBuilders.LaunchAction implements androidx.wear.protolayout.ActionBuilders.Action {
+    method public androidx.wear.protolayout.ActionBuilders.AndroidActivity? getAndroidActivity();
+  }
+
+  public static final class ActionBuilders.LaunchAction.Builder implements androidx.wear.protolayout.ActionBuilders.Action.Builder {
+    ctor public ActionBuilders.LaunchAction.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.LaunchAction build();
+    method public androidx.wear.protolayout.ActionBuilders.LaunchAction.Builder setAndroidActivity(androidx.wear.protolayout.ActionBuilders.AndroidActivity);
+  }
+
+  public static final class ActionBuilders.LoadAction implements androidx.wear.protolayout.ActionBuilders.Action {
+    method public androidx.wear.protolayout.StateBuilders.State? getRequestState();
+  }
+
+  public static final class ActionBuilders.LoadAction.Builder implements androidx.wear.protolayout.ActionBuilders.Action.Builder {
+    ctor public ActionBuilders.LoadAction.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.LoadAction build();
+    method public androidx.wear.protolayout.ActionBuilders.LoadAction.Builder setRequestState(androidx.wear.protolayout.StateBuilders.State);
+  }
+
+  public static interface ActionBuilders.LocalAction {
+  }
+
+  public static interface ActionBuilders.LocalAction.Builder {
+    method public androidx.wear.protolayout.ActionBuilders.LocalAction build();
+  }
+
+  public static final class ActionBuilders.SetStateAction implements androidx.wear.protolayout.ActionBuilders.LocalAction {
+    method public String getTargetKey();
+    method public androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue? getValue();
+  }
+
+  public static final class ActionBuilders.SetStateAction.Builder implements androidx.wear.protolayout.ActionBuilders.LocalAction.Builder {
+    ctor public ActionBuilders.SetStateAction.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.SetStateAction build();
+    method public androidx.wear.protolayout.ActionBuilders.SetStateAction.Builder setTargetKey(String);
+    method public androidx.wear.protolayout.ActionBuilders.SetStateAction.Builder setValue(androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue);
+  }
+
+  public final class ColorBuilders {
+    method public static androidx.wear.protolayout.ColorBuilders.ColorProp argb(@ColorInt int);
+  }
+
+  public static final class ColorBuilders.ColorProp {
+    method @ColorInt public int getArgb();
+  }
+
+  public static final class ColorBuilders.ColorProp.Builder {
+    ctor public ColorBuilders.ColorProp.Builder();
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp build();
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp.Builder setArgb(@ColorInt int);
+  }
+
+  public final class DeviceParametersBuilders {
+    field public static final int DEVICE_PLATFORM_UNDEFINED = 0; // 0x0
+    field public static final int DEVICE_PLATFORM_WEAR_OS = 1; // 0x1
+    field public static final int SCREEN_SHAPE_RECT = 2; // 0x2
+    field public static final int SCREEN_SHAPE_ROUND = 1; // 0x1
+    field public static final int SCREEN_SHAPE_UNDEFINED = 0; // 0x0
+  }
+
+  public static final class DeviceParametersBuilders.DeviceParameters {
+    method public int getDevicePlatform();
+    method @FloatRange(from=0.0, fromInclusive=false, toInclusive=false) public float getScreenDensity();
+    method @Dimension(unit=androidx.annotation.Dimension.DP) public int getScreenHeightDp();
+    method public int getScreenShape();
+    method @Dimension(unit=androidx.annotation.Dimension.DP) public int getScreenWidthDp();
+  }
+
+  public static final class DeviceParametersBuilders.DeviceParameters.Builder {
+    ctor public DeviceParametersBuilders.DeviceParameters.Builder();
+    method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters build();
+    method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters.Builder setDevicePlatform(int);
+    method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters.Builder setScreenDensity(@FloatRange(from=0.0, fromInclusive=false, toInclusive=false) float);
+    method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters.Builder setScreenHeightDp(@Dimension(unit=androidx.annotation.Dimension.DP) int);
+    method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters.Builder setScreenShape(int);
+    method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters.Builder setScreenWidthDp(@Dimension(unit=androidx.annotation.Dimension.DP) int);
+  }
+
+  public final class DimensionBuilders {
+    method public static androidx.wear.protolayout.DimensionBuilders.DegreesProp degrees(float);
+    method public static androidx.wear.protolayout.DimensionBuilders.DpProp dp(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+    method public static androidx.wear.protolayout.DimensionBuilders.EmProp em(int);
+    method public static androidx.wear.protolayout.DimensionBuilders.EmProp em(float);
+    method public static androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp expand();
+    method public static androidx.wear.protolayout.DimensionBuilders.SpProp sp(@Dimension(unit=androidx.annotation.Dimension.SP) float);
+    method public static androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp wrap();
+  }
+
+  public static interface DimensionBuilders.ContainerDimension {
+  }
+
+  public static interface DimensionBuilders.ContainerDimension.Builder {
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension build();
+  }
+
+  public static final class DimensionBuilders.DegreesProp {
+    method public float getValue();
+  }
+
+  public static final class DimensionBuilders.DegreesProp.Builder {
+    ctor public DimensionBuilders.DegreesProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.DegreesProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.DegreesProp.Builder setValue(float);
+  }
+
+  public static final class DimensionBuilders.DpProp implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension androidx.wear.protolayout.DimensionBuilders.ImageDimension androidx.wear.protolayout.DimensionBuilders.SpacerDimension {
+    method @Dimension(unit=androidx.annotation.Dimension.DP) public float getValue();
+  }
+
+  public static final class DimensionBuilders.DpProp.Builder implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension.Builder androidx.wear.protolayout.DimensionBuilders.ImageDimension.Builder androidx.wear.protolayout.DimensionBuilders.SpacerDimension.Builder {
+    ctor public DimensionBuilders.DpProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp.Builder setValue(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+  }
+
+  public static final class DimensionBuilders.EmProp {
+    method public float getValue();
+  }
+
+  public static final class DimensionBuilders.EmProp.Builder {
+    ctor public DimensionBuilders.EmProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.EmProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.EmProp.Builder setValue(float);
+  }
+
+  public static final class DimensionBuilders.ExpandedDimensionProp implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension androidx.wear.protolayout.DimensionBuilders.ImageDimension {
+  }
+
+  public static final class DimensionBuilders.ExpandedDimensionProp.Builder implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension.Builder androidx.wear.protolayout.DimensionBuilders.ImageDimension.Builder {
+    ctor public DimensionBuilders.ExpandedDimensionProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp build();
+  }
+
+  public static interface DimensionBuilders.ImageDimension {
+  }
+
+  public static interface DimensionBuilders.ImageDimension.Builder {
+    method public androidx.wear.protolayout.DimensionBuilders.ImageDimension build();
+  }
+
+  public static final class DimensionBuilders.ProportionalDimensionProp implements androidx.wear.protolayout.DimensionBuilders.ImageDimension {
+    method @IntRange(from=0) public int getAspectRatioHeight();
+    method @IntRange(from=0) public int getAspectRatioWidth();
+  }
+
+  public static final class DimensionBuilders.ProportionalDimensionProp.Builder implements androidx.wear.protolayout.DimensionBuilders.ImageDimension.Builder {
+    ctor public DimensionBuilders.ProportionalDimensionProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.ProportionalDimensionProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.ProportionalDimensionProp.Builder setAspectRatioHeight(@IntRange(from=0) int);
+    method public androidx.wear.protolayout.DimensionBuilders.ProportionalDimensionProp.Builder setAspectRatioWidth(@IntRange(from=0) int);
+  }
+
+  public static final class DimensionBuilders.SpProp {
+    method @Dimension(unit=androidx.annotation.Dimension.SP) public float getValue();
+  }
+
+  public static final class DimensionBuilders.SpProp.Builder {
+    ctor public DimensionBuilders.SpProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.SpProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.SpProp.Builder setValue(@Dimension(unit=androidx.annotation.Dimension.SP) float);
+  }
+
+  public static interface DimensionBuilders.SpacerDimension {
+  }
+
+  public static interface DimensionBuilders.SpacerDimension.Builder {
+    method public androidx.wear.protolayout.DimensionBuilders.SpacerDimension build();
+  }
+
+  public static final class DimensionBuilders.WrappedDimensionProp implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension {
+  }
+
+  public static final class DimensionBuilders.WrappedDimensionProp.Builder implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension.Builder {
+    ctor public DimensionBuilders.WrappedDimensionProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp build();
+  }
+
+  public final class LayoutElementBuilders {
+    field public static final int ARC_ANCHOR_CENTER = 2; // 0x2
+    field public static final int ARC_ANCHOR_END = 3; // 0x3
+    field public static final int ARC_ANCHOR_START = 1; // 0x1
+    field public static final int ARC_ANCHOR_UNDEFINED = 0; // 0x0
+    field public static final int CONTENT_SCALE_MODE_CROP = 2; // 0x2
+    field public static final int CONTENT_SCALE_MODE_FILL_BOUNDS = 3; // 0x3
+    field public static final int CONTENT_SCALE_MODE_FIT = 1; // 0x1
+    field public static final int CONTENT_SCALE_MODE_UNDEFINED = 0; // 0x0
+    field public static final int FONT_VARIANT_BODY = 2; // 0x2
+    field public static final int FONT_VARIANT_TITLE = 1; // 0x1
+    field public static final int FONT_VARIANT_UNDEFINED = 0; // 0x0
+    field public static final int FONT_WEIGHT_BOLD = 700; // 0x2bc
+    field public static final int FONT_WEIGHT_NORMAL = 400; // 0x190
+    field public static final int FONT_WEIGHT_UNDEFINED = 0; // 0x0
+    field public static final int HORIZONTAL_ALIGN_CENTER = 2; // 0x2
+    field public static final int HORIZONTAL_ALIGN_END = 5; // 0x5
+    field public static final int HORIZONTAL_ALIGN_LEFT = 1; // 0x1
+    field public static final int HORIZONTAL_ALIGN_RIGHT = 3; // 0x3
+    field public static final int HORIZONTAL_ALIGN_START = 4; // 0x4
+    field public static final int HORIZONTAL_ALIGN_UNDEFINED = 0; // 0x0
+    field public static final int SPAN_VERTICAL_ALIGN_BOTTOM = 1; // 0x1
+    field public static final int SPAN_VERTICAL_ALIGN_TEXT_BASELINE = 2; // 0x2
+    field public static final int SPAN_VERTICAL_ALIGN_UNDEFINED = 0; // 0x0
+    field public static final int TEXT_ALIGN_CENTER = 2; // 0x2
+    field public static final int TEXT_ALIGN_END = 3; // 0x3
+    field public static final int TEXT_ALIGN_START = 1; // 0x1
+    field public static final int TEXT_ALIGN_UNDEFINED = 0; // 0x0
+    field public static final int TEXT_OVERFLOW_ELLIPSIZE_END = 2; // 0x2
+    field public static final int TEXT_OVERFLOW_TRUNCATE = 1; // 0x1
+    field public static final int TEXT_OVERFLOW_UNDEFINED = 0; // 0x0
+    field public static final int VERTICAL_ALIGN_BOTTOM = 3; // 0x3
+    field public static final int VERTICAL_ALIGN_CENTER = 2; // 0x2
+    field public static final int VERTICAL_ALIGN_TOP = 1; // 0x1
+    field public static final int VERTICAL_ALIGN_UNDEFINED = 0; // 0x0
+  }
+
+  public static final class LayoutElementBuilders.Arc implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public androidx.wear.protolayout.DimensionBuilders.DegreesProp? getAnchorAngle();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAnchorTypeProp? getAnchorType();
+    method public java.util.List<androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement!> getContents();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp? getVerticalAlign();
+  }
+
+  public static final class LayoutElementBuilders.Arc.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Arc.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder addContent(androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder setAnchorAngle(androidx.wear.protolayout.DimensionBuilders.DegreesProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder setAnchorType(androidx.wear.protolayout.LayoutElementBuilders.ArcAnchorTypeProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder setAnchorType(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder setVerticalAlign(androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder setVerticalAlign(int);
+  }
+
+  public static final class LayoutElementBuilders.ArcAdapter implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement {
+    method public androidx.wear.protolayout.LayoutElementBuilders.LayoutElement? getContent();
+    method public androidx.wear.protolayout.TypeBuilders.BoolProp? getRotateContents();
+  }
+
+  public static final class LayoutElementBuilders.ArcAdapter.Builder implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement.Builder {
+    ctor public LayoutElementBuilders.ArcAdapter.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAdapter build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAdapter.Builder setContent(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAdapter.Builder setRotateContents(androidx.wear.protolayout.TypeBuilders.BoolProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAdapter.Builder setRotateContents(boolean);
+  }
+
+  public static final class LayoutElementBuilders.ArcAnchorTypeProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.ArcAnchorTypeProp.Builder {
+    ctor public LayoutElementBuilders.ArcAnchorTypeProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAnchorTypeProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAnchorTypeProp.Builder setValue(int);
+  }
+
+  public static interface LayoutElementBuilders.ArcLayoutElement {
+  }
+
+  public static interface LayoutElementBuilders.ArcLayoutElement.Builder {
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement build();
+  }
+
+  public static final class LayoutElementBuilders.ArcLine implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement {
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp? getColor();
+    method public androidx.wear.protolayout.DimensionBuilders.DegreesProp? getLength();
+    method public androidx.wear.protolayout.ModifiersBuilders.ArcModifiers? getModifiers();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getThickness();
+  }
+
+  public static final class LayoutElementBuilders.ArcLine.Builder implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement.Builder {
+    ctor public LayoutElementBuilders.ArcLine.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcLine build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcLine.Builder setColor(androidx.wear.protolayout.ColorBuilders.ColorProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcLine.Builder setLength(androidx.wear.protolayout.DimensionBuilders.DegreesProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcLine.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.ArcModifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcLine.Builder setThickness(androidx.wear.protolayout.DimensionBuilders.DpProp);
+  }
+
+  public static final class LayoutElementBuilders.ArcSpacer implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement {
+    method public androidx.wear.protolayout.DimensionBuilders.DegreesProp? getLength();
+    method public androidx.wear.protolayout.ModifiersBuilders.ArcModifiers? getModifiers();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getThickness();
+  }
+
+  public static final class LayoutElementBuilders.ArcSpacer.Builder implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement.Builder {
+    ctor public LayoutElementBuilders.ArcSpacer.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcSpacer build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcSpacer.Builder setLength(androidx.wear.protolayout.DimensionBuilders.DegreesProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcSpacer.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.ArcModifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcSpacer.Builder setThickness(androidx.wear.protolayout.DimensionBuilders.DpProp);
+  }
+
+  public static final class LayoutElementBuilders.ArcText implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement {
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle? getFontStyle();
+    method public androidx.wear.protolayout.ModifiersBuilders.ArcModifiers? getModifiers();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getText();
+  }
+
+  public static final class LayoutElementBuilders.ArcText.Builder implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement.Builder {
+    ctor public LayoutElementBuilders.ArcText.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcText build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcText.Builder setFontStyle(androidx.wear.protolayout.LayoutElementBuilders.FontStyle);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcText.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.ArcModifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcText.Builder setText(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcText.Builder setText(String);
+  }
+
+  public static final class LayoutElementBuilders.Box implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public java.util.List<androidx.wear.protolayout.LayoutElementBuilders.LayoutElement!> getContents();
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension? getHeight();
+    method public androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp? getHorizontalAlignment();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp? getVerticalAlignment();
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension? getWidth();
+  }
+
+  public static final class LayoutElementBuilders.Box.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Box.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder addContent(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setHorizontalAlignment(androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setHorizontalAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setVerticalAlignment(androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setVerticalAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
+  }
+
+  public static final class LayoutElementBuilders.ColorFilter {
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp? getTint();
+  }
+
+  public static final class LayoutElementBuilders.ColorFilter.Builder {
+    ctor public LayoutElementBuilders.ColorFilter.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ColorFilter build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ColorFilter.Builder setTint(androidx.wear.protolayout.ColorBuilders.ColorProp);
+  }
+
+  public static final class LayoutElementBuilders.Column implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public java.util.List<androidx.wear.protolayout.LayoutElementBuilders.LayoutElement!> getContents();
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension? getHeight();
+    method public androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp? getHorizontalAlignment();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension? getWidth();
+  }
+
+  public static final class LayoutElementBuilders.Column.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Column.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column.Builder addContent(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column.Builder setHorizontalAlignment(androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column.Builder setHorizontalAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
+  }
+
+  public static final class LayoutElementBuilders.ContentScaleModeProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.ContentScaleModeProp.Builder {
+    ctor public LayoutElementBuilders.ContentScaleModeProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ContentScaleModeProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ContentScaleModeProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.FontStyle {
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp? getColor();
+    method public androidx.wear.protolayout.TypeBuilders.BoolProp? getItalic();
+    method public androidx.wear.protolayout.DimensionBuilders.EmProp? getLetterSpacing();
+    method public androidx.wear.protolayout.DimensionBuilders.SpProp? getSize();
+    method public androidx.wear.protolayout.TypeBuilders.BoolProp? getUnderline();
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontWeightProp? getWeight();
+  }
+
+  public static final class LayoutElementBuilders.FontStyle.Builder {
+    ctor public LayoutElementBuilders.FontStyle.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setColor(androidx.wear.protolayout.ColorBuilders.ColorProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setItalic(androidx.wear.protolayout.TypeBuilders.BoolProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setItalic(boolean);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setLetterSpacing(androidx.wear.protolayout.DimensionBuilders.EmProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setSize(androidx.wear.protolayout.DimensionBuilders.SpProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setUnderline(androidx.wear.protolayout.TypeBuilders.BoolProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setUnderline(boolean);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setWeight(androidx.wear.protolayout.LayoutElementBuilders.FontWeightProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setWeight(int);
+  }
+
+  public static class LayoutElementBuilders.FontStyles {
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder body1(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder body2(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder button(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder caption1(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder caption2(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder display1(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder display2(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder display3(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder title1(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder title2(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder title3(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+  }
+
+  public static final class LayoutElementBuilders.FontWeightProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.FontWeightProp.Builder {
+    ctor public LayoutElementBuilders.FontWeightProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontWeightProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontWeightProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.HorizontalAlignmentProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.HorizontalAlignmentProp.Builder {
+    ctor public LayoutElementBuilders.HorizontalAlignmentProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.Image implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public androidx.wear.protolayout.LayoutElementBuilders.ColorFilter? getColorFilter();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ContentScaleModeProp? getContentScaleMode();
+    method public androidx.wear.protolayout.DimensionBuilders.ImageDimension? getHeight();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getResourceId();
+    method public androidx.wear.protolayout.DimensionBuilders.ImageDimension? getWidth();
+  }
+
+  public static final class LayoutElementBuilders.Image.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Image.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setColorFilter(androidx.wear.protolayout.LayoutElementBuilders.ColorFilter);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setContentScaleMode(androidx.wear.protolayout.LayoutElementBuilders.ContentScaleModeProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setContentScaleMode(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.ImageDimension);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setResourceId(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setResourceId(String);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.ImageDimension);
+  }
+
+  public static final class LayoutElementBuilders.Layout {
+    method public static androidx.wear.protolayout.LayoutElementBuilders.Layout fromLayoutElement(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.LayoutElement? getRoot();
+  }
+
+  public static final class LayoutElementBuilders.Layout.Builder {
+    ctor public LayoutElementBuilders.Layout.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Layout build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Layout.Builder setRoot(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+  }
+
+  public static interface LayoutElementBuilders.LayoutElement {
+  }
+
+  public static interface LayoutElementBuilders.LayoutElement.Builder {
+    method public androidx.wear.protolayout.LayoutElementBuilders.LayoutElement build();
+  }
+
+  public static final class LayoutElementBuilders.Row implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public java.util.List<androidx.wear.protolayout.LayoutElementBuilders.LayoutElement!> getContents();
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension? getHeight();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp? getVerticalAlignment();
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension? getWidth();
+  }
+
+  public static final class LayoutElementBuilders.Row.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Row.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row.Builder addContent(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row.Builder setVerticalAlignment(androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row.Builder setVerticalAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
+  }
+
+  public static final class LayoutElementBuilders.Spacer implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public androidx.wear.protolayout.DimensionBuilders.SpacerDimension? getHeight();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.DimensionBuilders.SpacerDimension? getWidth();
+  }
+
+  public static final class LayoutElementBuilders.Spacer.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Spacer.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spacer build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spacer.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.SpacerDimension);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spacer.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spacer.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.SpacerDimension);
+  }
+
+  public static interface LayoutElementBuilders.Span {
+  }
+
+  public static interface LayoutElementBuilders.Span.Builder {
+    method public androidx.wear.protolayout.LayoutElementBuilders.Span build();
+  }
+
+  public static final class LayoutElementBuilders.SpanImage implements androidx.wear.protolayout.LayoutElementBuilders.Span {
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanVerticalAlignmentProp? getAlignment();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getHeight();
+    method public androidx.wear.protolayout.ModifiersBuilders.SpanModifiers? getModifiers();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getResourceId();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getWidth();
+  }
+
+  public static final class LayoutElementBuilders.SpanImage.Builder implements androidx.wear.protolayout.LayoutElementBuilders.Span.Builder {
+    ctor public LayoutElementBuilders.SpanImage.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setAlignment(androidx.wear.protolayout.LayoutElementBuilders.SpanVerticalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.DpProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.SpanModifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setResourceId(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setResourceId(String);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.DpProp);
+  }
+
+  public static final class LayoutElementBuilders.SpanText implements androidx.wear.protolayout.LayoutElementBuilders.Span {
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle? getFontStyle();
+    method public androidx.wear.protolayout.ModifiersBuilders.SpanModifiers? getModifiers();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getText();
+  }
+
+  public static final class LayoutElementBuilders.SpanText.Builder implements androidx.wear.protolayout.LayoutElementBuilders.Span.Builder {
+    ctor public LayoutElementBuilders.SpanText.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanText build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanText.Builder setFontStyle(androidx.wear.protolayout.LayoutElementBuilders.FontStyle);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanText.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.SpanModifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanText.Builder setText(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanText.Builder setText(String);
+  }
+
+  public static final class LayoutElementBuilders.SpanVerticalAlignmentProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.SpanVerticalAlignmentProp.Builder {
+    ctor public LayoutElementBuilders.SpanVerticalAlignmentProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanVerticalAlignmentProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanVerticalAlignmentProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.Spannable implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public androidx.wear.protolayout.DimensionBuilders.SpProp? getLineHeight();
+    method public androidx.wear.protolayout.TypeBuilders.Int32Prop? getMaxLines();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp? getMultilineAlignment();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextOverflowProp? getOverflow();
+    method public java.util.List<androidx.wear.protolayout.LayoutElementBuilders.Span!> getSpans();
+  }
+
+  public static final class LayoutElementBuilders.Spannable.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Spannable.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder addSpan(androidx.wear.protolayout.LayoutElementBuilders.Span);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setLineHeight(androidx.wear.protolayout.DimensionBuilders.SpProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setMaxLines(androidx.wear.protolayout.TypeBuilders.Int32Prop);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setMaxLines(@IntRange(from=1) int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setMultilineAlignment(androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setMultilineAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setOverflow(androidx.wear.protolayout.LayoutElementBuilders.TextOverflowProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setOverflow(int);
+  }
+
+  public static final class LayoutElementBuilders.Text implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle? getFontStyle();
+    method public androidx.wear.protolayout.DimensionBuilders.SpProp? getLineHeight();
+    method public androidx.wear.protolayout.TypeBuilders.Int32Prop? getMaxLines();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextAlignmentProp? getMultilineAlignment();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextOverflowProp? getOverflow();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getText();
+  }
+
+  public static final class LayoutElementBuilders.Text.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Text.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setFontStyle(androidx.wear.protolayout.LayoutElementBuilders.FontStyle);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setLineHeight(androidx.wear.protolayout.DimensionBuilders.SpProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setMaxLines(androidx.wear.protolayout.TypeBuilders.Int32Prop);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setMaxLines(@IntRange(from=1) int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setMultilineAlignment(androidx.wear.protolayout.LayoutElementBuilders.TextAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setMultilineAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setOverflow(androidx.wear.protolayout.LayoutElementBuilders.TextOverflowProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setOverflow(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setText(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setText(String);
+  }
+
+  public static final class LayoutElementBuilders.TextAlignmentProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.TextAlignmentProp.Builder {
+    ctor public LayoutElementBuilders.TextAlignmentProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextAlignmentProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextAlignmentProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.TextOverflowProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.TextOverflowProp.Builder {
+    ctor public LayoutElementBuilders.TextOverflowProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextOverflowProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextOverflowProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.VerticalAlignmentProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.VerticalAlignmentProp.Builder {
+    ctor public LayoutElementBuilders.VerticalAlignmentProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp.Builder setValue(int);
+  }
+
+  public final class ModifiersBuilders {
+  }
+
+  public static final class ModifiersBuilders.ArcModifiers {
+    method public androidx.wear.protolayout.ModifiersBuilders.Clickable? getClickable();
+    method public androidx.wear.protolayout.ModifiersBuilders.Semantics? getSemantics();
+  }
+
+  public static final class ModifiersBuilders.ArcModifiers.Builder {
+    ctor public ModifiersBuilders.ArcModifiers.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.ArcModifiers build();
+    method public androidx.wear.protolayout.ModifiersBuilders.ArcModifiers.Builder setClickable(androidx.wear.protolayout.ModifiersBuilders.Clickable);
+    method public androidx.wear.protolayout.ModifiersBuilders.ArcModifiers.Builder setSemantics(androidx.wear.protolayout.ModifiersBuilders.Semantics);
+  }
+
+  public static final class ModifiersBuilders.Background {
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp? getColor();
+    method public androidx.wear.protolayout.ModifiersBuilders.Corner? getCorner();
+  }
+
+  public static final class ModifiersBuilders.Background.Builder {
+    ctor public ModifiersBuilders.Background.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Background build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Background.Builder setColor(androidx.wear.protolayout.ColorBuilders.ColorProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Background.Builder setCorner(androidx.wear.protolayout.ModifiersBuilders.Corner);
+  }
+
+  public static final class ModifiersBuilders.Border {
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp? getColor();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getWidth();
+  }
+
+  public static final class ModifiersBuilders.Border.Builder {
+    ctor public ModifiersBuilders.Border.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Border build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Border.Builder setColor(androidx.wear.protolayout.ColorBuilders.ColorProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Border.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.DpProp);
+  }
+
+  public static final class ModifiersBuilders.Clickable {
+    method public String getId();
+    method public androidx.wear.protolayout.ActionBuilders.Action? getOnClick();
+  }
+
+  public static final class ModifiersBuilders.Clickable.Builder {
+    ctor public ModifiersBuilders.Clickable.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Clickable build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Clickable.Builder setId(String);
+    method public androidx.wear.protolayout.ModifiersBuilders.Clickable.Builder setOnClick(androidx.wear.protolayout.ActionBuilders.Action);
+  }
+
+  public static final class ModifiersBuilders.Corner {
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getRadius();
+  }
+
+  public static final class ModifiersBuilders.Corner.Builder {
+    ctor public ModifiersBuilders.Corner.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Corner build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Corner.Builder setRadius(androidx.wear.protolayout.DimensionBuilders.DpProp);
+  }
+
+  public static final class ModifiersBuilders.ElementMetadata {
+    method public byte[] getTagData();
+  }
+
+  public static final class ModifiersBuilders.ElementMetadata.Builder {
+    ctor public ModifiersBuilders.ElementMetadata.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.ElementMetadata build();
+    method public androidx.wear.protolayout.ModifiersBuilders.ElementMetadata.Builder setTagData(byte[]);
+  }
+
+  public static final class ModifiersBuilders.Modifiers {
+    method public androidx.wear.protolayout.ModifiersBuilders.Background? getBackground();
+    method public androidx.wear.protolayout.ModifiersBuilders.Border? getBorder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Clickable? getClickable();
+    method public androidx.wear.protolayout.ModifiersBuilders.ElementMetadata? getMetadata();
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding? getPadding();
+    method public androidx.wear.protolayout.ModifiersBuilders.Semantics? getSemantics();
+  }
+
+  public static final class ModifiersBuilders.Modifiers.Builder {
+    ctor public ModifiersBuilders.Modifiers.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers.Builder setBackground(androidx.wear.protolayout.ModifiersBuilders.Background);
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers.Builder setBorder(androidx.wear.protolayout.ModifiersBuilders.Border);
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers.Builder setClickable(androidx.wear.protolayout.ModifiersBuilders.Clickable);
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers.Builder setMetadata(androidx.wear.protolayout.ModifiersBuilders.ElementMetadata);
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers.Builder setPadding(androidx.wear.protolayout.ModifiersBuilders.Padding);
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers.Builder setSemantics(androidx.wear.protolayout.ModifiersBuilders.Semantics);
+  }
+
+  public static final class ModifiersBuilders.Padding {
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getBottom();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getEnd();
+    method public androidx.wear.protolayout.TypeBuilders.BoolProp? getRtlAware();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getStart();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getTop();
+  }
+
+  public static final class ModifiersBuilders.Padding.Builder {
+    ctor public ModifiersBuilders.Padding.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setAll(androidx.wear.protolayout.DimensionBuilders.DpProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setBottom(androidx.wear.protolayout.DimensionBuilders.DpProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setEnd(androidx.wear.protolayout.DimensionBuilders.DpProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setRtlAware(androidx.wear.protolayout.TypeBuilders.BoolProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setRtlAware(boolean);
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setStart(androidx.wear.protolayout.DimensionBuilders.DpProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setTop(androidx.wear.protolayout.DimensionBuilders.DpProp);
+  }
+
+  public static final class ModifiersBuilders.Semantics {
+    method public String getContentDescription();
+  }
+
+  public static final class ModifiersBuilders.Semantics.Builder {
+    ctor public ModifiersBuilders.Semantics.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Semantics build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Semantics.Builder setContentDescription(String);
+  }
+
+  public static final class ModifiersBuilders.SpanModifiers {
+    method public androidx.wear.protolayout.ModifiersBuilders.Clickable? getClickable();
+  }
+
+  public static final class ModifiersBuilders.SpanModifiers.Builder {
+    ctor public ModifiersBuilders.SpanModifiers.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.SpanModifiers build();
+    method public androidx.wear.protolayout.ModifiersBuilders.SpanModifiers.Builder setClickable(androidx.wear.protolayout.ModifiersBuilders.Clickable);
+  }
+
+  public final class ResourceBuilders {
+    field public static final int IMAGE_FORMAT_RGB_565 = 1; // 0x1
+    field public static final int IMAGE_FORMAT_UNDEFINED = 0; // 0x0
+  }
+
+  public static final class ResourceBuilders.AndroidImageResourceByResId {
+    method @DrawableRes public int getResourceId();
+  }
+
+  public static final class ResourceBuilders.AndroidImageResourceByResId.Builder {
+    ctor public ResourceBuilders.AndroidImageResourceByResId.Builder();
+    method public androidx.wear.protolayout.ResourceBuilders.AndroidImageResourceByResId build();
+    method public androidx.wear.protolayout.ResourceBuilders.AndroidImageResourceByResId.Builder setResourceId(@DrawableRes int);
+  }
+
+  public static final class ResourceBuilders.ImageResource {
+    method public androidx.wear.protolayout.ResourceBuilders.AndroidImageResourceByResId? getAndroidResourceByResId();
+    method public androidx.wear.protolayout.ResourceBuilders.InlineImageResource? getInlineResource();
+  }
+
+  public static final class ResourceBuilders.ImageResource.Builder {
+    ctor public ResourceBuilders.ImageResource.Builder();
+    method public androidx.wear.protolayout.ResourceBuilders.ImageResource build();
+    method public androidx.wear.protolayout.ResourceBuilders.ImageResource.Builder setAndroidResourceByResId(androidx.wear.protolayout.ResourceBuilders.AndroidImageResourceByResId);
+    method public androidx.wear.protolayout.ResourceBuilders.ImageResource.Builder setInlineResource(androidx.wear.protolayout.ResourceBuilders.InlineImageResource);
+  }
+
+  public static final class ResourceBuilders.InlineImageResource {
+    method public byte[] getData();
+    method public int getFormat();
+    method @Dimension(unit=androidx.annotation.Dimension.PX) public int getHeightPx();
+    method @Dimension(unit=androidx.annotation.Dimension.PX) public int getWidthPx();
+  }
+
+  public static final class ResourceBuilders.InlineImageResource.Builder {
+    ctor public ResourceBuilders.InlineImageResource.Builder();
+    method public androidx.wear.protolayout.ResourceBuilders.InlineImageResource build();
+    method public androidx.wear.protolayout.ResourceBuilders.InlineImageResource.Builder setData(byte[]);
+    method public androidx.wear.protolayout.ResourceBuilders.InlineImageResource.Builder setFormat(int);
+    method public androidx.wear.protolayout.ResourceBuilders.InlineImageResource.Builder setHeightPx(@Dimension(unit=androidx.annotation.Dimension.PX) int);
+    method public androidx.wear.protolayout.ResourceBuilders.InlineImageResource.Builder setWidthPx(@Dimension(unit=androidx.annotation.Dimension.PX) int);
+  }
+
+  public static final class ResourceBuilders.Resources {
+    method public java.util.Map<java.lang.String!,androidx.wear.protolayout.ResourceBuilders.ImageResource!> getIdToImageMapping();
+    method public String getVersion();
+  }
+
+  public static final class ResourceBuilders.Resources.Builder {
+    ctor public ResourceBuilders.Resources.Builder();
+    method public androidx.wear.protolayout.ResourceBuilders.Resources.Builder addIdToImageMapping(String, androidx.wear.protolayout.ResourceBuilders.ImageResource);
+    method public androidx.wear.protolayout.ResourceBuilders.Resources build();
+    method public androidx.wear.protolayout.ResourceBuilders.Resources.Builder setVersion(String);
+  }
+
+  public final class StateBuilders {
+  }
+
+  public static final class StateBuilders.State {
+    method public java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!> getIdToValueMapping();
+    method public String getLastClickableId();
+  }
+
+  public static final class StateBuilders.State.Builder {
+    ctor public StateBuilders.State.Builder();
+    method public androidx.wear.protolayout.StateBuilders.State.Builder addIdToValueMapping(String, androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue);
+    method public androidx.wear.protolayout.StateBuilders.State build();
+  }
+
+  public final class TimelineBuilders {
+  }
+
+  public static final class TimelineBuilders.TimeInterval {
+    method public long getEndMillis();
+    method public long getStartMillis();
+  }
+
+  public static final class TimelineBuilders.TimeInterval.Builder {
+    ctor public TimelineBuilders.TimeInterval.Builder();
+    method public androidx.wear.protolayout.TimelineBuilders.TimeInterval build();
+    method public androidx.wear.protolayout.TimelineBuilders.TimeInterval.Builder setEndMillis(long);
+    method public androidx.wear.protolayout.TimelineBuilders.TimeInterval.Builder setStartMillis(long);
+  }
+
+  public static final class TimelineBuilders.Timeline {
+    method public static androidx.wear.protolayout.TimelineBuilders.Timeline fromLayoutElement(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public java.util.List<androidx.wear.protolayout.TimelineBuilders.TimelineEntry!> getTimelineEntries();
+  }
+
+  public static final class TimelineBuilders.Timeline.Builder {
+    ctor public TimelineBuilders.Timeline.Builder();
+    method public androidx.wear.protolayout.TimelineBuilders.Timeline.Builder addTimelineEntry(androidx.wear.protolayout.TimelineBuilders.TimelineEntry);
+    method public androidx.wear.protolayout.TimelineBuilders.Timeline build();
+  }
+
+  public static final class TimelineBuilders.TimelineEntry {
+    method public static androidx.wear.protolayout.TimelineBuilders.TimelineEntry fromLayoutElement(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Layout? getLayout();
+    method public androidx.wear.protolayout.TimelineBuilders.TimeInterval? getValidity();
+  }
+
+  public static final class TimelineBuilders.TimelineEntry.Builder {
+    ctor public TimelineBuilders.TimelineEntry.Builder();
+    method public androidx.wear.protolayout.TimelineBuilders.TimelineEntry build();
+    method public androidx.wear.protolayout.TimelineBuilders.TimelineEntry.Builder setLayout(androidx.wear.protolayout.LayoutElementBuilders.Layout);
+    method public androidx.wear.protolayout.TimelineBuilders.TimelineEntry.Builder setValidity(androidx.wear.protolayout.TimelineBuilders.TimeInterval);
+  }
+
+  public final class TypeBuilders {
+  }
+
+  public static final class TypeBuilders.BoolProp {
+    method public boolean getValue();
+  }
+
+  public static final class TypeBuilders.BoolProp.Builder {
+    ctor public TypeBuilders.BoolProp.Builder();
+    method public androidx.wear.protolayout.TypeBuilders.BoolProp build();
+    method public androidx.wear.protolayout.TypeBuilders.BoolProp.Builder setValue(boolean);
+  }
+
+  public static final class TypeBuilders.FloatProp {
+    method public float getValue();
+  }
+
+  public static final class TypeBuilders.FloatProp.Builder {
+    ctor public TypeBuilders.FloatProp.Builder();
+    method public androidx.wear.protolayout.TypeBuilders.FloatProp build();
+    method public androidx.wear.protolayout.TypeBuilders.FloatProp.Builder setValue(float);
+  }
+
+  public static final class TypeBuilders.Int32Prop {
+    method public int getValue();
+  }
+
+  public static final class TypeBuilders.Int32Prop.Builder {
+    ctor public TypeBuilders.Int32Prop.Builder();
+    method public androidx.wear.protolayout.TypeBuilders.Int32Prop build();
+    method public androidx.wear.protolayout.TypeBuilders.Int32Prop.Builder setValue(int);
+  }
+
+  public static final class TypeBuilders.StringProp {
+    method public String getValue();
+  }
+
+  public static final class TypeBuilders.StringProp.Builder {
+    ctor public TypeBuilders.StringProp.Builder();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp build();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp.Builder setValue(String);
+  }
+
+}
+
diff --git a/wear/protolayout/protolayout/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
index e6f50d0..38735dc 100644
--- a/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
@@ -1 +1,1023 @@
 // Signature format: 4.0
+package androidx.wear.protolayout {
+
+  public final class ActionBuilders {
+    method public static androidx.wear.protolayout.ActionBuilders.AndroidBooleanExtra booleanExtra(boolean);
+    method public static androidx.wear.protolayout.ActionBuilders.AndroidDoubleExtra doubleExtra(double);
+    method public static androidx.wear.protolayout.ActionBuilders.AndroidIntExtra intExtra(int);
+    method public static androidx.wear.protolayout.ActionBuilders.AndroidLongExtra longExtra(long);
+    method public static androidx.wear.protolayout.ActionBuilders.AndroidStringExtra stringExtra(String);
+  }
+
+  public static interface ActionBuilders.Action {
+  }
+
+  public static interface ActionBuilders.Action.Builder {
+    method public androidx.wear.protolayout.ActionBuilders.Action build();
+  }
+
+  public static final class ActionBuilders.AndroidActivity {
+    method public String getClassName();
+    method public java.util.Map<java.lang.String!,androidx.wear.protolayout.ActionBuilders.AndroidExtra!> getKeyToExtraMapping();
+    method public String getPackageName();
+  }
+
+  public static final class ActionBuilders.AndroidActivity.Builder {
+    ctor public ActionBuilders.AndroidActivity.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidActivity.Builder addKeyToExtraMapping(String, androidx.wear.protolayout.ActionBuilders.AndroidExtra);
+    method public androidx.wear.protolayout.ActionBuilders.AndroidActivity build();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidActivity.Builder setClassName(String);
+    method public androidx.wear.protolayout.ActionBuilders.AndroidActivity.Builder setPackageName(String);
+  }
+
+  public static final class ActionBuilders.AndroidBooleanExtra implements androidx.wear.protolayout.ActionBuilders.AndroidExtra {
+    method public boolean getValue();
+  }
+
+  public static final class ActionBuilders.AndroidBooleanExtra.Builder implements androidx.wear.protolayout.ActionBuilders.AndroidExtra.Builder {
+    ctor public ActionBuilders.AndroidBooleanExtra.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidBooleanExtra build();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidBooleanExtra.Builder setValue(boolean);
+  }
+
+  public static final class ActionBuilders.AndroidDoubleExtra implements androidx.wear.protolayout.ActionBuilders.AndroidExtra {
+    method public double getValue();
+  }
+
+  public static final class ActionBuilders.AndroidDoubleExtra.Builder implements androidx.wear.protolayout.ActionBuilders.AndroidExtra.Builder {
+    ctor public ActionBuilders.AndroidDoubleExtra.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidDoubleExtra build();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidDoubleExtra.Builder setValue(double);
+  }
+
+  public static interface ActionBuilders.AndroidExtra {
+  }
+
+  public static interface ActionBuilders.AndroidExtra.Builder {
+    method public androidx.wear.protolayout.ActionBuilders.AndroidExtra build();
+  }
+
+  public static final class ActionBuilders.AndroidIntExtra implements androidx.wear.protolayout.ActionBuilders.AndroidExtra {
+    method public int getValue();
+  }
+
+  public static final class ActionBuilders.AndroidIntExtra.Builder implements androidx.wear.protolayout.ActionBuilders.AndroidExtra.Builder {
+    ctor public ActionBuilders.AndroidIntExtra.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidIntExtra build();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidIntExtra.Builder setValue(int);
+  }
+
+  public static final class ActionBuilders.AndroidLongExtra implements androidx.wear.protolayout.ActionBuilders.AndroidExtra {
+    method public long getValue();
+  }
+
+  public static final class ActionBuilders.AndroidLongExtra.Builder implements androidx.wear.protolayout.ActionBuilders.AndroidExtra.Builder {
+    ctor public ActionBuilders.AndroidLongExtra.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidLongExtra build();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidLongExtra.Builder setValue(long);
+  }
+
+  public static final class ActionBuilders.AndroidStringExtra implements androidx.wear.protolayout.ActionBuilders.AndroidExtra {
+    method public String getValue();
+  }
+
+  public static final class ActionBuilders.AndroidStringExtra.Builder implements androidx.wear.protolayout.ActionBuilders.AndroidExtra.Builder {
+    ctor public ActionBuilders.AndroidStringExtra.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidStringExtra build();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidStringExtra.Builder setValue(String);
+  }
+
+  public static final class ActionBuilders.LaunchAction implements androidx.wear.protolayout.ActionBuilders.Action {
+    method public androidx.wear.protolayout.ActionBuilders.AndroidActivity? getAndroidActivity();
+  }
+
+  public static final class ActionBuilders.LaunchAction.Builder implements androidx.wear.protolayout.ActionBuilders.Action.Builder {
+    ctor public ActionBuilders.LaunchAction.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.LaunchAction build();
+    method public androidx.wear.protolayout.ActionBuilders.LaunchAction.Builder setAndroidActivity(androidx.wear.protolayout.ActionBuilders.AndroidActivity);
+  }
+
+  public static final class ActionBuilders.LoadAction implements androidx.wear.protolayout.ActionBuilders.Action {
+    method public androidx.wear.protolayout.StateBuilders.State? getRequestState();
+  }
+
+  public static final class ActionBuilders.LoadAction.Builder implements androidx.wear.protolayout.ActionBuilders.Action.Builder {
+    ctor public ActionBuilders.LoadAction.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.LoadAction build();
+    method public androidx.wear.protolayout.ActionBuilders.LoadAction.Builder setRequestState(androidx.wear.protolayout.StateBuilders.State);
+  }
+
+  public static interface ActionBuilders.LocalAction {
+  }
+
+  public static interface ActionBuilders.LocalAction.Builder {
+    method public androidx.wear.protolayout.ActionBuilders.LocalAction build();
+  }
+
+  public static final class ActionBuilders.SetStateAction implements androidx.wear.protolayout.ActionBuilders.LocalAction {
+    method public String getTargetKey();
+    method public androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue? getValue();
+  }
+
+  public static final class ActionBuilders.SetStateAction.Builder implements androidx.wear.protolayout.ActionBuilders.LocalAction.Builder {
+    ctor public ActionBuilders.SetStateAction.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.SetStateAction build();
+    method public androidx.wear.protolayout.ActionBuilders.SetStateAction.Builder setTargetKey(String);
+    method public androidx.wear.protolayout.ActionBuilders.SetStateAction.Builder setValue(androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue);
+  }
+
+  public final class ColorBuilders {
+    method public static androidx.wear.protolayout.ColorBuilders.ColorProp argb(@ColorInt int);
+  }
+
+  public static final class ColorBuilders.ColorProp {
+    method @ColorInt public int getArgb();
+  }
+
+  public static final class ColorBuilders.ColorProp.Builder {
+    ctor public ColorBuilders.ColorProp.Builder();
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp build();
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp.Builder setArgb(@ColorInt int);
+  }
+
+  public final class DeviceParametersBuilders {
+    field public static final int DEVICE_PLATFORM_UNDEFINED = 0; // 0x0
+    field public static final int DEVICE_PLATFORM_WEAR_OS = 1; // 0x1
+    field public static final int SCREEN_SHAPE_RECT = 2; // 0x2
+    field public static final int SCREEN_SHAPE_ROUND = 1; // 0x1
+    field public static final int SCREEN_SHAPE_UNDEFINED = 0; // 0x0
+  }
+
+  public static final class DeviceParametersBuilders.DeviceParameters {
+    method public int getDevicePlatform();
+    method @FloatRange(from=0.0, fromInclusive=false, toInclusive=false) public float getScreenDensity();
+    method @Dimension(unit=androidx.annotation.Dimension.DP) public int getScreenHeightDp();
+    method public int getScreenShape();
+    method @Dimension(unit=androidx.annotation.Dimension.DP) public int getScreenWidthDp();
+  }
+
+  public static final class DeviceParametersBuilders.DeviceParameters.Builder {
+    ctor public DeviceParametersBuilders.DeviceParameters.Builder();
+    method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters build();
+    method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters.Builder setDevicePlatform(int);
+    method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters.Builder setScreenDensity(@FloatRange(from=0.0, fromInclusive=false, toInclusive=false) float);
+    method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters.Builder setScreenHeightDp(@Dimension(unit=androidx.annotation.Dimension.DP) int);
+    method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters.Builder setScreenShape(int);
+    method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters.Builder setScreenWidthDp(@Dimension(unit=androidx.annotation.Dimension.DP) int);
+  }
+
+  public final class DimensionBuilders {
+    method public static androidx.wear.protolayout.DimensionBuilders.DegreesProp degrees(float);
+    method public static androidx.wear.protolayout.DimensionBuilders.DpProp dp(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+    method public static androidx.wear.protolayout.DimensionBuilders.EmProp em(int);
+    method public static androidx.wear.protolayout.DimensionBuilders.EmProp em(float);
+    method public static androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp expand();
+    method public static androidx.wear.protolayout.DimensionBuilders.SpProp sp(@Dimension(unit=androidx.annotation.Dimension.SP) float);
+    method public static androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp wrap();
+  }
+
+  public static interface DimensionBuilders.ContainerDimension {
+  }
+
+  public static interface DimensionBuilders.ContainerDimension.Builder {
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension build();
+  }
+
+  public static final class DimensionBuilders.DegreesProp {
+    method public float getValue();
+  }
+
+  public static final class DimensionBuilders.DegreesProp.Builder {
+    ctor public DimensionBuilders.DegreesProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.DegreesProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.DegreesProp.Builder setValue(float);
+  }
+
+  public static final class DimensionBuilders.DpProp implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension androidx.wear.protolayout.DimensionBuilders.ImageDimension androidx.wear.protolayout.DimensionBuilders.SpacerDimension {
+    method @Dimension(unit=androidx.annotation.Dimension.DP) public float getValue();
+  }
+
+  public static final class DimensionBuilders.DpProp.Builder implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension.Builder androidx.wear.protolayout.DimensionBuilders.ImageDimension.Builder androidx.wear.protolayout.DimensionBuilders.SpacerDimension.Builder {
+    ctor public DimensionBuilders.DpProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp.Builder setValue(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+  }
+
+  public static final class DimensionBuilders.EmProp {
+    method public float getValue();
+  }
+
+  public static final class DimensionBuilders.EmProp.Builder {
+    ctor public DimensionBuilders.EmProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.EmProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.EmProp.Builder setValue(float);
+  }
+
+  public static final class DimensionBuilders.ExpandedDimensionProp implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension androidx.wear.protolayout.DimensionBuilders.ImageDimension {
+  }
+
+  public static final class DimensionBuilders.ExpandedDimensionProp.Builder implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension.Builder androidx.wear.protolayout.DimensionBuilders.ImageDimension.Builder {
+    ctor public DimensionBuilders.ExpandedDimensionProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp build();
+  }
+
+  public static interface DimensionBuilders.ImageDimension {
+  }
+
+  public static interface DimensionBuilders.ImageDimension.Builder {
+    method public androidx.wear.protolayout.DimensionBuilders.ImageDimension build();
+  }
+
+  public static final class DimensionBuilders.ProportionalDimensionProp implements androidx.wear.protolayout.DimensionBuilders.ImageDimension {
+    method @IntRange(from=0) public int getAspectRatioHeight();
+    method @IntRange(from=0) public int getAspectRatioWidth();
+  }
+
+  public static final class DimensionBuilders.ProportionalDimensionProp.Builder implements androidx.wear.protolayout.DimensionBuilders.ImageDimension.Builder {
+    ctor public DimensionBuilders.ProportionalDimensionProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.ProportionalDimensionProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.ProportionalDimensionProp.Builder setAspectRatioHeight(@IntRange(from=0) int);
+    method public androidx.wear.protolayout.DimensionBuilders.ProportionalDimensionProp.Builder setAspectRatioWidth(@IntRange(from=0) int);
+  }
+
+  public static final class DimensionBuilders.SpProp {
+    method @Dimension(unit=androidx.annotation.Dimension.SP) public float getValue();
+  }
+
+  public static final class DimensionBuilders.SpProp.Builder {
+    ctor public DimensionBuilders.SpProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.SpProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.SpProp.Builder setValue(@Dimension(unit=androidx.annotation.Dimension.SP) float);
+  }
+
+  public static interface DimensionBuilders.SpacerDimension {
+  }
+
+  public static interface DimensionBuilders.SpacerDimension.Builder {
+    method public androidx.wear.protolayout.DimensionBuilders.SpacerDimension build();
+  }
+
+  public static final class DimensionBuilders.WrappedDimensionProp implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension {
+  }
+
+  public static final class DimensionBuilders.WrappedDimensionProp.Builder implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension.Builder {
+    ctor public DimensionBuilders.WrappedDimensionProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp build();
+  }
+
+  public final class LayoutElementBuilders {
+    field public static final int ARC_ANCHOR_CENTER = 2; // 0x2
+    field public static final int ARC_ANCHOR_END = 3; // 0x3
+    field public static final int ARC_ANCHOR_START = 1; // 0x1
+    field public static final int ARC_ANCHOR_UNDEFINED = 0; // 0x0
+    field public static final int CONTENT_SCALE_MODE_CROP = 2; // 0x2
+    field public static final int CONTENT_SCALE_MODE_FILL_BOUNDS = 3; // 0x3
+    field public static final int CONTENT_SCALE_MODE_FIT = 1; // 0x1
+    field public static final int CONTENT_SCALE_MODE_UNDEFINED = 0; // 0x0
+    field public static final int FONT_VARIANT_BODY = 2; // 0x2
+    field public static final int FONT_VARIANT_TITLE = 1; // 0x1
+    field public static final int FONT_VARIANT_UNDEFINED = 0; // 0x0
+    field public static final int FONT_WEIGHT_BOLD = 700; // 0x2bc
+    field @androidx.wear.protolayout.expression.ProtoLayoutExperimental public static final int FONT_WEIGHT_MEDIUM = 500; // 0x1f4
+    field public static final int FONT_WEIGHT_NORMAL = 400; // 0x190
+    field public static final int FONT_WEIGHT_UNDEFINED = 0; // 0x0
+    field public static final int HORIZONTAL_ALIGN_CENTER = 2; // 0x2
+    field public static final int HORIZONTAL_ALIGN_END = 5; // 0x5
+    field public static final int HORIZONTAL_ALIGN_LEFT = 1; // 0x1
+    field public static final int HORIZONTAL_ALIGN_RIGHT = 3; // 0x3
+    field public static final int HORIZONTAL_ALIGN_START = 4; // 0x4
+    field public static final int HORIZONTAL_ALIGN_UNDEFINED = 0; // 0x0
+    field public static final int SPAN_VERTICAL_ALIGN_BOTTOM = 1; // 0x1
+    field public static final int SPAN_VERTICAL_ALIGN_TEXT_BASELINE = 2; // 0x2
+    field public static final int SPAN_VERTICAL_ALIGN_UNDEFINED = 0; // 0x0
+    field public static final int TEXT_ALIGN_CENTER = 2; // 0x2
+    field public static final int TEXT_ALIGN_END = 3; // 0x3
+    field public static final int TEXT_ALIGN_START = 1; // 0x1
+    field public static final int TEXT_ALIGN_UNDEFINED = 0; // 0x0
+    field public static final int TEXT_OVERFLOW_ELLIPSIZE_END = 2; // 0x2
+    field public static final int TEXT_OVERFLOW_TRUNCATE = 1; // 0x1
+    field public static final int TEXT_OVERFLOW_UNDEFINED = 0; // 0x0
+    field public static final int VERTICAL_ALIGN_BOTTOM = 3; // 0x3
+    field public static final int VERTICAL_ALIGN_CENTER = 2; // 0x2
+    field public static final int VERTICAL_ALIGN_TOP = 1; // 0x1
+    field public static final int VERTICAL_ALIGN_UNDEFINED = 0; // 0x0
+  }
+
+  public static final class LayoutElementBuilders.Arc implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public androidx.wear.protolayout.DimensionBuilders.DegreesProp? getAnchorAngle();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAnchorTypeProp? getAnchorType();
+    method public java.util.List<androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement!> getContents();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp? getVerticalAlign();
+  }
+
+  public static final class LayoutElementBuilders.Arc.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Arc.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder addContent(androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder setAnchorAngle(androidx.wear.protolayout.DimensionBuilders.DegreesProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder setAnchorType(androidx.wear.protolayout.LayoutElementBuilders.ArcAnchorTypeProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder setAnchorType(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder setVerticalAlign(androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder setVerticalAlign(int);
+  }
+
+  public static final class LayoutElementBuilders.ArcAdapter implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement {
+    method public androidx.wear.protolayout.LayoutElementBuilders.LayoutElement? getContent();
+    method public androidx.wear.protolayout.TypeBuilders.BoolProp? getRotateContents();
+  }
+
+  public static final class LayoutElementBuilders.ArcAdapter.Builder implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement.Builder {
+    ctor public LayoutElementBuilders.ArcAdapter.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAdapter build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAdapter.Builder setContent(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAdapter.Builder setRotateContents(androidx.wear.protolayout.TypeBuilders.BoolProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAdapter.Builder setRotateContents(boolean);
+  }
+
+  public static final class LayoutElementBuilders.ArcAnchorTypeProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.ArcAnchorTypeProp.Builder {
+    ctor public LayoutElementBuilders.ArcAnchorTypeProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAnchorTypeProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAnchorTypeProp.Builder setValue(int);
+  }
+
+  public static interface LayoutElementBuilders.ArcLayoutElement {
+  }
+
+  public static interface LayoutElementBuilders.ArcLayoutElement.Builder {
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement build();
+  }
+
+  public static final class LayoutElementBuilders.ArcLine implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement {
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp? getColor();
+    method public androidx.wear.protolayout.DimensionBuilders.DegreesProp? getLength();
+    method public androidx.wear.protolayout.ModifiersBuilders.ArcModifiers? getModifiers();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getThickness();
+  }
+
+  public static final class LayoutElementBuilders.ArcLine.Builder implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement.Builder {
+    ctor public LayoutElementBuilders.ArcLine.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcLine build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcLine.Builder setColor(androidx.wear.protolayout.ColorBuilders.ColorProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcLine.Builder setLength(androidx.wear.protolayout.DimensionBuilders.DegreesProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcLine.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.ArcModifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcLine.Builder setThickness(androidx.wear.protolayout.DimensionBuilders.DpProp);
+  }
+
+  public static final class LayoutElementBuilders.ArcSpacer implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement {
+    method public androidx.wear.protolayout.DimensionBuilders.DegreesProp? getLength();
+    method public androidx.wear.protolayout.ModifiersBuilders.ArcModifiers? getModifiers();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getThickness();
+  }
+
+  public static final class LayoutElementBuilders.ArcSpacer.Builder implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement.Builder {
+    ctor public LayoutElementBuilders.ArcSpacer.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcSpacer build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcSpacer.Builder setLength(androidx.wear.protolayout.DimensionBuilders.DegreesProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcSpacer.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.ArcModifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcSpacer.Builder setThickness(androidx.wear.protolayout.DimensionBuilders.DpProp);
+  }
+
+  public static final class LayoutElementBuilders.ArcText implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement {
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle? getFontStyle();
+    method public androidx.wear.protolayout.ModifiersBuilders.ArcModifiers? getModifiers();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getText();
+  }
+
+  public static final class LayoutElementBuilders.ArcText.Builder implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement.Builder {
+    ctor public LayoutElementBuilders.ArcText.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcText build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcText.Builder setFontStyle(androidx.wear.protolayout.LayoutElementBuilders.FontStyle);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcText.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.ArcModifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcText.Builder setText(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcText.Builder setText(String);
+  }
+
+  public static final class LayoutElementBuilders.Box implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public java.util.List<androidx.wear.protolayout.LayoutElementBuilders.LayoutElement!> getContents();
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension? getHeight();
+    method public androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp? getHorizontalAlignment();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp? getVerticalAlignment();
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension? getWidth();
+  }
+
+  public static final class LayoutElementBuilders.Box.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Box.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder addContent(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setHorizontalAlignment(androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setHorizontalAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setVerticalAlignment(androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setVerticalAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
+  }
+
+  public static final class LayoutElementBuilders.ColorFilter {
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp? getTint();
+  }
+
+  public static final class LayoutElementBuilders.ColorFilter.Builder {
+    ctor public LayoutElementBuilders.ColorFilter.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ColorFilter build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ColorFilter.Builder setTint(androidx.wear.protolayout.ColorBuilders.ColorProp);
+  }
+
+  public static final class LayoutElementBuilders.Column implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public java.util.List<androidx.wear.protolayout.LayoutElementBuilders.LayoutElement!> getContents();
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension? getHeight();
+    method public androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp? getHorizontalAlignment();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension? getWidth();
+  }
+
+  public static final class LayoutElementBuilders.Column.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Column.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column.Builder addContent(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column.Builder setHorizontalAlignment(androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column.Builder setHorizontalAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
+  }
+
+  public static final class LayoutElementBuilders.ContentScaleModeProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.ContentScaleModeProp.Builder {
+    ctor public LayoutElementBuilders.ContentScaleModeProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ContentScaleModeProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ContentScaleModeProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.FontStyle {
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp? getColor();
+    method public androidx.wear.protolayout.TypeBuilders.BoolProp? getItalic();
+    method public androidx.wear.protolayout.DimensionBuilders.EmProp? getLetterSpacing();
+    method public androidx.wear.protolayout.DimensionBuilders.SpProp? getSize();
+    method public androidx.wear.protolayout.TypeBuilders.BoolProp? getUnderline();
+    method @androidx.wear.protolayout.expression.ProtoLayoutExperimental public androidx.wear.protolayout.LayoutElementBuilders.FontVariantProp? getVariant();
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontWeightProp? getWeight();
+  }
+
+  public static final class LayoutElementBuilders.FontStyle.Builder {
+    ctor public LayoutElementBuilders.FontStyle.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setColor(androidx.wear.protolayout.ColorBuilders.ColorProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setItalic(androidx.wear.protolayout.TypeBuilders.BoolProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setItalic(boolean);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setLetterSpacing(androidx.wear.protolayout.DimensionBuilders.EmProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setSize(androidx.wear.protolayout.DimensionBuilders.SpProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setUnderline(androidx.wear.protolayout.TypeBuilders.BoolProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setUnderline(boolean);
+    method @androidx.wear.protolayout.expression.ProtoLayoutExperimental public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setVariant(androidx.wear.protolayout.LayoutElementBuilders.FontVariantProp);
+    method @androidx.wear.protolayout.expression.ProtoLayoutExperimental public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setVariant(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setWeight(androidx.wear.protolayout.LayoutElementBuilders.FontWeightProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setWeight(int);
+  }
+
+  public static class LayoutElementBuilders.FontStyles {
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder body1(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder body2(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder button(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder caption1(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder caption2(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder display1(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder display2(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder display3(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder title1(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder title2(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder title3(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+  }
+
+  @androidx.wear.protolayout.expression.ProtoLayoutExperimental public static final class LayoutElementBuilders.FontVariantProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.FontVariantProp.Builder {
+    ctor public LayoutElementBuilders.FontVariantProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontVariantProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontVariantProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.FontWeightProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.FontWeightProp.Builder {
+    ctor public LayoutElementBuilders.FontWeightProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontWeightProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontWeightProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.HorizontalAlignmentProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.HorizontalAlignmentProp.Builder {
+    ctor public LayoutElementBuilders.HorizontalAlignmentProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.Image implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public androidx.wear.protolayout.LayoutElementBuilders.ColorFilter? getColorFilter();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ContentScaleModeProp? getContentScaleMode();
+    method public androidx.wear.protolayout.DimensionBuilders.ImageDimension? getHeight();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getResourceId();
+    method public androidx.wear.protolayout.DimensionBuilders.ImageDimension? getWidth();
+  }
+
+  public static final class LayoutElementBuilders.Image.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Image.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setColorFilter(androidx.wear.protolayout.LayoutElementBuilders.ColorFilter);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setContentScaleMode(androidx.wear.protolayout.LayoutElementBuilders.ContentScaleModeProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setContentScaleMode(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.ImageDimension);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setResourceId(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setResourceId(String);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.ImageDimension);
+  }
+
+  public static final class LayoutElementBuilders.Layout {
+    method @androidx.wear.protolayout.expression.ProtoLayoutExperimental public static androidx.wear.protolayout.LayoutElementBuilders.Layout? fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.Layout fromLayoutElement(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.LayoutElement? getRoot();
+    method @androidx.wear.protolayout.expression.ProtoLayoutExperimental public byte[] toByteArray();
+  }
+
+  public static final class LayoutElementBuilders.Layout.Builder {
+    ctor public LayoutElementBuilders.Layout.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Layout build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Layout.Builder setRoot(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+  }
+
+  public static interface LayoutElementBuilders.LayoutElement {
+  }
+
+  public static interface LayoutElementBuilders.LayoutElement.Builder {
+    method public androidx.wear.protolayout.LayoutElementBuilders.LayoutElement build();
+  }
+
+  public static final class LayoutElementBuilders.Row implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public java.util.List<androidx.wear.protolayout.LayoutElementBuilders.LayoutElement!> getContents();
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension? getHeight();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp? getVerticalAlignment();
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension? getWidth();
+  }
+
+  public static final class LayoutElementBuilders.Row.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Row.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row.Builder addContent(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row.Builder setVerticalAlignment(androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row.Builder setVerticalAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
+  }
+
+  public static final class LayoutElementBuilders.Spacer implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public androidx.wear.protolayout.DimensionBuilders.SpacerDimension? getHeight();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.DimensionBuilders.SpacerDimension? getWidth();
+  }
+
+  public static final class LayoutElementBuilders.Spacer.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Spacer.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spacer build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spacer.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.SpacerDimension);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spacer.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spacer.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.SpacerDimension);
+  }
+
+  public static interface LayoutElementBuilders.Span {
+  }
+
+  public static interface LayoutElementBuilders.Span.Builder {
+    method public androidx.wear.protolayout.LayoutElementBuilders.Span build();
+  }
+
+  public static final class LayoutElementBuilders.SpanImage implements androidx.wear.protolayout.LayoutElementBuilders.Span {
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanVerticalAlignmentProp? getAlignment();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getHeight();
+    method public androidx.wear.protolayout.ModifiersBuilders.SpanModifiers? getModifiers();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getResourceId();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getWidth();
+  }
+
+  public static final class LayoutElementBuilders.SpanImage.Builder implements androidx.wear.protolayout.LayoutElementBuilders.Span.Builder {
+    ctor public LayoutElementBuilders.SpanImage.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setAlignment(androidx.wear.protolayout.LayoutElementBuilders.SpanVerticalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.DpProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.SpanModifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setResourceId(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setResourceId(String);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.DpProp);
+  }
+
+  public static final class LayoutElementBuilders.SpanText implements androidx.wear.protolayout.LayoutElementBuilders.Span {
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle? getFontStyle();
+    method public androidx.wear.protolayout.ModifiersBuilders.SpanModifiers? getModifiers();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getText();
+  }
+
+  public static final class LayoutElementBuilders.SpanText.Builder implements androidx.wear.protolayout.LayoutElementBuilders.Span.Builder {
+    ctor public LayoutElementBuilders.SpanText.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanText build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanText.Builder setFontStyle(androidx.wear.protolayout.LayoutElementBuilders.FontStyle);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanText.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.SpanModifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanText.Builder setText(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanText.Builder setText(String);
+  }
+
+  public static final class LayoutElementBuilders.SpanVerticalAlignmentProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.SpanVerticalAlignmentProp.Builder {
+    ctor public LayoutElementBuilders.SpanVerticalAlignmentProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanVerticalAlignmentProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanVerticalAlignmentProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.Spannable implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public androidx.wear.protolayout.DimensionBuilders.SpProp? getLineHeight();
+    method public androidx.wear.protolayout.TypeBuilders.Int32Prop? getMaxLines();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp? getMultilineAlignment();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextOverflowProp? getOverflow();
+    method public java.util.List<androidx.wear.protolayout.LayoutElementBuilders.Span!> getSpans();
+  }
+
+  public static final class LayoutElementBuilders.Spannable.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Spannable.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder addSpan(androidx.wear.protolayout.LayoutElementBuilders.Span);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setLineHeight(androidx.wear.protolayout.DimensionBuilders.SpProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setMaxLines(androidx.wear.protolayout.TypeBuilders.Int32Prop);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setMaxLines(@IntRange(from=1) int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setMultilineAlignment(androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setMultilineAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setOverflow(androidx.wear.protolayout.LayoutElementBuilders.TextOverflowProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setOverflow(int);
+  }
+
+  public static final class LayoutElementBuilders.Text implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle? getFontStyle();
+    method public androidx.wear.protolayout.DimensionBuilders.SpProp? getLineHeight();
+    method public androidx.wear.protolayout.TypeBuilders.Int32Prop? getMaxLines();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextAlignmentProp? getMultilineAlignment();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextOverflowProp? getOverflow();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getText();
+  }
+
+  public static final class LayoutElementBuilders.Text.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Text.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setFontStyle(androidx.wear.protolayout.LayoutElementBuilders.FontStyle);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setLineHeight(androidx.wear.protolayout.DimensionBuilders.SpProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setMaxLines(androidx.wear.protolayout.TypeBuilders.Int32Prop);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setMaxLines(@IntRange(from=1) int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setMultilineAlignment(androidx.wear.protolayout.LayoutElementBuilders.TextAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setMultilineAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setOverflow(androidx.wear.protolayout.LayoutElementBuilders.TextOverflowProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setOverflow(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setText(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setText(String);
+  }
+
+  public static final class LayoutElementBuilders.TextAlignmentProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.TextAlignmentProp.Builder {
+    ctor public LayoutElementBuilders.TextAlignmentProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextAlignmentProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextAlignmentProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.TextOverflowProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.TextOverflowProp.Builder {
+    ctor public LayoutElementBuilders.TextOverflowProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextOverflowProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextOverflowProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.VerticalAlignmentProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.VerticalAlignmentProp.Builder {
+    ctor public LayoutElementBuilders.VerticalAlignmentProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp.Builder setValue(int);
+  }
+
+  public final class ModifiersBuilders {
+  }
+
+  public static final class ModifiersBuilders.ArcModifiers {
+    method public androidx.wear.protolayout.ModifiersBuilders.Clickable? getClickable();
+    method public androidx.wear.protolayout.ModifiersBuilders.Semantics? getSemantics();
+  }
+
+  public static final class ModifiersBuilders.ArcModifiers.Builder {
+    ctor public ModifiersBuilders.ArcModifiers.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.ArcModifiers build();
+    method public androidx.wear.protolayout.ModifiersBuilders.ArcModifiers.Builder setClickable(androidx.wear.protolayout.ModifiersBuilders.Clickable);
+    method public androidx.wear.protolayout.ModifiersBuilders.ArcModifiers.Builder setSemantics(androidx.wear.protolayout.ModifiersBuilders.Semantics);
+  }
+
+  public static final class ModifiersBuilders.Background {
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp? getColor();
+    method public androidx.wear.protolayout.ModifiersBuilders.Corner? getCorner();
+  }
+
+  public static final class ModifiersBuilders.Background.Builder {
+    ctor public ModifiersBuilders.Background.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Background build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Background.Builder setColor(androidx.wear.protolayout.ColorBuilders.ColorProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Background.Builder setCorner(androidx.wear.protolayout.ModifiersBuilders.Corner);
+  }
+
+  public static final class ModifiersBuilders.Border {
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp? getColor();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getWidth();
+  }
+
+  public static final class ModifiersBuilders.Border.Builder {
+    ctor public ModifiersBuilders.Border.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Border build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Border.Builder setColor(androidx.wear.protolayout.ColorBuilders.ColorProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Border.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.DpProp);
+  }
+
+  public static final class ModifiersBuilders.Clickable {
+    method public String getId();
+    method public androidx.wear.protolayout.ActionBuilders.Action? getOnClick();
+  }
+
+  public static final class ModifiersBuilders.Clickable.Builder {
+    ctor public ModifiersBuilders.Clickable.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Clickable build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Clickable.Builder setId(String);
+    method public androidx.wear.protolayout.ModifiersBuilders.Clickable.Builder setOnClick(androidx.wear.protolayout.ActionBuilders.Action);
+  }
+
+  public static final class ModifiersBuilders.Corner {
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getRadius();
+  }
+
+  public static final class ModifiersBuilders.Corner.Builder {
+    ctor public ModifiersBuilders.Corner.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Corner build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Corner.Builder setRadius(androidx.wear.protolayout.DimensionBuilders.DpProp);
+  }
+
+  public static final class ModifiersBuilders.ElementMetadata {
+    method public byte[] getTagData();
+  }
+
+  public static final class ModifiersBuilders.ElementMetadata.Builder {
+    ctor public ModifiersBuilders.ElementMetadata.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.ElementMetadata build();
+    method public androidx.wear.protolayout.ModifiersBuilders.ElementMetadata.Builder setTagData(byte[]);
+  }
+
+  public static final class ModifiersBuilders.Modifiers {
+    method public androidx.wear.protolayout.ModifiersBuilders.Background? getBackground();
+    method public androidx.wear.protolayout.ModifiersBuilders.Border? getBorder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Clickable? getClickable();
+    method public androidx.wear.protolayout.ModifiersBuilders.ElementMetadata? getMetadata();
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding? getPadding();
+    method public androidx.wear.protolayout.ModifiersBuilders.Semantics? getSemantics();
+  }
+
+  public static final class ModifiersBuilders.Modifiers.Builder {
+    ctor public ModifiersBuilders.Modifiers.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers.Builder setBackground(androidx.wear.protolayout.ModifiersBuilders.Background);
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers.Builder setBorder(androidx.wear.protolayout.ModifiersBuilders.Border);
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers.Builder setClickable(androidx.wear.protolayout.ModifiersBuilders.Clickable);
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers.Builder setMetadata(androidx.wear.protolayout.ModifiersBuilders.ElementMetadata);
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers.Builder setPadding(androidx.wear.protolayout.ModifiersBuilders.Padding);
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers.Builder setSemantics(androidx.wear.protolayout.ModifiersBuilders.Semantics);
+  }
+
+  public static final class ModifiersBuilders.Padding {
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getBottom();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getEnd();
+    method public androidx.wear.protolayout.TypeBuilders.BoolProp? getRtlAware();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getStart();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getTop();
+  }
+
+  public static final class ModifiersBuilders.Padding.Builder {
+    ctor public ModifiersBuilders.Padding.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setAll(androidx.wear.protolayout.DimensionBuilders.DpProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setBottom(androidx.wear.protolayout.DimensionBuilders.DpProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setEnd(androidx.wear.protolayout.DimensionBuilders.DpProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setRtlAware(androidx.wear.protolayout.TypeBuilders.BoolProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setRtlAware(boolean);
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setStart(androidx.wear.protolayout.DimensionBuilders.DpProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setTop(androidx.wear.protolayout.DimensionBuilders.DpProp);
+  }
+
+  public static final class ModifiersBuilders.Semantics {
+    method public String getContentDescription();
+  }
+
+  public static final class ModifiersBuilders.Semantics.Builder {
+    ctor public ModifiersBuilders.Semantics.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Semantics build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Semantics.Builder setContentDescription(String);
+  }
+
+  public static final class ModifiersBuilders.SpanModifiers {
+    method public androidx.wear.protolayout.ModifiersBuilders.Clickable? getClickable();
+  }
+
+  public static final class ModifiersBuilders.SpanModifiers.Builder {
+    ctor public ModifiersBuilders.SpanModifiers.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.SpanModifiers build();
+    method public androidx.wear.protolayout.ModifiersBuilders.SpanModifiers.Builder setClickable(androidx.wear.protolayout.ModifiersBuilders.Clickable);
+  }
+
+  public final class ResourceBuilders {
+    field public static final int IMAGE_FORMAT_RGB_565 = 1; // 0x1
+    field public static final int IMAGE_FORMAT_UNDEFINED = 0; // 0x0
+  }
+
+  public static final class ResourceBuilders.AndroidImageResourceByResId {
+    method @DrawableRes public int getResourceId();
+  }
+
+  public static final class ResourceBuilders.AndroidImageResourceByResId.Builder {
+    ctor public ResourceBuilders.AndroidImageResourceByResId.Builder();
+    method public androidx.wear.protolayout.ResourceBuilders.AndroidImageResourceByResId build();
+    method public androidx.wear.protolayout.ResourceBuilders.AndroidImageResourceByResId.Builder setResourceId(@DrawableRes int);
+  }
+
+  public static final class ResourceBuilders.ImageResource {
+    method public androidx.wear.protolayout.ResourceBuilders.AndroidImageResourceByResId? getAndroidResourceByResId();
+    method public androidx.wear.protolayout.ResourceBuilders.InlineImageResource? getInlineResource();
+  }
+
+  public static final class ResourceBuilders.ImageResource.Builder {
+    ctor public ResourceBuilders.ImageResource.Builder();
+    method public androidx.wear.protolayout.ResourceBuilders.ImageResource build();
+    method public androidx.wear.protolayout.ResourceBuilders.ImageResource.Builder setAndroidResourceByResId(androidx.wear.protolayout.ResourceBuilders.AndroidImageResourceByResId);
+    method public androidx.wear.protolayout.ResourceBuilders.ImageResource.Builder setInlineResource(androidx.wear.protolayout.ResourceBuilders.InlineImageResource);
+  }
+
+  public static final class ResourceBuilders.InlineImageResource {
+    method public byte[] getData();
+    method public int getFormat();
+    method @Dimension(unit=androidx.annotation.Dimension.PX) public int getHeightPx();
+    method @Dimension(unit=androidx.annotation.Dimension.PX) public int getWidthPx();
+  }
+
+  public static final class ResourceBuilders.InlineImageResource.Builder {
+    ctor public ResourceBuilders.InlineImageResource.Builder();
+    method public androidx.wear.protolayout.ResourceBuilders.InlineImageResource build();
+    method public androidx.wear.protolayout.ResourceBuilders.InlineImageResource.Builder setData(byte[]);
+    method public androidx.wear.protolayout.ResourceBuilders.InlineImageResource.Builder setFormat(int);
+    method public androidx.wear.protolayout.ResourceBuilders.InlineImageResource.Builder setHeightPx(@Dimension(unit=androidx.annotation.Dimension.PX) int);
+    method public androidx.wear.protolayout.ResourceBuilders.InlineImageResource.Builder setWidthPx(@Dimension(unit=androidx.annotation.Dimension.PX) int);
+  }
+
+  public static final class ResourceBuilders.Resources {
+    method @androidx.wear.protolayout.expression.ProtoLayoutExperimental public static androidx.wear.protolayout.ResourceBuilders.Resources? fromByteArray(byte[]);
+    method public java.util.Map<java.lang.String!,androidx.wear.protolayout.ResourceBuilders.ImageResource!> getIdToImageMapping();
+    method public String getVersion();
+    method @androidx.wear.protolayout.expression.ProtoLayoutExperimental public byte[] toByteArray();
+  }
+
+  public static final class ResourceBuilders.Resources.Builder {
+    ctor public ResourceBuilders.Resources.Builder();
+    method public androidx.wear.protolayout.ResourceBuilders.Resources.Builder addIdToImageMapping(String, androidx.wear.protolayout.ResourceBuilders.ImageResource);
+    method public androidx.wear.protolayout.ResourceBuilders.Resources build();
+    method public androidx.wear.protolayout.ResourceBuilders.Resources.Builder setVersion(String);
+  }
+
+  public final class StateBuilders {
+  }
+
+  public static final class StateBuilders.State {
+    method public java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!> getIdToValueMapping();
+    method public String getLastClickableId();
+  }
+
+  public static final class StateBuilders.State.Builder {
+    ctor public StateBuilders.State.Builder();
+    method public androidx.wear.protolayout.StateBuilders.State.Builder addIdToValueMapping(String, androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue);
+    method public androidx.wear.protolayout.StateBuilders.State build();
+  }
+
+  public final class TimelineBuilders {
+  }
+
+  public static final class TimelineBuilders.TimeInterval {
+    method public long getEndMillis();
+    method public long getStartMillis();
+  }
+
+  public static final class TimelineBuilders.TimeInterval.Builder {
+    ctor public TimelineBuilders.TimeInterval.Builder();
+    method public androidx.wear.protolayout.TimelineBuilders.TimeInterval build();
+    method public androidx.wear.protolayout.TimelineBuilders.TimeInterval.Builder setEndMillis(long);
+    method public androidx.wear.protolayout.TimelineBuilders.TimeInterval.Builder setStartMillis(long);
+  }
+
+  public static final class TimelineBuilders.Timeline {
+    method public static androidx.wear.protolayout.TimelineBuilders.Timeline fromLayoutElement(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public java.util.List<androidx.wear.protolayout.TimelineBuilders.TimelineEntry!> getTimelineEntries();
+  }
+
+  public static final class TimelineBuilders.Timeline.Builder {
+    ctor public TimelineBuilders.Timeline.Builder();
+    method public androidx.wear.protolayout.TimelineBuilders.Timeline.Builder addTimelineEntry(androidx.wear.protolayout.TimelineBuilders.TimelineEntry);
+    method public androidx.wear.protolayout.TimelineBuilders.Timeline build();
+  }
+
+  public static final class TimelineBuilders.TimelineEntry {
+    method public static androidx.wear.protolayout.TimelineBuilders.TimelineEntry fromLayoutElement(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Layout? getLayout();
+    method public androidx.wear.protolayout.TimelineBuilders.TimeInterval? getValidity();
+  }
+
+  public static final class TimelineBuilders.TimelineEntry.Builder {
+    ctor public TimelineBuilders.TimelineEntry.Builder();
+    method public androidx.wear.protolayout.TimelineBuilders.TimelineEntry build();
+    method public androidx.wear.protolayout.TimelineBuilders.TimelineEntry.Builder setLayout(androidx.wear.protolayout.LayoutElementBuilders.Layout);
+    method public androidx.wear.protolayout.TimelineBuilders.TimelineEntry.Builder setValidity(androidx.wear.protolayout.TimelineBuilders.TimeInterval);
+  }
+
+  public final class TypeBuilders {
+  }
+
+  public static final class TypeBuilders.BoolProp {
+    method public boolean getValue();
+  }
+
+  public static final class TypeBuilders.BoolProp.Builder {
+    ctor public TypeBuilders.BoolProp.Builder();
+    method public androidx.wear.protolayout.TypeBuilders.BoolProp build();
+    method public androidx.wear.protolayout.TypeBuilders.BoolProp.Builder setValue(boolean);
+  }
+
+  public static final class TypeBuilders.FloatProp {
+    method public float getValue();
+  }
+
+  public static final class TypeBuilders.FloatProp.Builder {
+    ctor public TypeBuilders.FloatProp.Builder();
+    method public androidx.wear.protolayout.TypeBuilders.FloatProp build();
+    method public androidx.wear.protolayout.TypeBuilders.FloatProp.Builder setValue(float);
+  }
+
+  public static final class TypeBuilders.Int32Prop {
+    method public int getValue();
+  }
+
+  public static final class TypeBuilders.Int32Prop.Builder {
+    ctor public TypeBuilders.Int32Prop.Builder();
+    method public androidx.wear.protolayout.TypeBuilders.Int32Prop build();
+    method public androidx.wear.protolayout.TypeBuilders.Int32Prop.Builder setValue(int);
+  }
+
+  public static final class TypeBuilders.StringProp {
+    method public String getValue();
+  }
+
+  public static final class TypeBuilders.StringProp.Builder {
+    ctor public TypeBuilders.StringProp.Builder();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp build();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp.Builder setValue(String);
+  }
+
+}
+
diff --git a/wear/protolayout/protolayout/api/restricted_current.txt b/wear/protolayout/protolayout/api/restricted_current.txt
index e6f50d0..2d0294f 100644
--- a/wear/protolayout/protolayout/api/restricted_current.txt
+++ b/wear/protolayout/protolayout/api/restricted_current.txt
@@ -1 +1,1005 @@
 // Signature format: 4.0
+package androidx.wear.protolayout {
+
+  public final class ActionBuilders {
+    method public static androidx.wear.protolayout.ActionBuilders.AndroidBooleanExtra booleanExtra(boolean);
+    method public static androidx.wear.protolayout.ActionBuilders.AndroidDoubleExtra doubleExtra(double);
+    method public static androidx.wear.protolayout.ActionBuilders.AndroidIntExtra intExtra(int);
+    method public static androidx.wear.protolayout.ActionBuilders.AndroidLongExtra longExtra(long);
+    method public static androidx.wear.protolayout.ActionBuilders.AndroidStringExtra stringExtra(String);
+  }
+
+  public static interface ActionBuilders.Action {
+  }
+
+  public static interface ActionBuilders.Action.Builder {
+    method public androidx.wear.protolayout.ActionBuilders.Action build();
+  }
+
+  public static final class ActionBuilders.AndroidActivity {
+    method public String getClassName();
+    method public java.util.Map<java.lang.String!,androidx.wear.protolayout.ActionBuilders.AndroidExtra!> getKeyToExtraMapping();
+    method public String getPackageName();
+  }
+
+  public static final class ActionBuilders.AndroidActivity.Builder {
+    ctor public ActionBuilders.AndroidActivity.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidActivity.Builder addKeyToExtraMapping(String, androidx.wear.protolayout.ActionBuilders.AndroidExtra);
+    method public androidx.wear.protolayout.ActionBuilders.AndroidActivity build();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidActivity.Builder setClassName(String);
+    method public androidx.wear.protolayout.ActionBuilders.AndroidActivity.Builder setPackageName(String);
+  }
+
+  public static final class ActionBuilders.AndroidBooleanExtra implements androidx.wear.protolayout.ActionBuilders.AndroidExtra {
+    method public boolean getValue();
+  }
+
+  public static final class ActionBuilders.AndroidBooleanExtra.Builder implements androidx.wear.protolayout.ActionBuilders.AndroidExtra.Builder {
+    ctor public ActionBuilders.AndroidBooleanExtra.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidBooleanExtra build();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidBooleanExtra.Builder setValue(boolean);
+  }
+
+  public static final class ActionBuilders.AndroidDoubleExtra implements androidx.wear.protolayout.ActionBuilders.AndroidExtra {
+    method public double getValue();
+  }
+
+  public static final class ActionBuilders.AndroidDoubleExtra.Builder implements androidx.wear.protolayout.ActionBuilders.AndroidExtra.Builder {
+    ctor public ActionBuilders.AndroidDoubleExtra.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidDoubleExtra build();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidDoubleExtra.Builder setValue(double);
+  }
+
+  public static interface ActionBuilders.AndroidExtra {
+  }
+
+  public static interface ActionBuilders.AndroidExtra.Builder {
+    method public androidx.wear.protolayout.ActionBuilders.AndroidExtra build();
+  }
+
+  public static final class ActionBuilders.AndroidIntExtra implements androidx.wear.protolayout.ActionBuilders.AndroidExtra {
+    method public int getValue();
+  }
+
+  public static final class ActionBuilders.AndroidIntExtra.Builder implements androidx.wear.protolayout.ActionBuilders.AndroidExtra.Builder {
+    ctor public ActionBuilders.AndroidIntExtra.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidIntExtra build();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidIntExtra.Builder setValue(int);
+  }
+
+  public static final class ActionBuilders.AndroidLongExtra implements androidx.wear.protolayout.ActionBuilders.AndroidExtra {
+    method public long getValue();
+  }
+
+  public static final class ActionBuilders.AndroidLongExtra.Builder implements androidx.wear.protolayout.ActionBuilders.AndroidExtra.Builder {
+    ctor public ActionBuilders.AndroidLongExtra.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidLongExtra build();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidLongExtra.Builder setValue(long);
+  }
+
+  public static final class ActionBuilders.AndroidStringExtra implements androidx.wear.protolayout.ActionBuilders.AndroidExtra {
+    method public String getValue();
+  }
+
+  public static final class ActionBuilders.AndroidStringExtra.Builder implements androidx.wear.protolayout.ActionBuilders.AndroidExtra.Builder {
+    ctor public ActionBuilders.AndroidStringExtra.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidStringExtra build();
+    method public androidx.wear.protolayout.ActionBuilders.AndroidStringExtra.Builder setValue(String);
+  }
+
+  public static final class ActionBuilders.LaunchAction implements androidx.wear.protolayout.ActionBuilders.Action {
+    method public androidx.wear.protolayout.ActionBuilders.AndroidActivity? getAndroidActivity();
+  }
+
+  public static final class ActionBuilders.LaunchAction.Builder implements androidx.wear.protolayout.ActionBuilders.Action.Builder {
+    ctor public ActionBuilders.LaunchAction.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.LaunchAction build();
+    method public androidx.wear.protolayout.ActionBuilders.LaunchAction.Builder setAndroidActivity(androidx.wear.protolayout.ActionBuilders.AndroidActivity);
+  }
+
+  public static final class ActionBuilders.LoadAction implements androidx.wear.protolayout.ActionBuilders.Action {
+    method public androidx.wear.protolayout.StateBuilders.State? getRequestState();
+  }
+
+  public static final class ActionBuilders.LoadAction.Builder implements androidx.wear.protolayout.ActionBuilders.Action.Builder {
+    ctor public ActionBuilders.LoadAction.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.LoadAction build();
+    method public androidx.wear.protolayout.ActionBuilders.LoadAction.Builder setRequestState(androidx.wear.protolayout.StateBuilders.State);
+  }
+
+  public static interface ActionBuilders.LocalAction {
+  }
+
+  public static interface ActionBuilders.LocalAction.Builder {
+    method public androidx.wear.protolayout.ActionBuilders.LocalAction build();
+  }
+
+  public static final class ActionBuilders.SetStateAction implements androidx.wear.protolayout.ActionBuilders.LocalAction {
+    method public String getTargetKey();
+    method public androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue? getValue();
+  }
+
+  public static final class ActionBuilders.SetStateAction.Builder implements androidx.wear.protolayout.ActionBuilders.LocalAction.Builder {
+    ctor public ActionBuilders.SetStateAction.Builder();
+    method public androidx.wear.protolayout.ActionBuilders.SetStateAction build();
+    method public androidx.wear.protolayout.ActionBuilders.SetStateAction.Builder setTargetKey(String);
+    method public androidx.wear.protolayout.ActionBuilders.SetStateAction.Builder setValue(androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue);
+  }
+
+  public final class ColorBuilders {
+    method public static androidx.wear.protolayout.ColorBuilders.ColorProp argb(@ColorInt int);
+  }
+
+  public static final class ColorBuilders.ColorProp {
+    method @ColorInt public int getArgb();
+  }
+
+  public static final class ColorBuilders.ColorProp.Builder {
+    ctor public ColorBuilders.ColorProp.Builder();
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp build();
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp.Builder setArgb(@ColorInt int);
+  }
+
+  public final class DeviceParametersBuilders {
+    field public static final int DEVICE_PLATFORM_UNDEFINED = 0; // 0x0
+    field public static final int DEVICE_PLATFORM_WEAR_OS = 1; // 0x1
+    field public static final int SCREEN_SHAPE_RECT = 2; // 0x2
+    field public static final int SCREEN_SHAPE_ROUND = 1; // 0x1
+    field public static final int SCREEN_SHAPE_UNDEFINED = 0; // 0x0
+  }
+
+  public static final class DeviceParametersBuilders.DeviceParameters {
+    method public int getDevicePlatform();
+    method @FloatRange(from=0.0, fromInclusive=false, toInclusive=false) public float getScreenDensity();
+    method @Dimension(unit=androidx.annotation.Dimension.DP) public int getScreenHeightDp();
+    method public int getScreenShape();
+    method @Dimension(unit=androidx.annotation.Dimension.DP) public int getScreenWidthDp();
+  }
+
+  public static final class DeviceParametersBuilders.DeviceParameters.Builder {
+    ctor public DeviceParametersBuilders.DeviceParameters.Builder();
+    method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters build();
+    method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters.Builder setDevicePlatform(int);
+    method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters.Builder setScreenDensity(@FloatRange(from=0.0, fromInclusive=false, toInclusive=false) float);
+    method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters.Builder setScreenHeightDp(@Dimension(unit=androidx.annotation.Dimension.DP) int);
+    method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters.Builder setScreenShape(int);
+    method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters.Builder setScreenWidthDp(@Dimension(unit=androidx.annotation.Dimension.DP) int);
+  }
+
+  public final class DimensionBuilders {
+    method public static androidx.wear.protolayout.DimensionBuilders.DegreesProp degrees(float);
+    method public static androidx.wear.protolayout.DimensionBuilders.DpProp dp(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+    method public static androidx.wear.protolayout.DimensionBuilders.EmProp em(int);
+    method public static androidx.wear.protolayout.DimensionBuilders.EmProp em(float);
+    method public static androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp expand();
+    method public static androidx.wear.protolayout.DimensionBuilders.SpProp sp(@Dimension(unit=androidx.annotation.Dimension.SP) float);
+    method public static androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp wrap();
+  }
+
+  public static interface DimensionBuilders.ContainerDimension {
+  }
+
+  public static interface DimensionBuilders.ContainerDimension.Builder {
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension build();
+  }
+
+  public static final class DimensionBuilders.DegreesProp {
+    method public float getValue();
+  }
+
+  public static final class DimensionBuilders.DegreesProp.Builder {
+    ctor public DimensionBuilders.DegreesProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.DegreesProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.DegreesProp.Builder setValue(float);
+  }
+
+  public static final class DimensionBuilders.DpProp implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension androidx.wear.protolayout.DimensionBuilders.ImageDimension androidx.wear.protolayout.DimensionBuilders.SpacerDimension {
+    method @Dimension(unit=androidx.annotation.Dimension.DP) public float getValue();
+  }
+
+  public static final class DimensionBuilders.DpProp.Builder implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension.Builder androidx.wear.protolayout.DimensionBuilders.ImageDimension.Builder androidx.wear.protolayout.DimensionBuilders.SpacerDimension.Builder {
+    ctor public DimensionBuilders.DpProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp.Builder setValue(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+  }
+
+  public static final class DimensionBuilders.EmProp {
+    method public float getValue();
+  }
+
+  public static final class DimensionBuilders.EmProp.Builder {
+    ctor public DimensionBuilders.EmProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.EmProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.EmProp.Builder setValue(float);
+  }
+
+  public static final class DimensionBuilders.ExpandedDimensionProp implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension androidx.wear.protolayout.DimensionBuilders.ImageDimension {
+  }
+
+  public static final class DimensionBuilders.ExpandedDimensionProp.Builder implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension.Builder androidx.wear.protolayout.DimensionBuilders.ImageDimension.Builder {
+    ctor public DimensionBuilders.ExpandedDimensionProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp build();
+  }
+
+  public static interface DimensionBuilders.ImageDimension {
+  }
+
+  public static interface DimensionBuilders.ImageDimension.Builder {
+    method public androidx.wear.protolayout.DimensionBuilders.ImageDimension build();
+  }
+
+  public static final class DimensionBuilders.ProportionalDimensionProp implements androidx.wear.protolayout.DimensionBuilders.ImageDimension {
+    method @IntRange(from=0) public int getAspectRatioHeight();
+    method @IntRange(from=0) public int getAspectRatioWidth();
+  }
+
+  public static final class DimensionBuilders.ProportionalDimensionProp.Builder implements androidx.wear.protolayout.DimensionBuilders.ImageDimension.Builder {
+    ctor public DimensionBuilders.ProportionalDimensionProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.ProportionalDimensionProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.ProportionalDimensionProp.Builder setAspectRatioHeight(@IntRange(from=0) int);
+    method public androidx.wear.protolayout.DimensionBuilders.ProportionalDimensionProp.Builder setAspectRatioWidth(@IntRange(from=0) int);
+  }
+
+  public static final class DimensionBuilders.SpProp {
+    method @Dimension(unit=androidx.annotation.Dimension.SP) public float getValue();
+  }
+
+  public static final class DimensionBuilders.SpProp.Builder {
+    ctor public DimensionBuilders.SpProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.SpProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.SpProp.Builder setValue(@Dimension(unit=androidx.annotation.Dimension.SP) float);
+  }
+
+  public static interface DimensionBuilders.SpacerDimension {
+  }
+
+  public static interface DimensionBuilders.SpacerDimension.Builder {
+    method public androidx.wear.protolayout.DimensionBuilders.SpacerDimension build();
+  }
+
+  public static final class DimensionBuilders.WrappedDimensionProp implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension {
+  }
+
+  public static final class DimensionBuilders.WrappedDimensionProp.Builder implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension.Builder {
+    ctor public DimensionBuilders.WrappedDimensionProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp build();
+  }
+
+  public final class LayoutElementBuilders {
+    field public static final int ARC_ANCHOR_CENTER = 2; // 0x2
+    field public static final int ARC_ANCHOR_END = 3; // 0x3
+    field public static final int ARC_ANCHOR_START = 1; // 0x1
+    field public static final int ARC_ANCHOR_UNDEFINED = 0; // 0x0
+    field public static final int CONTENT_SCALE_MODE_CROP = 2; // 0x2
+    field public static final int CONTENT_SCALE_MODE_FILL_BOUNDS = 3; // 0x3
+    field public static final int CONTENT_SCALE_MODE_FIT = 1; // 0x1
+    field public static final int CONTENT_SCALE_MODE_UNDEFINED = 0; // 0x0
+    field public static final int FONT_VARIANT_BODY = 2; // 0x2
+    field public static final int FONT_VARIANT_TITLE = 1; // 0x1
+    field public static final int FONT_VARIANT_UNDEFINED = 0; // 0x0
+    field public static final int FONT_WEIGHT_BOLD = 700; // 0x2bc
+    field public static final int FONT_WEIGHT_NORMAL = 400; // 0x190
+    field public static final int FONT_WEIGHT_UNDEFINED = 0; // 0x0
+    field public static final int HORIZONTAL_ALIGN_CENTER = 2; // 0x2
+    field public static final int HORIZONTAL_ALIGN_END = 5; // 0x5
+    field public static final int HORIZONTAL_ALIGN_LEFT = 1; // 0x1
+    field public static final int HORIZONTAL_ALIGN_RIGHT = 3; // 0x3
+    field public static final int HORIZONTAL_ALIGN_START = 4; // 0x4
+    field public static final int HORIZONTAL_ALIGN_UNDEFINED = 0; // 0x0
+    field public static final int SPAN_VERTICAL_ALIGN_BOTTOM = 1; // 0x1
+    field public static final int SPAN_VERTICAL_ALIGN_TEXT_BASELINE = 2; // 0x2
+    field public static final int SPAN_VERTICAL_ALIGN_UNDEFINED = 0; // 0x0
+    field public static final int TEXT_ALIGN_CENTER = 2; // 0x2
+    field public static final int TEXT_ALIGN_END = 3; // 0x3
+    field public static final int TEXT_ALIGN_START = 1; // 0x1
+    field public static final int TEXT_ALIGN_UNDEFINED = 0; // 0x0
+    field public static final int TEXT_OVERFLOW_ELLIPSIZE_END = 2; // 0x2
+    field public static final int TEXT_OVERFLOW_TRUNCATE = 1; // 0x1
+    field public static final int TEXT_OVERFLOW_UNDEFINED = 0; // 0x0
+    field public static final int VERTICAL_ALIGN_BOTTOM = 3; // 0x3
+    field public static final int VERTICAL_ALIGN_CENTER = 2; // 0x2
+    field public static final int VERTICAL_ALIGN_TOP = 1; // 0x1
+    field public static final int VERTICAL_ALIGN_UNDEFINED = 0; // 0x0
+  }
+
+  public static final class LayoutElementBuilders.Arc implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public androidx.wear.protolayout.DimensionBuilders.DegreesProp? getAnchorAngle();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAnchorTypeProp? getAnchorType();
+    method public java.util.List<androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement!> getContents();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp? getVerticalAlign();
+  }
+
+  public static final class LayoutElementBuilders.Arc.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Arc.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder addContent(androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder setAnchorAngle(androidx.wear.protolayout.DimensionBuilders.DegreesProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder setAnchorType(androidx.wear.protolayout.LayoutElementBuilders.ArcAnchorTypeProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder setAnchorType(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder setVerticalAlign(androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Arc.Builder setVerticalAlign(int);
+  }
+
+  public static final class LayoutElementBuilders.ArcAdapter implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement {
+    method public androidx.wear.protolayout.LayoutElementBuilders.LayoutElement? getContent();
+    method public androidx.wear.protolayout.TypeBuilders.BoolProp? getRotateContents();
+  }
+
+  public static final class LayoutElementBuilders.ArcAdapter.Builder implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement.Builder {
+    ctor public LayoutElementBuilders.ArcAdapter.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAdapter build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAdapter.Builder setContent(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAdapter.Builder setRotateContents(androidx.wear.protolayout.TypeBuilders.BoolProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAdapter.Builder setRotateContents(boolean);
+  }
+
+  public static final class LayoutElementBuilders.ArcAnchorTypeProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.ArcAnchorTypeProp.Builder {
+    ctor public LayoutElementBuilders.ArcAnchorTypeProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAnchorTypeProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcAnchorTypeProp.Builder setValue(int);
+  }
+
+  public static interface LayoutElementBuilders.ArcLayoutElement {
+  }
+
+  public static interface LayoutElementBuilders.ArcLayoutElement.Builder {
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement build();
+  }
+
+  public static final class LayoutElementBuilders.ArcLine implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement {
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp? getColor();
+    method public androidx.wear.protolayout.DimensionBuilders.DegreesProp? getLength();
+    method public androidx.wear.protolayout.ModifiersBuilders.ArcModifiers? getModifiers();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getThickness();
+  }
+
+  public static final class LayoutElementBuilders.ArcLine.Builder implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement.Builder {
+    ctor public LayoutElementBuilders.ArcLine.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcLine build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcLine.Builder setColor(androidx.wear.protolayout.ColorBuilders.ColorProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcLine.Builder setLength(androidx.wear.protolayout.DimensionBuilders.DegreesProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcLine.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.ArcModifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcLine.Builder setThickness(androidx.wear.protolayout.DimensionBuilders.DpProp);
+  }
+
+  public static final class LayoutElementBuilders.ArcSpacer implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement {
+    method public androidx.wear.protolayout.DimensionBuilders.DegreesProp? getLength();
+    method public androidx.wear.protolayout.ModifiersBuilders.ArcModifiers? getModifiers();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getThickness();
+  }
+
+  public static final class LayoutElementBuilders.ArcSpacer.Builder implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement.Builder {
+    ctor public LayoutElementBuilders.ArcSpacer.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcSpacer build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcSpacer.Builder setLength(androidx.wear.protolayout.DimensionBuilders.DegreesProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcSpacer.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.ArcModifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcSpacer.Builder setThickness(androidx.wear.protolayout.DimensionBuilders.DpProp);
+  }
+
+  public static final class LayoutElementBuilders.ArcText implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement {
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle? getFontStyle();
+    method public androidx.wear.protolayout.ModifiersBuilders.ArcModifiers? getModifiers();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getText();
+  }
+
+  public static final class LayoutElementBuilders.ArcText.Builder implements androidx.wear.protolayout.LayoutElementBuilders.ArcLayoutElement.Builder {
+    ctor public LayoutElementBuilders.ArcText.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcText build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcText.Builder setFontStyle(androidx.wear.protolayout.LayoutElementBuilders.FontStyle);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcText.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.ArcModifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcText.Builder setText(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.ArcText.Builder setText(String);
+  }
+
+  public static final class LayoutElementBuilders.Box implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public java.util.List<androidx.wear.protolayout.LayoutElementBuilders.LayoutElement!> getContents();
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension? getHeight();
+    method public androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp? getHorizontalAlignment();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp? getVerticalAlignment();
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension? getWidth();
+  }
+
+  public static final class LayoutElementBuilders.Box.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Box.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder addContent(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setHorizontalAlignment(androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setHorizontalAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setVerticalAlignment(androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setVerticalAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Box.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
+  }
+
+  public static final class LayoutElementBuilders.ColorFilter {
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp? getTint();
+  }
+
+  public static final class LayoutElementBuilders.ColorFilter.Builder {
+    ctor public LayoutElementBuilders.ColorFilter.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ColorFilter build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ColorFilter.Builder setTint(androidx.wear.protolayout.ColorBuilders.ColorProp);
+  }
+
+  public static final class LayoutElementBuilders.Column implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public java.util.List<androidx.wear.protolayout.LayoutElementBuilders.LayoutElement!> getContents();
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension? getHeight();
+    method public androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp? getHorizontalAlignment();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension? getWidth();
+  }
+
+  public static final class LayoutElementBuilders.Column.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Column.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column.Builder addContent(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column.Builder setHorizontalAlignment(androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column.Builder setHorizontalAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Column.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
+  }
+
+  public static final class LayoutElementBuilders.ContentScaleModeProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.ContentScaleModeProp.Builder {
+    ctor public LayoutElementBuilders.ContentScaleModeProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ContentScaleModeProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ContentScaleModeProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.FontStyle {
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp? getColor();
+    method public androidx.wear.protolayout.TypeBuilders.BoolProp? getItalic();
+    method public androidx.wear.protolayout.DimensionBuilders.EmProp? getLetterSpacing();
+    method public androidx.wear.protolayout.DimensionBuilders.SpProp? getSize();
+    method public androidx.wear.protolayout.TypeBuilders.BoolProp? getUnderline();
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontWeightProp? getWeight();
+  }
+
+  public static final class LayoutElementBuilders.FontStyle.Builder {
+    ctor public LayoutElementBuilders.FontStyle.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setColor(androidx.wear.protolayout.ColorBuilders.ColorProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setItalic(androidx.wear.protolayout.TypeBuilders.BoolProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setItalic(boolean);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setLetterSpacing(androidx.wear.protolayout.DimensionBuilders.EmProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setSize(androidx.wear.protolayout.DimensionBuilders.SpProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setUnderline(androidx.wear.protolayout.TypeBuilders.BoolProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setUnderline(boolean);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setWeight(androidx.wear.protolayout.LayoutElementBuilders.FontWeightProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder setWeight(int);
+  }
+
+  public static class LayoutElementBuilders.FontStyles {
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder body1(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder body2(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder button(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder caption1(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder caption2(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder display1(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder display2(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder display3(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder title1(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder title2(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle.Builder title3(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+  }
+
+  public static final class LayoutElementBuilders.FontWeightProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.FontWeightProp.Builder {
+    ctor public LayoutElementBuilders.FontWeightProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontWeightProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontWeightProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.HorizontalAlignmentProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.HorizontalAlignmentProp.Builder {
+    ctor public LayoutElementBuilders.HorizontalAlignmentProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.Image implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public androidx.wear.protolayout.LayoutElementBuilders.ColorFilter? getColorFilter();
+    method public androidx.wear.protolayout.LayoutElementBuilders.ContentScaleModeProp? getContentScaleMode();
+    method public androidx.wear.protolayout.DimensionBuilders.ImageDimension? getHeight();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getResourceId();
+    method public androidx.wear.protolayout.DimensionBuilders.ImageDimension? getWidth();
+  }
+
+  public static final class LayoutElementBuilders.Image.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Image.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setColorFilter(androidx.wear.protolayout.LayoutElementBuilders.ColorFilter);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setContentScaleMode(androidx.wear.protolayout.LayoutElementBuilders.ContentScaleModeProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setContentScaleMode(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.ImageDimension);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setResourceId(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setResourceId(String);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Image.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.ImageDimension);
+  }
+
+  public static final class LayoutElementBuilders.Layout {
+    method public static androidx.wear.protolayout.LayoutElementBuilders.Layout fromLayoutElement(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.LayoutElement? getRoot();
+  }
+
+  public static final class LayoutElementBuilders.Layout.Builder {
+    ctor public LayoutElementBuilders.Layout.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Layout build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Layout.Builder setRoot(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+  }
+
+  public static interface LayoutElementBuilders.LayoutElement {
+  }
+
+  public static interface LayoutElementBuilders.LayoutElement.Builder {
+    method public androidx.wear.protolayout.LayoutElementBuilders.LayoutElement build();
+  }
+
+  public static final class LayoutElementBuilders.Row implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public java.util.List<androidx.wear.protolayout.LayoutElementBuilders.LayoutElement!> getContents();
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension? getHeight();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp? getVerticalAlignment();
+    method public androidx.wear.protolayout.DimensionBuilders.ContainerDimension? getWidth();
+  }
+
+  public static final class LayoutElementBuilders.Row.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Row.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row.Builder addContent(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row.Builder setVerticalAlignment(androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row.Builder setVerticalAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Row.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
+  }
+
+  public static final class LayoutElementBuilders.Spacer implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public androidx.wear.protolayout.DimensionBuilders.SpacerDimension? getHeight();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.DimensionBuilders.SpacerDimension? getWidth();
+  }
+
+  public static final class LayoutElementBuilders.Spacer.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Spacer.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spacer build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spacer.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.SpacerDimension);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spacer.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spacer.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.SpacerDimension);
+  }
+
+  public static interface LayoutElementBuilders.Span {
+  }
+
+  public static interface LayoutElementBuilders.Span.Builder {
+    method public androidx.wear.protolayout.LayoutElementBuilders.Span build();
+  }
+
+  public static final class LayoutElementBuilders.SpanImage implements androidx.wear.protolayout.LayoutElementBuilders.Span {
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanVerticalAlignmentProp? getAlignment();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getHeight();
+    method public androidx.wear.protolayout.ModifiersBuilders.SpanModifiers? getModifiers();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getResourceId();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getWidth();
+  }
+
+  public static final class LayoutElementBuilders.SpanImage.Builder implements androidx.wear.protolayout.LayoutElementBuilders.Span.Builder {
+    ctor public LayoutElementBuilders.SpanImage.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setAlignment(androidx.wear.protolayout.LayoutElementBuilders.SpanVerticalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.DpProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.SpanModifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setResourceId(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setResourceId(String);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanImage.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.DpProp);
+  }
+
+  public static final class LayoutElementBuilders.SpanText implements androidx.wear.protolayout.LayoutElementBuilders.Span {
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle? getFontStyle();
+    method public androidx.wear.protolayout.ModifiersBuilders.SpanModifiers? getModifiers();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getText();
+  }
+
+  public static final class LayoutElementBuilders.SpanText.Builder implements androidx.wear.protolayout.LayoutElementBuilders.Span.Builder {
+    ctor public LayoutElementBuilders.SpanText.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanText build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanText.Builder setFontStyle(androidx.wear.protolayout.LayoutElementBuilders.FontStyle);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanText.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.SpanModifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanText.Builder setText(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanText.Builder setText(String);
+  }
+
+  public static final class LayoutElementBuilders.SpanVerticalAlignmentProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.SpanVerticalAlignmentProp.Builder {
+    ctor public LayoutElementBuilders.SpanVerticalAlignmentProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanVerticalAlignmentProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.SpanVerticalAlignmentProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.Spannable implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public androidx.wear.protolayout.DimensionBuilders.SpProp? getLineHeight();
+    method public androidx.wear.protolayout.TypeBuilders.Int32Prop? getMaxLines();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp? getMultilineAlignment();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextOverflowProp? getOverflow();
+    method public java.util.List<androidx.wear.protolayout.LayoutElementBuilders.Span!> getSpans();
+  }
+
+  public static final class LayoutElementBuilders.Spannable.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Spannable.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder addSpan(androidx.wear.protolayout.LayoutElementBuilders.Span);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setLineHeight(androidx.wear.protolayout.DimensionBuilders.SpProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setMaxLines(androidx.wear.protolayout.TypeBuilders.Int32Prop);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setMaxLines(@IntRange(from=1) int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setMultilineAlignment(androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setMultilineAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setOverflow(androidx.wear.protolayout.LayoutElementBuilders.TextOverflowProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setOverflow(int);
+  }
+
+  public static final class LayoutElementBuilders.Text implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+    method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle? getFontStyle();
+    method public androidx.wear.protolayout.DimensionBuilders.SpProp? getLineHeight();
+    method public androidx.wear.protolayout.TypeBuilders.Int32Prop? getMaxLines();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextAlignmentProp? getMultilineAlignment();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextOverflowProp? getOverflow();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getText();
+  }
+
+  public static final class LayoutElementBuilders.Text.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+    ctor public LayoutElementBuilders.Text.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setFontStyle(androidx.wear.protolayout.LayoutElementBuilders.FontStyle);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setLineHeight(androidx.wear.protolayout.DimensionBuilders.SpProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setMaxLines(androidx.wear.protolayout.TypeBuilders.Int32Prop);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setMaxLines(@IntRange(from=1) int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setMultilineAlignment(androidx.wear.protolayout.LayoutElementBuilders.TextAlignmentProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setMultilineAlignment(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setOverflow(androidx.wear.protolayout.LayoutElementBuilders.TextOverflowProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setOverflow(int);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setText(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setText(String);
+  }
+
+  public static final class LayoutElementBuilders.TextAlignmentProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.TextAlignmentProp.Builder {
+    ctor public LayoutElementBuilders.TextAlignmentProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextAlignmentProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextAlignmentProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.TextOverflowProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.TextOverflowProp.Builder {
+    ctor public LayoutElementBuilders.TextOverflowProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextOverflowProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.TextOverflowProp.Builder setValue(int);
+  }
+
+  public static final class LayoutElementBuilders.VerticalAlignmentProp {
+    method public int getValue();
+  }
+
+  public static final class LayoutElementBuilders.VerticalAlignmentProp.Builder {
+    ctor public LayoutElementBuilders.VerticalAlignmentProp.Builder();
+    method public androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp build();
+    method public androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignmentProp.Builder setValue(int);
+  }
+
+  public final class ModifiersBuilders {
+  }
+
+  public static final class ModifiersBuilders.ArcModifiers {
+    method public androidx.wear.protolayout.ModifiersBuilders.Clickable? getClickable();
+    method public androidx.wear.protolayout.ModifiersBuilders.Semantics? getSemantics();
+  }
+
+  public static final class ModifiersBuilders.ArcModifiers.Builder {
+    ctor public ModifiersBuilders.ArcModifiers.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.ArcModifiers build();
+    method public androidx.wear.protolayout.ModifiersBuilders.ArcModifiers.Builder setClickable(androidx.wear.protolayout.ModifiersBuilders.Clickable);
+    method public androidx.wear.protolayout.ModifiersBuilders.ArcModifiers.Builder setSemantics(androidx.wear.protolayout.ModifiersBuilders.Semantics);
+  }
+
+  public static final class ModifiersBuilders.Background {
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp? getColor();
+    method public androidx.wear.protolayout.ModifiersBuilders.Corner? getCorner();
+  }
+
+  public static final class ModifiersBuilders.Background.Builder {
+    ctor public ModifiersBuilders.Background.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Background build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Background.Builder setColor(androidx.wear.protolayout.ColorBuilders.ColorProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Background.Builder setCorner(androidx.wear.protolayout.ModifiersBuilders.Corner);
+  }
+
+  public static final class ModifiersBuilders.Border {
+    method public androidx.wear.protolayout.ColorBuilders.ColorProp? getColor();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getWidth();
+  }
+
+  public static final class ModifiersBuilders.Border.Builder {
+    ctor public ModifiersBuilders.Border.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Border build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Border.Builder setColor(androidx.wear.protolayout.ColorBuilders.ColorProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Border.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.DpProp);
+  }
+
+  public static final class ModifiersBuilders.Clickable {
+    method public String getId();
+    method public androidx.wear.protolayout.ActionBuilders.Action? getOnClick();
+  }
+
+  public static final class ModifiersBuilders.Clickable.Builder {
+    ctor public ModifiersBuilders.Clickable.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Clickable build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Clickable.Builder setId(String);
+    method public androidx.wear.protolayout.ModifiersBuilders.Clickable.Builder setOnClick(androidx.wear.protolayout.ActionBuilders.Action);
+  }
+
+  public static final class ModifiersBuilders.Corner {
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getRadius();
+  }
+
+  public static final class ModifiersBuilders.Corner.Builder {
+    ctor public ModifiersBuilders.Corner.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Corner build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Corner.Builder setRadius(androidx.wear.protolayout.DimensionBuilders.DpProp);
+  }
+
+  public static final class ModifiersBuilders.ElementMetadata {
+    method public byte[] getTagData();
+  }
+
+  public static final class ModifiersBuilders.ElementMetadata.Builder {
+    ctor public ModifiersBuilders.ElementMetadata.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.ElementMetadata build();
+    method public androidx.wear.protolayout.ModifiersBuilders.ElementMetadata.Builder setTagData(byte[]);
+  }
+
+  public static final class ModifiersBuilders.Modifiers {
+    method public androidx.wear.protolayout.ModifiersBuilders.Background? getBackground();
+    method public androidx.wear.protolayout.ModifiersBuilders.Border? getBorder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Clickable? getClickable();
+    method public androidx.wear.protolayout.ModifiersBuilders.ElementMetadata? getMetadata();
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding? getPadding();
+    method public androidx.wear.protolayout.ModifiersBuilders.Semantics? getSemantics();
+  }
+
+  public static final class ModifiersBuilders.Modifiers.Builder {
+    ctor public ModifiersBuilders.Modifiers.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers.Builder setBackground(androidx.wear.protolayout.ModifiersBuilders.Background);
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers.Builder setBorder(androidx.wear.protolayout.ModifiersBuilders.Border);
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers.Builder setClickable(androidx.wear.protolayout.ModifiersBuilders.Clickable);
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers.Builder setMetadata(androidx.wear.protolayout.ModifiersBuilders.ElementMetadata);
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers.Builder setPadding(androidx.wear.protolayout.ModifiersBuilders.Padding);
+    method public androidx.wear.protolayout.ModifiersBuilders.Modifiers.Builder setSemantics(androidx.wear.protolayout.ModifiersBuilders.Semantics);
+  }
+
+  public static final class ModifiersBuilders.Padding {
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getBottom();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getEnd();
+    method public androidx.wear.protolayout.TypeBuilders.BoolProp? getRtlAware();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getStart();
+    method public androidx.wear.protolayout.DimensionBuilders.DpProp? getTop();
+  }
+
+  public static final class ModifiersBuilders.Padding.Builder {
+    ctor public ModifiersBuilders.Padding.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setAll(androidx.wear.protolayout.DimensionBuilders.DpProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setBottom(androidx.wear.protolayout.DimensionBuilders.DpProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setEnd(androidx.wear.protolayout.DimensionBuilders.DpProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setRtlAware(androidx.wear.protolayout.TypeBuilders.BoolProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setRtlAware(boolean);
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setStart(androidx.wear.protolayout.DimensionBuilders.DpProp);
+    method public androidx.wear.protolayout.ModifiersBuilders.Padding.Builder setTop(androidx.wear.protolayout.DimensionBuilders.DpProp);
+  }
+
+  public static final class ModifiersBuilders.Semantics {
+    method public String getContentDescription();
+  }
+
+  public static final class ModifiersBuilders.Semantics.Builder {
+    ctor public ModifiersBuilders.Semantics.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.Semantics build();
+    method public androidx.wear.protolayout.ModifiersBuilders.Semantics.Builder setContentDescription(String);
+  }
+
+  public static final class ModifiersBuilders.SpanModifiers {
+    method public androidx.wear.protolayout.ModifiersBuilders.Clickable? getClickable();
+  }
+
+  public static final class ModifiersBuilders.SpanModifiers.Builder {
+    ctor public ModifiersBuilders.SpanModifiers.Builder();
+    method public androidx.wear.protolayout.ModifiersBuilders.SpanModifiers build();
+    method public androidx.wear.protolayout.ModifiersBuilders.SpanModifiers.Builder setClickable(androidx.wear.protolayout.ModifiersBuilders.Clickable);
+  }
+
+  public final class ResourceBuilders {
+    field public static final int IMAGE_FORMAT_RGB_565 = 1; // 0x1
+    field public static final int IMAGE_FORMAT_UNDEFINED = 0; // 0x0
+  }
+
+  public static final class ResourceBuilders.AndroidImageResourceByResId {
+    method @DrawableRes public int getResourceId();
+  }
+
+  public static final class ResourceBuilders.AndroidImageResourceByResId.Builder {
+    ctor public ResourceBuilders.AndroidImageResourceByResId.Builder();
+    method public androidx.wear.protolayout.ResourceBuilders.AndroidImageResourceByResId build();
+    method public androidx.wear.protolayout.ResourceBuilders.AndroidImageResourceByResId.Builder setResourceId(@DrawableRes int);
+  }
+
+  public static final class ResourceBuilders.ImageResource {
+    method public androidx.wear.protolayout.ResourceBuilders.AndroidImageResourceByResId? getAndroidResourceByResId();
+    method public androidx.wear.protolayout.ResourceBuilders.InlineImageResource? getInlineResource();
+  }
+
+  public static final class ResourceBuilders.ImageResource.Builder {
+    ctor public ResourceBuilders.ImageResource.Builder();
+    method public androidx.wear.protolayout.ResourceBuilders.ImageResource build();
+    method public androidx.wear.protolayout.ResourceBuilders.ImageResource.Builder setAndroidResourceByResId(androidx.wear.protolayout.ResourceBuilders.AndroidImageResourceByResId);
+    method public androidx.wear.protolayout.ResourceBuilders.ImageResource.Builder setInlineResource(androidx.wear.protolayout.ResourceBuilders.InlineImageResource);
+  }
+
+  public static final class ResourceBuilders.InlineImageResource {
+    method public byte[] getData();
+    method public int getFormat();
+    method @Dimension(unit=androidx.annotation.Dimension.PX) public int getHeightPx();
+    method @Dimension(unit=androidx.annotation.Dimension.PX) public int getWidthPx();
+  }
+
+  public static final class ResourceBuilders.InlineImageResource.Builder {
+    ctor public ResourceBuilders.InlineImageResource.Builder();
+    method public androidx.wear.protolayout.ResourceBuilders.InlineImageResource build();
+    method public androidx.wear.protolayout.ResourceBuilders.InlineImageResource.Builder setData(byte[]);
+    method public androidx.wear.protolayout.ResourceBuilders.InlineImageResource.Builder setFormat(int);
+    method public androidx.wear.protolayout.ResourceBuilders.InlineImageResource.Builder setHeightPx(@Dimension(unit=androidx.annotation.Dimension.PX) int);
+    method public androidx.wear.protolayout.ResourceBuilders.InlineImageResource.Builder setWidthPx(@Dimension(unit=androidx.annotation.Dimension.PX) int);
+  }
+
+  public static final class ResourceBuilders.Resources {
+    method public java.util.Map<java.lang.String!,androidx.wear.protolayout.ResourceBuilders.ImageResource!> getIdToImageMapping();
+    method public String getVersion();
+  }
+
+  public static final class ResourceBuilders.Resources.Builder {
+    ctor public ResourceBuilders.Resources.Builder();
+    method public androidx.wear.protolayout.ResourceBuilders.Resources.Builder addIdToImageMapping(String, androidx.wear.protolayout.ResourceBuilders.ImageResource);
+    method public androidx.wear.protolayout.ResourceBuilders.Resources build();
+    method public androidx.wear.protolayout.ResourceBuilders.Resources.Builder setVersion(String);
+  }
+
+  public final class StateBuilders {
+  }
+
+  public static final class StateBuilders.State {
+    method public java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!> getIdToValueMapping();
+    method public String getLastClickableId();
+  }
+
+  public static final class StateBuilders.State.Builder {
+    ctor public StateBuilders.State.Builder();
+    method public androidx.wear.protolayout.StateBuilders.State.Builder addIdToValueMapping(String, androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue);
+    method public androidx.wear.protolayout.StateBuilders.State build();
+  }
+
+  public final class TimelineBuilders {
+  }
+
+  public static final class TimelineBuilders.TimeInterval {
+    method public long getEndMillis();
+    method public long getStartMillis();
+  }
+
+  public static final class TimelineBuilders.TimeInterval.Builder {
+    ctor public TimelineBuilders.TimeInterval.Builder();
+    method public androidx.wear.protolayout.TimelineBuilders.TimeInterval build();
+    method public androidx.wear.protolayout.TimelineBuilders.TimeInterval.Builder setEndMillis(long);
+    method public androidx.wear.protolayout.TimelineBuilders.TimeInterval.Builder setStartMillis(long);
+  }
+
+  public static final class TimelineBuilders.Timeline {
+    method public static androidx.wear.protolayout.TimelineBuilders.Timeline fromLayoutElement(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public java.util.List<androidx.wear.protolayout.TimelineBuilders.TimelineEntry!> getTimelineEntries();
+  }
+
+  public static final class TimelineBuilders.Timeline.Builder {
+    ctor public TimelineBuilders.Timeline.Builder();
+    method public androidx.wear.protolayout.TimelineBuilders.Timeline.Builder addTimelineEntry(androidx.wear.protolayout.TimelineBuilders.TimelineEntry);
+    method public androidx.wear.protolayout.TimelineBuilders.Timeline build();
+  }
+
+  public static final class TimelineBuilders.TimelineEntry {
+    method public static androidx.wear.protolayout.TimelineBuilders.TimelineEntry fromLayoutElement(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
+    method public androidx.wear.protolayout.LayoutElementBuilders.Layout? getLayout();
+    method public androidx.wear.protolayout.TimelineBuilders.TimeInterval? getValidity();
+  }
+
+  public static final class TimelineBuilders.TimelineEntry.Builder {
+    ctor public TimelineBuilders.TimelineEntry.Builder();
+    method public androidx.wear.protolayout.TimelineBuilders.TimelineEntry build();
+    method public androidx.wear.protolayout.TimelineBuilders.TimelineEntry.Builder setLayout(androidx.wear.protolayout.LayoutElementBuilders.Layout);
+    method public androidx.wear.protolayout.TimelineBuilders.TimelineEntry.Builder setValidity(androidx.wear.protolayout.TimelineBuilders.TimeInterval);
+  }
+
+  public final class TypeBuilders {
+  }
+
+  public static final class TypeBuilders.BoolProp {
+    method public boolean getValue();
+  }
+
+  public static final class TypeBuilders.BoolProp.Builder {
+    ctor public TypeBuilders.BoolProp.Builder();
+    method public androidx.wear.protolayout.TypeBuilders.BoolProp build();
+    method public androidx.wear.protolayout.TypeBuilders.BoolProp.Builder setValue(boolean);
+  }
+
+  public static final class TypeBuilders.FloatProp {
+    method public float getValue();
+  }
+
+  public static final class TypeBuilders.FloatProp.Builder {
+    ctor public TypeBuilders.FloatProp.Builder();
+    method public androidx.wear.protolayout.TypeBuilders.FloatProp build();
+    method public androidx.wear.protolayout.TypeBuilders.FloatProp.Builder setValue(float);
+  }
+
+  public static final class TypeBuilders.Int32Prop {
+    method public int getValue();
+  }
+
+  public static final class TypeBuilders.Int32Prop.Builder {
+    ctor public TypeBuilders.Int32Prop.Builder();
+    method public androidx.wear.protolayout.TypeBuilders.Int32Prop build();
+    method public androidx.wear.protolayout.TypeBuilders.Int32Prop.Builder setValue(int);
+  }
+
+  public static final class TypeBuilders.StringProp {
+    method public String getValue();
+  }
+
+  public static final class TypeBuilders.StringProp.Builder {
+    ctor public TypeBuilders.StringProp.Builder();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp build();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp.Builder setValue(String);
+  }
+
+}
+
diff --git a/wear/protolayout/protolayout/build.gradle b/wear/protolayout/protolayout/build.gradle
index 8332043..e622d23 100644
--- a/wear/protolayout/protolayout/build.gradle
+++ b/wear/protolayout/protolayout/build.gradle
@@ -23,10 +23,29 @@
 
 dependencies {
     annotationProcessor(libs.nullaway)
+    api("androidx.annotation:annotation:1.2.0")
+
+    implementation("androidx.annotation:annotation-experimental:1.2.0")
+    implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
+    implementation(project(":wear:protolayout:protolayout-expression"))
+
+    compileOnly(libs.kotlinStdlib) // For annotation-experimental
+
+    testImplementation(libs.testExtJunit)
+    testImplementation(libs.testExtTruth)
+    testImplementation(libs.testRunner)
+    testImplementation(libs.robolectric)
 }
 
 android {
     namespace "androidx.wear.protolayout"
+
+    defaultConfig {
+        minSdkVersion 25
+    }
+    buildTypes.all {
+        consumerProguardFiles "proguard-rules.pro"
+    }
 }
 
 androidx {
diff --git a/wear/protolayout/protolayout/proguard-rules.pro b/wear/protolayout/protolayout/proguard-rules.pro
new file mode 100644
index 0000000..014faf2
--- /dev/null
+++ b/wear/protolayout/protolayout/proguard-rules.pro
@@ -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.
+
+# libproto uses reflection to deserialize a Proto, which Proguard can't accurately detect.
+# Keep all the class members of any generated messages to ensure we can deserialize properly inside
+# these classes.
+-keepclassmembers class * extends androidx.wear.protolayout.protobuf.GeneratedMessageLite {
+  <fields>;
+}
\ No newline at end of file
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ActionBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ActionBuilders.java
new file mode 100644
index 0000000..403e493
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ActionBuilders.java
@@ -0,0 +1,999 @@
+/*
+ * 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.wear.protolayout;
+
+import static androidx.wear.protolayout.expression.Preconditions.checkNotNull;
+
+import android.annotation.SuppressLint;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.protolayout.StateBuilders.State;
+import androidx.wear.protolayout.expression.Fingerprint;
+import androidx.wear.protolayout.expression.StateEntryBuilders;
+import androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue;
+import androidx.wear.protolayout.proto.ActionProto;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/** Builders for actions that can be performed when a user interacts with layout elements. */
+public final class ActionBuilders {
+  private ActionBuilders() {}
+
+  /** Shortcut for building an {@link AndroidStringExtra}. */
+  @NonNull
+  public static AndroidStringExtra stringExtra(@NonNull String value) {
+    return new AndroidStringExtra.Builder().setValue(value).build();
+  }
+
+  /** Shortcut for building an {@link AndroidIntExtra}. */
+  @NonNull
+  public static AndroidIntExtra intExtra(int value) {
+    return new AndroidIntExtra.Builder().setValue(value).build();
+  }
+
+  /** Shortcut for building an {@link AndroidLongExtra}. */
+  @NonNull
+  public static AndroidLongExtra longExtra(long value) {
+    return new AndroidLongExtra.Builder().setValue(value).build();
+  }
+
+  /** Shortcut for building an {@link AndroidDoubleExtra}. */
+  @NonNull
+  public static AndroidDoubleExtra doubleExtra(double value) {
+    return new AndroidDoubleExtra.Builder().setValue(value).build();
+  }
+
+  /** Shortcut for building an {@link AndroidBooleanExtra}. */
+  @NonNull
+  public static AndroidBooleanExtra booleanExtra(boolean value) {
+    return new AndroidBooleanExtra.Builder().setValue(value).build();
+  }
+
+  /**
+   * A string value that can be added to an Android intent's extras.
+   *
+   * @since 1.0
+   */
+  public static final class AndroidStringExtra implements AndroidExtra {
+    private final ActionProto.AndroidStringExtra mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    AndroidStringExtra(ActionProto.AndroidStringExtra impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the value.
+     *
+     * @since 1.0
+     */
+    @NonNull
+    public String getValue() {
+      return mImpl.getValue();
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static AndroidStringExtra fromProto(@NonNull ActionProto.AndroidStringExtra proto) {
+      return new AndroidStringExtra(proto, null);
+    }
+
+    @NonNull
+    ActionProto.AndroidStringExtra toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public ActionProto.AndroidExtra toAndroidExtraProto() {
+      return ActionProto.AndroidExtra.newBuilder().setStringVal(mImpl).build();
+    }
+
+    /** Builder for {@link AndroidStringExtra}. */
+    public static final class Builder implements AndroidExtra.Builder {
+      private final ActionProto.AndroidStringExtra.Builder mImpl =
+          ActionProto.AndroidStringExtra.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-973795259);
+
+      public Builder() {}
+
+      /**
+       * Sets the value.
+       *
+       * @since 1.0
+       */
+      @NonNull
+      public Builder setValue(@NonNull String value) {
+        mImpl.setValue(value);
+        mFingerprint.recordPropertyUpdate(1, value.hashCode());
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public AndroidStringExtra build() {
+        return new AndroidStringExtra(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * An integer value that can be added to an Android intent's extras.
+   *
+   * @since 1.0
+   */
+  public static final class AndroidIntExtra implements AndroidExtra {
+    private final ActionProto.AndroidIntExtra mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    AndroidIntExtra(ActionProto.AndroidIntExtra impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the value.
+     *
+     * @since 1.0
+     */
+    public int getValue() {
+      return mImpl.getValue();
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static AndroidIntExtra fromProto(@NonNull ActionProto.AndroidIntExtra proto) {
+      return new AndroidIntExtra(proto, null);
+    }
+
+    @NonNull
+    ActionProto.AndroidIntExtra toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public ActionProto.AndroidExtra toAndroidExtraProto() {
+      return ActionProto.AndroidExtra.newBuilder().setIntVal(mImpl).build();
+    }
+
+    /** Builder for {@link AndroidIntExtra}. */
+    public static final class Builder implements AndroidExtra.Builder {
+      private final ActionProto.AndroidIntExtra.Builder mImpl =
+          ActionProto.AndroidIntExtra.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-1199435881);
+
+      public Builder() {}
+
+      /**
+       * Sets the value.
+       *
+       * @since 1.0
+       */
+      @NonNull
+      public Builder setValue(int value) {
+        mImpl.setValue(value);
+        mFingerprint.recordPropertyUpdate(1, value);
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public AndroidIntExtra build() {
+        return new AndroidIntExtra(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * A long value that can be added to an Android intent's extras.
+   *
+   * @since 1.0
+   */
+  public static final class AndroidLongExtra implements AndroidExtra {
+    private final ActionProto.AndroidLongExtra mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    AndroidLongExtra(ActionProto.AndroidLongExtra impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the value.
+     *
+     * @since 1.0
+     */
+    public long getValue() {
+      return mImpl.getValue();
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static AndroidLongExtra fromProto(@NonNull ActionProto.AndroidLongExtra proto) {
+      return new AndroidLongExtra(proto, null);
+    }
+
+    @NonNull
+    ActionProto.AndroidLongExtra toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public ActionProto.AndroidExtra toAndroidExtraProto() {
+      return ActionProto.AndroidExtra.newBuilder().setLongVal(mImpl).build();
+    }
+
+    /** Builder for {@link AndroidLongExtra}. */
+    public static final class Builder implements AndroidExtra.Builder {
+      private final ActionProto.AndroidLongExtra.Builder mImpl =
+          ActionProto.AndroidLongExtra.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-906933303);
+
+      public Builder() {}
+
+      /**
+       * Sets the value.
+       *
+       * @since 1.0
+       */
+      @NonNull
+      public Builder setValue(long value) {
+        mImpl.setValue(value);
+        mFingerprint.recordPropertyUpdate(1, Long.hashCode(value));
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public AndroidLongExtra build() {
+        return new AndroidLongExtra(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * A double value that can be added to an Android intent's extras.
+   *
+   * @since 1.0
+   */
+  public static final class AndroidDoubleExtra implements AndroidExtra {
+    private final ActionProto.AndroidDoubleExtra mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    AndroidDoubleExtra(ActionProto.AndroidDoubleExtra impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the value.
+     *
+     * @since 1.0
+     */
+    public double getValue() {
+      return mImpl.getValue();
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static AndroidDoubleExtra fromProto(@NonNull ActionProto.AndroidDoubleExtra proto) {
+      return new AndroidDoubleExtra(proto, null);
+    }
+
+    @NonNull
+    ActionProto.AndroidDoubleExtra toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public ActionProto.AndroidExtra toAndroidExtraProto() {
+      return ActionProto.AndroidExtra.newBuilder().setDoubleVal(mImpl).build();
+    }
+
+    /** Builder for {@link AndroidDoubleExtra}. */
+    public static final class Builder implements AndroidExtra.Builder {
+      private final ActionProto.AndroidDoubleExtra.Builder mImpl =
+          ActionProto.AndroidDoubleExtra.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-1104636989);
+
+      public Builder() {}
+
+      /**
+       * Sets the value.
+       *
+       * @since 1.0
+       */
+      @NonNull
+      public Builder setValue(double value) {
+        mImpl.setValue(value);
+        mFingerprint.recordPropertyUpdate(1, Double.hashCode(value));
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public AndroidDoubleExtra build() {
+        return new AndroidDoubleExtra(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * A boolean value that can be added to an Android intent's extras.
+   *
+   * @since 1.0
+   */
+  public static final class AndroidBooleanExtra implements AndroidExtra {
+    private final ActionProto.AndroidBooleanExtra mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    AndroidBooleanExtra(ActionProto.AndroidBooleanExtra impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the value.
+     *
+     * @since 1.0
+     */
+    public boolean getValue() {
+      return mImpl.getValue();
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static AndroidBooleanExtra fromProto(@NonNull ActionProto.AndroidBooleanExtra proto) {
+      return new AndroidBooleanExtra(proto, null);
+    }
+
+    @NonNull
+    ActionProto.AndroidBooleanExtra toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public ActionProto.AndroidExtra toAndroidExtraProto() {
+      return ActionProto.AndroidExtra.newBuilder().setBooleanVal(mImpl).build();
+    }
+
+    /** Builder for {@link AndroidBooleanExtra}. */
+    public static final class Builder implements AndroidExtra.Builder {
+      private final ActionProto.AndroidBooleanExtra.Builder mImpl =
+          ActionProto.AndroidBooleanExtra.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-1244694745);
+
+      public Builder() {}
+
+      /**
+       * Sets the value.
+       *
+       * @since 1.0
+       */
+      @SuppressLint("MissingGetterMatchingBuilder")
+      @NonNull
+      public Builder setValue(boolean value) {
+        mImpl.setValue(value);
+        mFingerprint.recordPropertyUpdate(1, Boolean.hashCode(value));
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public AndroidBooleanExtra build() {
+        return new AndroidBooleanExtra(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * Interface defining an item that can be included in the extras of an intent that will be sent to
+   * an Android activity. Supports types in android.os.PersistableBundle, excluding arrays.
+   *
+   * @since 1.0
+   */
+  public interface AndroidExtra {
+    /**
+     * Get the protocol buffer representation of this object.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    ActionProto.AndroidExtra toAndroidExtraProto();
+
+    /**
+     * Get the fingerprint for this object or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    Fingerprint getFingerprint();
+
+    /** Builder to create {@link AndroidExtra} objects. */
+    @SuppressLint("StaticFinalBuilder")
+    interface Builder {
+
+      /** Builds an instance with values accumulated in this Builder. */
+      @NonNull
+      AndroidExtra build();
+    }
+  }
+
+  @NonNull
+  static AndroidExtra androidExtraFromProto(@NonNull ActionProto.AndroidExtra proto) {
+    if (proto.hasStringVal()) {
+      return AndroidStringExtra.fromProto(proto.getStringVal());
+    }
+    if (proto.hasIntVal()) {
+      return AndroidIntExtra.fromProto(proto.getIntVal());
+    }
+    if (proto.hasLongVal()) {
+      return AndroidLongExtra.fromProto(proto.getLongVal());
+    }
+    if (proto.hasDoubleVal()) {
+      return AndroidDoubleExtra.fromProto(proto.getDoubleVal());
+    }
+    if (proto.hasBooleanVal()) {
+      return AndroidBooleanExtra.fromProto(proto.getBooleanVal());
+    }
+    throw new IllegalStateException("Proto was not a recognised instance of AndroidExtra");
+  }
+
+  /**
+   * A launch action to send an intent to an Android activity.
+   *
+   * @since 1.0
+   */
+  public static final class AndroidActivity {
+    private final ActionProto.AndroidActivity mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    AndroidActivity(ActionProto.AndroidActivity impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the package name to send the intent to, for example, "com.example.weather".
+     *
+     * @since 1.0
+     */
+    @NonNull
+    public String getPackageName() {
+      return mImpl.getPackageName();
+    }
+
+    /**
+     * Gets the fully qualified class name (including the package) to send the intent to, for
+     * example, "com.example.weather.WeatherOverviewActivity".
+     *
+     * @since 1.0
+     */
+    @NonNull
+    public String getClassName() {
+      return mImpl.getClassName();
+    }
+
+    /**
+     * Gets the extras to be included in the intent.
+     *
+     * @since 1.0
+     */
+    @NonNull
+    public Map<String, AndroidExtra> getKeyToExtraMapping() {
+      Map<String, AndroidExtra> map = new HashMap<>();
+      for (Entry<String, ActionProto.AndroidExtra> entry : mImpl.getKeyToExtraMap().entrySet()) {
+        map.put(entry.getKey(), ActionBuilders.androidExtraFromProto(entry.getValue()));
+      }
+      return Collections.unmodifiableMap(map);
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static AndroidActivity fromProto(@NonNull ActionProto.AndroidActivity proto) {
+      return new AndroidActivity(proto, null);
+    }
+
+    @NonNull
+    ActionProto.AndroidActivity toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link AndroidActivity} */
+    public static final class Builder {
+      private final ActionProto.AndroidActivity.Builder mImpl =
+          ActionProto.AndroidActivity.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-1799520061);
+
+      public Builder() {}
+
+      /**
+       * Sets the package name to send the intent to, for example, "com.example.weather".
+       *
+       * @since 1.0
+       */
+      @NonNull
+      public Builder setPackageName(@NonNull String packageName) {
+        mImpl.setPackageName(packageName);
+        mFingerprint.recordPropertyUpdate(1, packageName.hashCode());
+        return this;
+      }
+
+      /**
+       * Sets the fully qualified class name (including the package) to send the intent to, for
+       * example, "com.example.weather.WeatherOverviewActivity".
+       *
+       * @since 1.0
+       */
+      @NonNull
+      public Builder setClassName(@NonNull String className) {
+        mImpl.setClassName(className);
+        mFingerprint.recordPropertyUpdate(2, className.hashCode());
+        return this;
+      }
+
+      /**
+       * Adds an entry into the extras to be included in the intent.
+       *
+       * @since 1.0
+       */
+      @SuppressLint("MissingGetterMatchingBuilder")
+      @NonNull
+      public Builder addKeyToExtraMapping(@NonNull String key, @NonNull AndroidExtra extra) {
+        mImpl.putKeyToExtra(key, extra.toAndroidExtraProto());
+        mFingerprint.recordPropertyUpdate(
+            key.hashCode(), checkNotNull(extra.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public AndroidActivity build() {
+        return new AndroidActivity(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * An action used to launch another activity on the system. This can hold multiple different
+   * underlying action types, which will be picked based on what the underlying runtime believes to
+   * be suitable.
+   *
+   * @since 1.0
+   */
+  public static final class LaunchAction implements Action {
+    private final ActionProto.LaunchAction mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    LaunchAction(ActionProto.LaunchAction impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets an action to launch an Android activity.
+     *
+     * @since 1.0
+     */
+    @Nullable
+    public AndroidActivity getAndroidActivity() {
+      if (mImpl.hasAndroidActivity()) {
+        return AndroidActivity.fromProto(mImpl.getAndroidActivity());
+      } else {
+        return null;
+      }
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static LaunchAction fromProto(@NonNull ActionProto.LaunchAction proto) {
+      return new LaunchAction(proto, null);
+    }
+
+    @NonNull
+    ActionProto.LaunchAction toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public ActionProto.Action toActionProto() {
+      return ActionProto.Action.newBuilder().setLaunchAction(mImpl).build();
+    }
+
+    /** Builder for {@link LaunchAction}. */
+    public static final class Builder implements Action.Builder {
+      private final ActionProto.LaunchAction.Builder mImpl = ActionProto.LaunchAction.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(2004803940);
+
+      public Builder() {}
+
+      /**
+       * Sets an action to launch an Android activity.
+       *
+       * @since 1.0
+       */
+      @NonNull
+      public Builder setAndroidActivity(@NonNull AndroidActivity androidActivity) {
+        mImpl.setAndroidActivity(androidActivity.toProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(androidActivity.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public LaunchAction build() {
+        return new LaunchAction(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * An action used to load (or reload) the layout contents.
+   *
+   * @since 1.0
+   */
+  public static final class LoadAction implements Action {
+    private final ActionProto.LoadAction mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    LoadAction(ActionProto.LoadAction impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the state to load the next layout with. This will be included in the layout request sent
+     * after this action is invoked by a {@link
+     * androidx.wear.protolayout.ModifiersBuilders.Clickable}.
+     *
+     * @since 1.0
+     */
+    @Nullable
+    public State getRequestState() {
+      if (mImpl.hasRequestState()) {
+        return State.fromProto(mImpl.getRequestState());
+      } else {
+        return null;
+      }
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static LoadAction fromProto(@NonNull ActionProto.LoadAction proto) {
+      return new LoadAction(proto, null);
+    }
+
+    @NonNull
+    ActionProto.LoadAction toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public ActionProto.Action toActionProto() {
+      return ActionProto.Action.newBuilder().setLoadAction(mImpl).build();
+    }
+
+    /** Builder for {@link LoadAction}. */
+    public static final class Builder implements Action.Builder {
+      private final ActionProto.LoadAction.Builder mImpl = ActionProto.LoadAction.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(674205536);
+
+      public Builder() {}
+
+      /**
+       * Sets the state to load the next layout with. This will be included in the layout request
+       * sent after this action is invoked by a {@link
+       * androidx.wear.protolayout.ModifiersBuilders.Clickable}.
+       *
+       * @since 1.0
+       */
+      @NonNull
+      public Builder setRequestState(@NonNull State requestState) {
+        mImpl.setRequestState(requestState.toProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(requestState.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public LoadAction build() {
+        return new LoadAction(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * Interface defining an action that can be used by a layout element.
+   *
+   * @since 1.0
+   */
+  public interface Action {
+    /**
+     * Get the protocol buffer representation of this object.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    ActionProto.Action toActionProto();
+
+    /**
+     * Get the fingerprint for this object or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    Fingerprint getFingerprint();
+
+    /** Builder to create {@link Action} objects. */
+    @SuppressLint("StaticFinalBuilder")
+    interface Builder {
+
+      /** Builds an instance with values accumulated in this Builder. */
+      @NonNull
+      Action build();
+    }
+  }
+
+  @NonNull
+  static Action actionFromProto(@NonNull ActionProto.Action proto) {
+    if (proto.hasLaunchAction()) {
+      return LaunchAction.fromProto(proto.getLaunchAction());
+    }
+    if (proto.hasLoadAction()) {
+      return LoadAction.fromProto(proto.getLoadAction());
+    }
+    throw new IllegalStateException("Proto was not a recognised instance of Action");
+  }
+
+  /**
+   * Interface defining an action that is handled internal to the current layout and won't cause a
+   * layout refresh.
+   *
+   * @since 1.2
+   */
+  public interface LocalAction {
+    /**
+     * Get the protocol buffer representation of this object.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    ActionProto.LocalAction toLocalActionProto();
+
+    /**
+     * Get the fingerprint for this object or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    Fingerprint getFingerprint();
+
+    /** Builder to create {@link LocalAction} objects. */
+    @SuppressLint("StaticFinalBuilder")
+    interface Builder {
+
+      /** Builds an instance with values accumulated in this Builder. */
+      @NonNull
+      LocalAction build();
+    }
+  }
+
+  @NonNull
+  static LocalAction localActionFromProto(@NonNull ActionProto.LocalAction proto) {
+    if (proto.hasSetState()) {
+      return SetStateAction.fromProto(proto.getSetState());
+    }
+    throw new IllegalStateException("Proto was not a recognised instance of LocalAction");
+  }
+
+  /**
+   * An action that sets a new value for a {@link androidx.wear.protolayout.StateBuilders.State}.
+   *
+   * @since 1.2
+   */
+  public static final class SetStateAction implements LocalAction {
+    private final ActionProto.SetStateAction mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    SetStateAction(ActionProto.SetStateAction impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the target key of the state item for this action.
+     *
+     * @since 1.2
+     */
+    @NonNull
+    public String getTargetKey() {
+      return mImpl.getTargetKey();
+    }
+
+    /**
+     * Gets the value to set the state item to, when this action is executed.
+     *
+     * @since 1.2
+     */
+    @Nullable
+    public StateEntryValue getValue() {
+      if (mImpl.hasValue()) {
+        return StateEntryBuilders.stateEntryValueFromProto(mImpl.getValue());
+      } else {
+        return null;
+      }
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static SetStateAction fromProto(@NonNull ActionProto.SetStateAction proto) {
+      return new SetStateAction(proto, null);
+    }
+
+    @NonNull
+    ActionProto.SetStateAction toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public ActionProto.LocalAction toLocalActionProto() {
+      return ActionProto.LocalAction.newBuilder().setSetState(mImpl).build();
+    }
+
+    /** Builder for {@link SetStateAction}. */
+    public static final class Builder implements LocalAction.Builder {
+      private final ActionProto.SetStateAction.Builder mImpl =
+          ActionProto.SetStateAction.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(2126563467);
+
+      public Builder() {}
+
+      /**
+       * Sets the target key of the state item for this action.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setTargetKey(@NonNull String targetKey) {
+        mImpl.setTargetKey(targetKey);
+        mFingerprint.recordPropertyUpdate(1, targetKey.hashCode());
+        return this;
+      }
+
+      /**
+       * Sets the value to set the state item to, when this action is executed.
+       *
+       * @since 1.2
+       */
+      @NonNull
+      public Builder setValue(@NonNull StateEntryValue value) {
+        mImpl.setValue(value.toStateEntryValueProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(value.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public SetStateAction build() {
+        return new SetStateAction(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ColorBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ColorBuilders.java
new file mode 100644
index 0000000..a53dde2
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ColorBuilders.java
@@ -0,0 +1,108 @@
+/*
+ * 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.wear.protolayout;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.protolayout.expression.Fingerprint;
+import androidx.wear.protolayout.proto.ColorProto;
+
+/** Builders for color utilities for layout elements. */
+public final class ColorBuilders {
+  private ColorBuilders() {}
+
+  /** Shortcut for building a {@link ColorProp} using an ARGB value. */
+  @NonNull
+  public static ColorProp argb(@ColorInt int colorArgb) {
+    return new ColorProp.Builder().setArgb(colorArgb).build();
+  }
+
+  /**
+   * A property defining a color.
+   *
+   * @since 1.0
+   */
+  public static final class ColorProp {
+    private final ColorProto.ColorProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    ColorProp(ColorProto.ColorProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the color value, in ARGB format.
+     *
+     * @since 1.0
+     */
+    @ColorInt
+    public int getArgb() {
+      return mImpl.getArgb();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static ColorProp fromProto(@NonNull ColorProto.ColorProp proto) {
+      return new ColorProp(proto, null);
+    }
+
+    @NonNull
+    ColorProto.ColorProp toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link ColorProp} */
+    public static final class Builder {
+      private final ColorProto.ColorProp.Builder mImpl = ColorProto.ColorProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-1955659823);
+
+      public Builder() {}
+
+      /**
+       * Sets the color value, in ARGB format.
+       *
+       * @since 1.0
+       */
+      @NonNull
+      public Builder setArgb(@ColorInt int argb) {
+        mImpl.setArgb(argb);
+        mFingerprint.recordPropertyUpdate(1, argb);
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public ColorProp build() {
+        return new ColorProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/DeviceParametersBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/DeviceParametersBuilders.java
new file mode 100644
index 0000000..b46c5a4
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/DeviceParametersBuilders.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2021-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.wear.protolayout;
+
+import static androidx.annotation.Dimension.DP;
+
+import androidx.annotation.Dimension;
+import androidx.annotation.FloatRange;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.wear.protolayout.proto.DeviceParametersProto;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Builders for request messages used to fetch tiles and resources. */
+public final class DeviceParametersBuilders {
+  private DeviceParametersBuilders() {}
+
+  /**
+   * The platform of the device requesting a tile.
+   *
+   * @hide
+   */
+  @RestrictTo(RestrictTo.Scope.LIBRARY)
+  @IntDef({DEVICE_PLATFORM_UNDEFINED, DEVICE_PLATFORM_WEAR_OS})
+  @Retention(RetentionPolicy.SOURCE)
+  public @interface DevicePlatform {}
+
+  /** Device platform is undefined. */
+  public static final int DEVICE_PLATFORM_UNDEFINED = 0;
+
+  /** Device is a Wear OS device. */
+  public static final int DEVICE_PLATFORM_WEAR_OS = 1;
+
+  /**
+   * The shape of a screen.
+   *
+   * @hide
+   */
+  @RestrictTo(RestrictTo.Scope.LIBRARY)
+  @IntDef({SCREEN_SHAPE_UNDEFINED, SCREEN_SHAPE_ROUND, SCREEN_SHAPE_RECT})
+  @Retention(RetentionPolicy.SOURCE)
+  public @interface ScreenShape {}
+
+  /** Screen shape is undefined. */
+  public static final int SCREEN_SHAPE_UNDEFINED = 0;
+
+  /** A round screen (typically found on most Wear devices). */
+  public static final int SCREEN_SHAPE_ROUND = 1;
+
+  /** Rectangular screens. */
+  public static final int SCREEN_SHAPE_RECT = 2;
+
+  /**
+   * Parameters describing the device requesting a tile update. This contains physical and logical
+   * characteristics about the device (e.g. screen size and density, etc).
+   */
+  public static final class DeviceParameters {
+    private final DeviceParametersProto.DeviceParameters mImpl;
+
+    private DeviceParameters(DeviceParametersProto.DeviceParameters impl) {
+      this.mImpl = impl;
+    }
+
+    /** Gets width of the device's screen in DP. */
+    @Dimension(unit = DP)
+    public int getScreenWidthDp() {
+      return mImpl.getScreenWidthDp();
+    }
+
+    /** Gets height of the device's screen in DP. */
+    @Dimension(unit = DP)
+    public int getScreenHeightDp() {
+      return mImpl.getScreenHeightDp();
+    }
+
+    /**
+     * Gets density of the display. This value is the scaling factor to get from DP to Pixels (px =
+     * dp * density).
+     */
+    @FloatRange(from = 0.0, fromInclusive = false, toInclusive = false)
+    public float getScreenDensity() {
+      return mImpl.getScreenDensity();
+    }
+
+    /** Gets the platform of the device. */
+    @DevicePlatform
+    public int getDevicePlatform() {
+      return mImpl.getDevicePlatform().getNumber();
+    }
+
+    /** Gets the shape of the device's screen. */
+    @ScreenShape
+    public int getScreenShape() {
+      return mImpl.getScreenShape().getNumber();
+    }
+
+    @NonNull
+    static DeviceParameters fromProto(@NonNull DeviceParametersProto.DeviceParameters proto) {
+      return new DeviceParameters(proto);
+    }
+
+    @NonNull
+    DeviceParametersProto.DeviceParameters toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link DeviceParameters} */
+    public static final class Builder {
+      private final DeviceParametersProto.DeviceParameters.Builder mImpl =
+          DeviceParametersProto.DeviceParameters.newBuilder();
+
+      public Builder() {}
+
+      /** Sets width of the device's screen in DP. */
+      @NonNull
+      public Builder setScreenWidthDp(@Dimension(unit = DP) int screenWidthDp) {
+        mImpl.setScreenWidthDp(screenWidthDp);
+        return this;
+      }
+
+      /** Sets height of the device's screen in DP. */
+      @NonNull
+      public Builder setScreenHeightDp(@Dimension(unit = DP) int screenHeightDp) {
+        mImpl.setScreenHeightDp(screenHeightDp);
+        return this;
+      }
+
+      /**
+       * Sets density of the display. This value is the scaling factor to get from DP to Pixels (px
+       * = dp * density).
+       */
+      @NonNull
+      public Builder setScreenDensity(
+          @FloatRange(from = 0.0, fromInclusive = false, toInclusive = false) float screenDensity) {
+        mImpl.setScreenDensity(screenDensity);
+        return this;
+      }
+
+      /** Sets the platform of the device. */
+      @NonNull
+      public Builder setDevicePlatform(@DevicePlatform int devicePlatform) {
+        mImpl.setDevicePlatform(DeviceParametersProto.DevicePlatform.forNumber(devicePlatform));
+        return this;
+      }
+
+      /** Sets the shape of the device's screen. */
+      @NonNull
+      public Builder setScreenShape(@ScreenShape int screenShape) {
+        mImpl.setScreenShape(DeviceParametersProto.ScreenShape.forNumber(screenShape));
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public DeviceParameters build() {
+        return DeviceParameters.fromProto(mImpl.build());
+      }
+    }
+  }
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/DimensionBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/DimensionBuilders.java
new file mode 100644
index 0000000..e900f65
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/DimensionBuilders.java
@@ -0,0 +1,692 @@
+/*
+ * Copyright 2021-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.wear.protolayout;
+
+import static androidx.annotation.Dimension.DP;
+import static androidx.annotation.Dimension.SP;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.util.DisplayMetrics;
+import androidx.annotation.Dimension;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.protolayout.expression.Fingerprint;
+import androidx.wear.protolayout.proto.DimensionProto;
+
+/** Builders for dimensions for layout elements. */
+public final class DimensionBuilders {
+  private DimensionBuilders() {}
+
+  private static final ExpandedDimensionProp EXPAND = new ExpandedDimensionProp.Builder().build();
+  private static final WrappedDimensionProp WRAP = new WrappedDimensionProp.Builder().build();
+
+  /** Shortcut for building a {@link DpProp} using a measurement in DP. */
+  @NonNull
+  public static DpProp dp(@Dimension(unit = DP) float valueDp) {
+    return new DpProp.Builder().setValue(valueDp).build();
+  }
+
+  /** Shortcut for building a {@link SpProp} using a measurement in SP. */
+  @NonNull
+  public static SpProp sp(@Dimension(unit = SP) float valueSp) {
+    return new SpProp.Builder().setValue(valueSp).build();
+  }
+
+  /** Shortcut for building a {@link EmProp} using a measurement in EM. */
+  @NonNull
+  public static EmProp em(int valueEm) {
+    return new EmProp.Builder().setValue(valueEm).build();
+  }
+
+  /** Shortcut for building a {@link EmProp} using a measurement in EM. */
+  @NonNull
+  public static EmProp em(float valueEm) {
+    return new EmProp.Builder().setValue(valueEm).build();
+  }
+
+  /** Shortcut for building an {@link DegreesProp} using a measurement in degrees. */
+  @NonNull
+  public static DegreesProp degrees(float valueDegrees) {
+    return new DegreesProp.Builder().setValue(valueDegrees).build();
+  }
+
+  /**
+   * Shortcut for building an {@link ExpandedDimensionProp} that will expand to the size of its
+   * parent.
+   */
+  @NonNull
+  public static ExpandedDimensionProp expand() {
+    return EXPAND;
+  }
+
+  /**
+   * Shortcut for building an {@link WrappedDimensionProp} that will shrink to the size of its
+   * children.
+   */
+  @NonNull
+  public static WrappedDimensionProp wrap() {
+    return WRAP;
+  }
+
+  /** A type for linear dimensions, measured in dp. */
+  public static final class DpProp implements ContainerDimension, ImageDimension, SpacerDimension {
+    private final DimensionProto.DpProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    DpProp(DimensionProto.DpProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the value, in dp. Intended for testing purposes only. */
+    @Dimension(unit = DP)
+    public float getValue() {
+      return mImpl.getValue();
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static DpProp fromProto(@NonNull DimensionProto.DpProp proto) {
+      return new DpProp(proto, null);
+    }
+
+    @NonNull
+    DimensionProto.DpProp toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public DimensionProto.ContainerDimension toContainerDimensionProto() {
+      return DimensionProto.ContainerDimension.newBuilder().setLinearDimension(mImpl).build();
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public DimensionProto.ImageDimension toImageDimensionProto() {
+      return DimensionProto.ImageDimension.newBuilder().setLinearDimension(mImpl).build();
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public DimensionProto.SpacerDimension toSpacerDimensionProto() {
+      return DimensionProto.SpacerDimension.newBuilder().setLinearDimension(mImpl).build();
+    }
+
+    /** Builder for {@link DpProp}. */
+    public static final class Builder
+        implements ContainerDimension.Builder, ImageDimension.Builder, SpacerDimension.Builder {
+      private final DimensionProto.DpProp.Builder mImpl = DimensionProto.DpProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(752970309);
+
+      public Builder() {}
+
+      /** Sets the value, in dp. */
+      @NonNull
+      public Builder setValue(@Dimension(unit = DP) float value) {
+        mImpl.setValue(value);
+        mFingerprint.recordPropertyUpdate(1, Float.floatToIntBits(value));
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public DpProp build() {
+        return new DpProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** A type for font sizes, measured in sp. */
+  public static final class SpProp {
+    private final DimensionProto.SpProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    SpProp(DimensionProto.SpProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the value, in sp. Intended for testing purposes only. */
+    @Dimension(unit = SP)
+    public float getValue() {
+      return mImpl.getValue();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static SpProp fromProto(@NonNull DimensionProto.SpProp proto) {
+      return new SpProp(proto, null);
+    }
+
+    @NonNull
+    DimensionProto.SpProp toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link SpProp} */
+    public static final class Builder {
+      private final DimensionProto.SpProp.Builder mImpl = DimensionProto.SpProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-2144685857);
+
+      public Builder() {}
+
+      /** Sets the value, in sp. */
+      @NonNull
+      public Builder setValue(@Dimension(unit = SP) float value) {
+        mImpl.setValue(value);
+        mFingerprint.recordPropertyUpdate(2, Float.floatToIntBits(value));
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public SpProp build() {
+        return new SpProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** A type for font spacing, measured in em. */
+  public static final class EmProp {
+    private final DimensionProto.EmProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    EmProp(DimensionProto.EmProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the value, in em. Intended for testing purposes only. */
+    public float getValue() {
+      return mImpl.getValue();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static EmProp fromProto(@NonNull DimensionProto.EmProp proto) {
+      return new EmProp(proto, null);
+    }
+
+    @NonNull
+    DimensionProto.EmProp toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link EmProp} */
+    public static final class Builder {
+      private final DimensionProto.EmProp.Builder mImpl = DimensionProto.EmProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(1628313311);
+
+      public Builder() {}
+
+      /** Sets the value, in em. */
+      @NonNull
+      public Builder setValue(float value) {
+        mImpl.setValue(value);
+        mFingerprint.recordPropertyUpdate(1, Float.floatToIntBits(value));
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public EmProp build() {
+        return new EmProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** A type for angular dimensions, measured in degrees. */
+  public static final class DegreesProp {
+    private final DimensionProto.DegreesProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    DegreesProp(DimensionProto.DegreesProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the value, in degrees. Intended for testing purposes only. */
+    public float getValue() {
+      return mImpl.getValue();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static DegreesProp fromProto(@NonNull DimensionProto.DegreesProp proto) {
+      return new DegreesProp(proto, null);
+    }
+
+    @NonNull
+    DimensionProto.DegreesProp toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link DegreesProp} */
+    public static final class Builder {
+      private final DimensionProto.DegreesProp.Builder mImpl =
+          DimensionProto.DegreesProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(405060347);
+
+      public Builder() {}
+
+      /** Sets the value, in degrees. */
+      @NonNull
+      public Builder setValue(float value) {
+        mImpl.setValue(value);
+        mFingerprint.recordPropertyUpdate(1, Float.floatToIntBits(value));
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public DegreesProp build() {
+        return new DegreesProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * A type for a dimension that fills all the space it can (i.e. MATCH_PARENT in Android parlance).
+   */
+  public static final class ExpandedDimensionProp implements ContainerDimension, ImageDimension {
+    private final DimensionProto.ExpandedDimensionProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    ExpandedDimensionProp(
+        DimensionProto.ExpandedDimensionProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static ExpandedDimensionProp fromProto(@NonNull DimensionProto.ExpandedDimensionProp proto) {
+      return new ExpandedDimensionProp(proto, null);
+    }
+
+    @NonNull
+    DimensionProto.ExpandedDimensionProp toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public DimensionProto.ContainerDimension toContainerDimensionProto() {
+      return DimensionProto.ContainerDimension.newBuilder().setExpandedDimension(mImpl).build();
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public DimensionProto.ImageDimension toImageDimensionProto() {
+      return DimensionProto.ImageDimension.newBuilder().setExpandedDimension(mImpl).build();
+    }
+
+    /** Builder for {@link ExpandedDimensionProp}. */
+    public static final class Builder
+        implements ContainerDimension.Builder, ImageDimension.Builder {
+      private final DimensionProto.ExpandedDimensionProp.Builder mImpl =
+          DimensionProto.ExpandedDimensionProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(1053378170);
+
+      public Builder() {}
+
+      @Override
+      @NonNull
+      public ExpandedDimensionProp build() {
+        return new ExpandedDimensionProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * A type for a dimension that sizes itself to the size of its children (i.e. WRAP_CONTENT in
+   * Android parlance).
+   */
+  public static final class WrappedDimensionProp implements ContainerDimension {
+    private final DimensionProto.WrappedDimensionProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    WrappedDimensionProp(
+        DimensionProto.WrappedDimensionProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static WrappedDimensionProp fromProto(@NonNull DimensionProto.WrappedDimensionProp proto) {
+      return new WrappedDimensionProp(proto, null);
+    }
+
+    @NonNull
+    DimensionProto.WrappedDimensionProp toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public DimensionProto.ContainerDimension toContainerDimensionProto() {
+      return DimensionProto.ContainerDimension.newBuilder().setWrappedDimension(mImpl).build();
+    }
+
+    /** Builder for {@link WrappedDimensionProp}. */
+    public static final class Builder implements ContainerDimension.Builder {
+      private final DimensionProto.WrappedDimensionProp.Builder mImpl =
+          DimensionProto.WrappedDimensionProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-113456542);
+
+      public Builder() {}
+
+      @Override
+      @NonNull
+      public WrappedDimensionProp build() {
+        return new WrappedDimensionProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * A type for a dimension that scales itself proportionally to another dimension such that the
+   * aspect ratio defined by the given width and height values is preserved.
+   *
+   * <p>Note that the width and height are unitless; only their ratio is relevant. This allows for
+   * specifying an element's size using common ratios (e.g. width=4, height=3), or to allow an
+   * element to be resized proportionally based on the size of an underlying asset (e.g. an 800x600
+   * image being added to a smaller container and resized accordingly).
+   */
+  public static final class ProportionalDimensionProp implements ImageDimension {
+    private final DimensionProto.ProportionalDimensionProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    ProportionalDimensionProp(
+        DimensionProto.ProportionalDimensionProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the width to be used when calculating the aspect ratio to preserve. Intended for testing
+     * purposes only.
+     */
+    @IntRange(from = 0)
+    public int getAspectRatioWidth() {
+      return mImpl.getAspectRatioWidth();
+    }
+
+    /**
+     * Gets the height to be used when calculating the aspect ratio ratio to preserve. Intended for
+     * testing purposes only.
+     */
+    @IntRange(from = 0)
+    public int getAspectRatioHeight() {
+      return mImpl.getAspectRatioHeight();
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static ProportionalDimensionProp fromProto(
+        @NonNull DimensionProto.ProportionalDimensionProp proto) {
+      return new ProportionalDimensionProp(proto, null);
+    }
+
+    @NonNull
+    DimensionProto.ProportionalDimensionProp toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public DimensionProto.ImageDimension toImageDimensionProto() {
+      return DimensionProto.ImageDimension.newBuilder().setProportionalDimension(mImpl).build();
+    }
+
+    /** Builder for {@link ProportionalDimensionProp}. */
+    public static final class Builder implements ImageDimension.Builder {
+      private final DimensionProto.ProportionalDimensionProp.Builder mImpl =
+          DimensionProto.ProportionalDimensionProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-2079046835);
+
+      public Builder() {}
+
+      /** Sets the width to be used when calculating the aspect ratio to preserve. */
+      @NonNull
+      public Builder setAspectRatioWidth(@IntRange(from = 0) int aspectRatioWidth) {
+        mImpl.setAspectRatioWidth(aspectRatioWidth);
+        mFingerprint.recordPropertyUpdate(1, aspectRatioWidth);
+        return this;
+      }
+
+      /** Sets the height to be used when calculating the aspect ratio ratio to preserve. */
+      @NonNull
+      public Builder setAspectRatioHeight(@IntRange(from = 0) int aspectRatioHeight) {
+        mImpl.setAspectRatioHeight(aspectRatioHeight);
+        mFingerprint.recordPropertyUpdate(2, aspectRatioHeight);
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public ProportionalDimensionProp build() {
+        return new ProportionalDimensionProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** Interface defining a dimension that can be applied to a container. */
+  public interface ContainerDimension {
+    /**
+     * Get the protocol buffer representation of this object.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    DimensionProto.ContainerDimension toContainerDimensionProto();
+
+    /**
+     * Get the fingerprint for this object or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    Fingerprint getFingerprint();
+
+    /** Builder to create {@link ContainerDimension} objects. */
+    @SuppressLint("StaticFinalBuilder")
+    interface Builder {
+
+      /** Builds an instance with values accumulated in this Builder. */
+      @NonNull
+      ContainerDimension build();
+    }
+  }
+
+  @NonNull
+  static ContainerDimension containerDimensionFromProto(
+      @NonNull DimensionProto.ContainerDimension proto) {
+    if (proto.hasLinearDimension()) {
+      return DpProp.fromProto(proto.getLinearDimension());
+    }
+    if (proto.hasExpandedDimension()) {
+      return ExpandedDimensionProp.fromProto(proto.getExpandedDimension());
+    }
+    if (proto.hasWrappedDimension()) {
+      return WrappedDimensionProp.fromProto(proto.getWrappedDimension());
+    }
+    throw new IllegalStateException("Proto was not a recognised instance of ContainerDimension");
+  }
+
+  /** Interface defining a dimension that can be applied to an image. */
+  public interface ImageDimension {
+    /**
+     * Get the protocol buffer representation of this object.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    DimensionProto.ImageDimension toImageDimensionProto();
+
+    /**
+     * Get the fingerprint for this object or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    Fingerprint getFingerprint();
+
+    /** Builder to create {@link ImageDimension} objects. */
+    @SuppressLint("StaticFinalBuilder")
+    interface Builder {
+
+      /** Builds an instance with values accumulated in this Builder. */
+      @NonNull
+      ImageDimension build();
+    }
+  }
+
+  @NonNull
+  static ImageDimension imageDimensionFromProto(@NonNull DimensionProto.ImageDimension proto) {
+    if (proto.hasLinearDimension()) {
+      return DpProp.fromProto(proto.getLinearDimension());
+    }
+    if (proto.hasExpandedDimension()) {
+      return ExpandedDimensionProp.fromProto(proto.getExpandedDimension());
+    }
+    if (proto.hasProportionalDimension()) {
+      return ProportionalDimensionProp.fromProto(proto.getProportionalDimension());
+    }
+    throw new IllegalStateException("Proto was not a recognised instance of ImageDimension");
+  }
+
+  /** Interface defining a dimension that can be applied to a spacer. */
+  public interface SpacerDimension {
+    /**
+     * Get the protocol buffer representation of this object.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    DimensionProto.SpacerDimension toSpacerDimensionProto();
+
+    /**
+     * Get the fingerprint for this object or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    Fingerprint getFingerprint();
+
+    /** Builder to create {@link SpacerDimension} objects. */
+    @SuppressLint("StaticFinalBuilder")
+    interface Builder {
+
+      /** Builds an instance with values accumulated in this Builder. */
+      @NonNull
+      SpacerDimension build();
+    }
+  }
+
+  @NonNull
+  static SpacerDimension spacerDimensionFromProto(@NonNull DimensionProto.SpacerDimension proto) {
+    if (proto.hasLinearDimension()) {
+      return DpProp.fromProto(proto.getLinearDimension());
+    }
+    throw new IllegalStateException("Proto was not a recognised instance of SpacerDimension");
+  }
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
new file mode 100644
index 0000000..b123903
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
@@ -0,0 +1,4163 @@
+/*
+ * Copyright 2021-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.wear.protolayout;
+
+import static androidx.wear.protolayout.expression.Preconditions.checkNotNull;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.protolayout.ColorBuilders.ColorProp;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.DimensionBuilders.ContainerDimension;
+import androidx.wear.protolayout.DimensionBuilders.DegreesProp;
+import androidx.wear.protolayout.DimensionBuilders.DpProp;
+import androidx.wear.protolayout.DimensionBuilders.EmProp;
+import androidx.wear.protolayout.DimensionBuilders.ImageDimension;
+import androidx.wear.protolayout.DimensionBuilders.SpProp;
+import androidx.wear.protolayout.DimensionBuilders.SpacerDimension;
+import androidx.wear.protolayout.ModifiersBuilders.ArcModifiers;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+import androidx.wear.protolayout.ModifiersBuilders.SpanModifiers;
+import androidx.wear.protolayout.TypeBuilders.BoolProp;
+import androidx.wear.protolayout.TypeBuilders.Int32Prop;
+import androidx.wear.protolayout.TypeBuilders.StringProp;
+import androidx.wear.protolayout.expression.Fingerprint;
+import androidx.wear.protolayout.expression.ProtoLayoutExperimental;
+import androidx.wear.protolayout.proto.AlignmentProto;
+import androidx.wear.protolayout.proto.FingerprintProto;
+import androidx.wear.protolayout.proto.FingerprintProto.TreeFingerprint;
+import androidx.wear.protolayout.proto.LayoutElementProto;
+import androidx.wear.protolayout.proto.TypesProto;
+import androidx.wear.protolayout.protobuf.InvalidProtocolBufferException;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Builders for composable layout elements that can be combined together to create renderable
+ * UI */
+public final class LayoutElementBuilders {
+  private LayoutElementBuilders() {}
+
+  /**
+   * The weight to be applied to the font.
+   *
+   * @hide
+   */
+  @RestrictTo(RestrictTo.Scope.LIBRARY)
+  @IntDef({FONT_WEIGHT_UNDEFINED, FONT_WEIGHT_NORMAL, FONT_WEIGHT_MEDIUM, FONT_WEIGHT_BOLD})
+  @Retention(RetentionPolicy.SOURCE)
+  public @interface FontWeight {}
+
+  /** Font weight is undefined. */
+  public static final int FONT_WEIGHT_UNDEFINED = 0;
+
+  /** Normal font weight. */
+  public static final int FONT_WEIGHT_NORMAL = 400;
+
+  /** Medium font weight. */
+  @ProtoLayoutExperimental public static final int FONT_WEIGHT_MEDIUM = 500;
+
+  /** Bold font weight. */
+  public static final int FONT_WEIGHT_BOLD = 700;
+
+  /**
+   * The variant of a font. Some renderers may use different fonts for title and body text, which
+   * can be selected using this field.
+   *
+   * @hide
+   */
+  @RestrictTo(RestrictTo.Scope.LIBRARY)
+  @IntDef({FONT_VARIANT_UNDEFINED, FONT_VARIANT_TITLE, FONT_VARIANT_BODY})
+  @Retention(RetentionPolicy.SOURCE)
+  public @interface FontVariant {}
+
+  /** Font variant is undefined. */
+  public static final int FONT_VARIANT_UNDEFINED = 0;
+
+  /** Font variant suited for title text. */
+  public static final int FONT_VARIANT_TITLE = 1;
+
+  /** Font variant suited for body text. */
+  public static final int FONT_VARIANT_BODY = 2;
+
+  /**
+   * The alignment of a {@link SpanImage} within the line height of the surrounding {@link
+   * Spannable}.
+   *
+   * @hide
+   */
+  @RestrictTo(RestrictTo.Scope.LIBRARY)
+  @IntDef({
+    SPAN_VERTICAL_ALIGN_UNDEFINED,
+    SPAN_VERTICAL_ALIGN_BOTTOM,
+    SPAN_VERTICAL_ALIGN_TEXT_BASELINE
+  })
+  @Retention(RetentionPolicy.SOURCE)
+  public @interface SpanVerticalAlignment {}
+
+  /** Alignment is undefined. */
+  public static final int SPAN_VERTICAL_ALIGN_UNDEFINED = 0;
+
+  /**
+   * Align to the bottom of the line (descent of the largest text in this line). If there is no text
+   * in the line containing this image, this will align to the bottom of the line, where the line
+   * height is defined as the height of the largest image in the line.
+   */
+  public static final int SPAN_VERTICAL_ALIGN_BOTTOM = 1;
+
+  /**
+   * Align to the baseline of the text. Note that if the line in the {@link Spannable} which
+   * contains this image does not contain any text, the effects of using this alignment are
+   * undefined.
+   */
+  public static final int SPAN_VERTICAL_ALIGN_TEXT_BASELINE = 2;
+
+  /**
+   * How text that will not fit inside the bounds of a {@link Text} element will be handled.
+   *
+   * @hide
+   */
+  @RestrictTo(RestrictTo.Scope.LIBRARY)
+  @IntDef({TEXT_OVERFLOW_UNDEFINED, TEXT_OVERFLOW_TRUNCATE, TEXT_OVERFLOW_ELLIPSIZE_END})
+  @Retention(RetentionPolicy.SOURCE)
+  public @interface TextOverflow {}
+
+  /** Overflow behavior is undefined. */
+  public static final int TEXT_OVERFLOW_UNDEFINED = 0;
+
+  /**
+   * Truncate the text to fit inside of the {@link Text} element's bounds. If text is truncated, it
+   * will be truncated on a word boundary.
+   */
+  public static final int TEXT_OVERFLOW_TRUNCATE = 1;
+
+  /**
+   * Truncate the text to fit in the {@link Text} element's bounds, but add an ellipsis (i.e. ...)
+   * to the end of the text if it has been truncated.
+   */
+  public static final int TEXT_OVERFLOW_ELLIPSIZE_END = 2;
+
+  /**
+   * How content which does not match the dimensions of its bounds (e.g. an image resource being
+   * drawn inside an {@link Image}) will be resized to fit its bounds.
+   *
+   * @hide
+   */
+  @RestrictTo(RestrictTo.Scope.LIBRARY)
+  @IntDef({
+    CONTENT_SCALE_MODE_UNDEFINED,
+    CONTENT_SCALE_MODE_FIT,
+    CONTENT_SCALE_MODE_CROP,
+    CONTENT_SCALE_MODE_FILL_BOUNDS
+  })
+  @Retention(RetentionPolicy.SOURCE)
+  public @interface ContentScaleMode {}
+
+  /** Content scaling is undefined. */
+  public static final int CONTENT_SCALE_MODE_UNDEFINED = 0;
+
+  /**
+   * Content will be scaled to fit inside its bounds, proportionally. As an example, If a 10x5 image
+   * was going to be drawn inside a 50x50 {@link Image} element, the actual image resource would be
+   * drawn as a 50x25 image, centered within the 50x50 bounds.
+   */
+  public static final int CONTENT_SCALE_MODE_FIT = 1;
+
+  /**
+   * Content will be resized proportionally so it completely fills its bounds, and anything outside
+   * of the bounds will be cropped. As an example, if a 10x5 image was going to be drawn inside a
+   * 50x50 {@link Image} element, the image resource would be drawn as a 100x50 image, centered
+   * within its bounds (and with 25px cropped from both the left and right sides).
+   */
+  public static final int CONTENT_SCALE_MODE_CROP = 2;
+
+  /**
+   * Content will be resized to fill its bounds, without taking into account the aspect ratio. If a
+   * 10x5 image was going to be drawn inside a 50x50 {@link Image} element, the image would be drawn
+   * as a 50x50 image, stretched vertically.
+   */
+  public static final int CONTENT_SCALE_MODE_FILL_BOUNDS = 3;
+
+  /** An extensible {@code FontWeight} property. */
+  public static final class FontWeightProp {
+    private final LayoutElementProto.FontWeightProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    FontWeightProp(
+        LayoutElementProto.FontWeightProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the value. Intended for testing purposes only. */
+    @FontWeight
+    public int getValue() {
+      return mImpl.getValue().getNumber();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static FontWeightProp fromProto(@NonNull LayoutElementProto.FontWeightProp proto) {
+      return new FontWeightProp(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.FontWeightProp toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link FontWeightProp} */
+    public static final class Builder {
+      private final LayoutElementProto.FontWeightProp.Builder mImpl =
+          LayoutElementProto.FontWeightProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(1793388920);
+
+      public Builder() {}
+
+      /** Sets the value. */
+      @NonNull
+      public Builder setValue(@FontWeight int value) {
+        mImpl.setValue(LayoutElementProto.FontWeight.forNumber(value));
+        mFingerprint.recordPropertyUpdate(1, value);
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public FontWeightProp build() {
+        return new FontWeightProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** An extensible {@code FontVariant} property. */
+  @ProtoLayoutExperimental
+  public static final class FontVariantProp {
+    private final LayoutElementProto.FontVariantProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    FontVariantProp(
+        LayoutElementProto.FontVariantProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the value. Intended for testing purposes only. */
+    @FontVariant
+    public int getValue() {
+      return mImpl.getValue().getNumber();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static FontVariantProp fromProto(@NonNull LayoutElementProto.FontVariantProp proto) {
+      return new FontVariantProp(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.FontVariantProp toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link FontVariantProp} */
+    public static final class Builder {
+      private final LayoutElementProto.FontVariantProp.Builder mImpl =
+          LayoutElementProto.FontVariantProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-293831500);
+
+      public Builder() {}
+
+      /** Sets the value. */
+      @NonNull
+      public Builder setValue(@FontVariant int value) {
+        mImpl.setValue(LayoutElementProto.FontVariant.forNumber(value));
+        mFingerprint.recordPropertyUpdate(1, value);
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public FontVariantProp build() {
+        return new FontVariantProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** An extensible {@code SpanVerticalAlignment} property. */
+  public static final class SpanVerticalAlignmentProp {
+    private final LayoutElementProto.SpanVerticalAlignmentProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    SpanVerticalAlignmentProp(
+        LayoutElementProto.SpanVerticalAlignmentProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the value. Intended for testing purposes only. */
+    @SpanVerticalAlignment
+    public int getValue() {
+      return mImpl.getValue().getNumber();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static SpanVerticalAlignmentProp fromProto(
+        @NonNull LayoutElementProto.SpanVerticalAlignmentProp proto) {
+      return new SpanVerticalAlignmentProp(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.SpanVerticalAlignmentProp toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link SpanVerticalAlignmentProp} */
+    public static final class Builder {
+      private final LayoutElementProto.SpanVerticalAlignmentProp.Builder mImpl =
+          LayoutElementProto.SpanVerticalAlignmentProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(1008812329);
+
+      public Builder() {}
+
+      /** Sets the value. */
+      @NonNull
+      public Builder setValue(@SpanVerticalAlignment int value) {
+        mImpl.setValue(LayoutElementProto.SpanVerticalAlignment.forNumber(value));
+        mFingerprint.recordPropertyUpdate(1, value);
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public SpanVerticalAlignmentProp build() {
+        return new SpanVerticalAlignmentProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** The styling of a font (e.g. font size, and metrics). */
+  public static final class FontStyle {
+    private final LayoutElementProto.FontStyle mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    FontStyle(LayoutElementProto.FontStyle impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the size of the font, in scaled pixels (sp). If not specified, defaults to the size of
+     * the system's "body" font. Intended for testing purposes only.
+     */
+    @Nullable
+    public SpProp getSize() {
+      if (mImpl.hasSize()) {
+        return SpProp.fromProto(mImpl.getSize());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets whether the text should be rendered in a italic typeface. If not specified, defaults to
+     * "false". Intended for testing purposes only.
+     */
+    @Nullable
+    public BoolProp getItalic() {
+      if (mImpl.hasItalic()) {
+        return BoolProp.fromProto(mImpl.getItalic());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets whether the text should be rendered with an underline. If not specified, defaults to
+     * "false". Intended for testing purposes only.
+     */
+    @Nullable
+    public BoolProp getUnderline() {
+      if (mImpl.hasUnderline()) {
+        return BoolProp.fromProto(mImpl.getUnderline());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the text color. If not defined, defaults to white. Intended for testing purposes only.
+     */
+    @Nullable
+    public ColorProp getColor() {
+      if (mImpl.hasColor()) {
+        return ColorProp.fromProto(mImpl.getColor());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the weight of the font. If the provided value is not supported on a platform, the
+     * nearest supported value will be used. If not defined, or when set to an invalid value,
+     * defaults to "normal". Intended for testing purposes only.
+     */
+    @Nullable
+    public FontWeightProp getWeight() {
+      if (mImpl.hasWeight()) {
+        return FontWeightProp.fromProto(mImpl.getWeight());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the text letter-spacing. Positive numbers increase the space between letters while
+     * negative numbers tighten the space. If not specified, defaults to 0. Intended for testing
+     * purposes only.
+     */
+    @Nullable
+    public EmProp getLetterSpacing() {
+      if (mImpl.hasLetterSpacing()) {
+        return EmProp.fromProto(mImpl.getLetterSpacing());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the variant of a font. Some renderers may use different fonts for title and body
+     * text, which can be selected using this field. If not specified, defaults to "body".
+     * Intended for testing purposes only.
+     */
+    @ProtoLayoutExperimental
+    @Nullable
+    public FontVariantProp getVariant() {
+      if (mImpl.hasVariant()) {
+        return FontVariantProp.fromProto(mImpl.getVariant());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static FontStyle fromProto(@NonNull LayoutElementProto.FontStyle proto) {
+      return new FontStyle(proto, null);
+    }
+
+    /**
+     * Returns the internal proto instance.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public LayoutElementProto.FontStyle toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link FontStyle} */
+    public static final class Builder {
+      private final LayoutElementProto.FontStyle.Builder mImpl =
+          LayoutElementProto.FontStyle.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(181264306);
+
+      public Builder() {}
+
+      /**
+       * Sets the size of the font, in scaled pixels (sp). If not specified, defaults to the size of
+       * the system's "body" font.
+       */
+      @NonNull
+      public Builder setSize(@NonNull SpProp size) {
+        mImpl.setSize(size.toProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(size.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets whether the text should be rendered in a italic typeface. If not specified, defaults
+       * to "false".
+       */
+      @NonNull
+      public Builder setItalic(@NonNull BoolProp italic) {
+        mImpl.setItalic(italic.toProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(italic.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+      /**
+       * Sets whether the text should be rendered in a italic typeface. If not specified,
+       * defaults to "false".
+       */
+      @SuppressLint("MissingGetterMatchingBuilder")
+      @NonNull
+      public Builder setItalic(boolean italic) {
+        mImpl.setItalic(TypesProto.BoolProp.newBuilder().setValue(italic));
+        return this;
+      }
+      /**
+       * Sets whether the text should be rendered with an underline. If not specified, defaults to
+       * "false".
+       */
+      @NonNull
+      public Builder setUnderline(@NonNull BoolProp underline) {
+        mImpl.setUnderline(underline.toProto());
+        mFingerprint.recordPropertyUpdate(
+            3, checkNotNull(underline.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+      /**
+       * Sets whether the text should be rendered with an underline. If not specified,
+       * defaults to "false".
+       */
+      @SuppressLint("MissingGetterMatchingBuilder")
+      @NonNull
+      public Builder setUnderline(boolean underline) {
+        mImpl.setUnderline(TypesProto.BoolProp.newBuilder().setValue(underline));
+        return this;
+      }
+
+      /**
+       * Sets the text color. If not defined, defaults to white.
+       */
+      @NonNull
+      public Builder setColor(@NonNull ColorProp color) {
+        mImpl.setColor(color.toProto());
+        mFingerprint.recordPropertyUpdate(
+            4, checkNotNull(color.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the weight of the font. If the provided value is not supported on a platform, the
+       * nearest supported value will be used. If not defined, or when set to an invalid value,
+       * defaults to "normal".
+       */
+      @NonNull
+      public Builder setWeight(@NonNull FontWeightProp weight) {
+        mImpl.setWeight(weight.toProto());
+        mFingerprint.recordPropertyUpdate(
+            5, checkNotNull(weight.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+      /**
+       * Sets the weight of the font. If the provided value is not supported on a platform,
+       * the nearest supported value will be used. If not defined, or when set to an invalid
+       * value, defaults to "normal".
+       */
+      @NonNull
+      public Builder setWeight(@FontWeight int weight) {
+        mImpl.setWeight(
+                LayoutElementProto.FontWeightProp.newBuilder()
+                        .setValue(LayoutElementProto.FontWeight.forNumber(weight)));
+        return this;
+      }
+
+      /**
+       * Sets the text letter-spacing. Positive numbers increase the space between letters while
+       * negative numbers tighten the space. If not specified, defaults to 0.
+       */
+      @NonNull
+      public Builder setLetterSpacing(@NonNull EmProp letterSpacing) {
+        mImpl.setLetterSpacing(letterSpacing.toProto());
+        mFingerprint.recordPropertyUpdate(
+            6, checkNotNull(letterSpacing.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the variant of a font. Some renderers may use different fonts for title and body
+       * text, which can be selected using this field. If not specified, defaults to "body".
+       */
+      @ProtoLayoutExperimental
+      @NonNull
+      public Builder setVariant(@NonNull FontVariantProp variant) {
+        mImpl.setVariant(variant.toProto());
+        return this;
+      }
+      /**
+       * Sets the variant of a font. Some renderers may use different fonts for title and body
+       * text, which can be selected using this field. If not specified, defaults to "body".
+       */
+      @ProtoLayoutExperimental
+      @NonNull
+      public Builder setVariant(@FontVariant int variant) {
+        mImpl.setVariant(
+                LayoutElementProto.FontVariantProp.newBuilder()
+                        .setValue(LayoutElementProto.FontVariant.forNumber(variant)));
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public FontStyle build() {
+        return new FontStyle(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** An extensible {@code TextOverflow} property. */
+  public static final class TextOverflowProp {
+    private final LayoutElementProto.TextOverflowProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    TextOverflowProp(
+        LayoutElementProto.TextOverflowProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the value. Intended for testing purposes only. */
+    @TextOverflow
+    public int getValue() {
+      return mImpl.getValue().getNumber();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static TextOverflowProp fromProto(@NonNull LayoutElementProto.TextOverflowProp proto) {
+      return new TextOverflowProp(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.TextOverflowProp toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link TextOverflowProp} */
+    public static final class Builder {
+      private final LayoutElementProto.TextOverflowProp.Builder mImpl =
+          LayoutElementProto.TextOverflowProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(1183432233);
+
+      public Builder() {}
+
+      /** Sets the value. */
+      @NonNull
+      public Builder setValue(@TextOverflow int value) {
+        mImpl.setValue(LayoutElementProto.TextOverflow.forNumber(value));
+        mFingerprint.recordPropertyUpdate(1, value);
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public TextOverflowProp build() {
+        return new TextOverflowProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** A text string. */
+  public static final class Text implements LayoutElement {
+    private final LayoutElementProto.Text mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    Text(LayoutElementProto.Text impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the text to render. Intended for testing purposes only. */
+    @Nullable
+    public StringProp getText() {
+      if (mImpl.hasText()) {
+        return StringProp.fromProto(mImpl.getText());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the style of font to use (size, bold etc). If not specified, defaults to the platform's
+     * default body font. Intended for testing purposes only.
+     */
+    @Nullable
+    public FontStyle getFontStyle() {
+      if (mImpl.hasFontStyle()) {
+        return FontStyle.fromProto(mImpl.getFontStyle());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public Modifiers getModifiers() {
+      if (mImpl.hasModifiers()) {
+        return Modifiers.fromProto(mImpl.getModifiers());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the maximum number of lines that can be represented by the {@link Text} element. If not
+     * defined, the {@link Text} element will be treated as a single-line element. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public Int32Prop getMaxLines() {
+      if (mImpl.hasMaxLines()) {
+        return Int32Prop.fromProto(mImpl.getMaxLines());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets alignment of the text within its bounds. Note that a {@link Text} element will size
+     * itself to wrap its contents, so this option is meaningless for single-line text (for that,
+     * use alignment of the outer container). For multi-line text, however, this will set the
+     * alignment of lines relative to the {@link Text} element bounds. If not defined, defaults to
+     * TEXT_ALIGN_CENTER. Intended for testing purposes only.
+     */
+    @Nullable
+    public TextAlignmentProp getMultilineAlignment() {
+      if (mImpl.hasMultilineAlignment()) {
+        return TextAlignmentProp.fromProto(mImpl.getMultilineAlignment());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets how to handle text which overflows the bound of the {@link Text} element. A {@link Text}
+     * element will grow as large as possible inside its parent container (while still respecting
+     * max_lines); if it cannot grow large enough to render all of its text, the text which cannot
+     * fit inside its container will be truncated. If not defined, defaults to
+     * TEXT_OVERFLOW_TRUNCATE. Intended for testing purposes only.
+     */
+    @Nullable
+    public TextOverflowProp getOverflow() {
+      if (mImpl.hasOverflow()) {
+        return TextOverflowProp.fromProto(mImpl.getOverflow());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the explicit height between lines of text. This is equivalent to the vertical distance
+     * between subsequent baselines. If not specified, defaults the font's recommended interline
+     * spacing. Intended for testing purposes only.
+     */
+    @Nullable
+    public SpProp getLineHeight() {
+      if (mImpl.hasLineHeight()) {
+        return SpProp.fromProto(mImpl.getLineHeight());
+      } else {
+        return null;
+      }
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static Text fromProto(@NonNull LayoutElementProto.Text proto) {
+      return new Text(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.Text toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public LayoutElementProto.LayoutElement toLayoutElementProto() {
+      return LayoutElementProto.LayoutElement.newBuilder().setText(mImpl).build();
+    }
+
+    /** Builder for {@link Text}. */
+    public static final class Builder implements LayoutElement.Builder {
+      private final LayoutElementProto.Text.Builder mImpl = LayoutElementProto.Text.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(1976530157);
+
+      public Builder() {}
+
+      /**
+       * Sets the text to render.
+       */
+      @NonNull
+      public Builder setText(@NonNull StringProp text) {
+        mImpl.setText(text.toProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(text.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+      /** Sets the text to render. */
+      @NonNull
+      public Builder setText(@NonNull String text) {
+        mImpl.setText(TypesProto.StringProp.newBuilder().setValue(text));
+        return this;
+      }
+
+      /**
+       * Sets the style of font to use (size, bold etc). If not specified, defaults to the
+       * platform's default body font.
+       */
+      @NonNull
+      public Builder setFontStyle(@NonNull FontStyle fontStyle) {
+        mImpl.setFontStyle(fontStyle.toProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(fontStyle.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Sets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. */
+      @NonNull
+      public Builder setModifiers(@NonNull Modifiers modifiers) {
+        mImpl.setModifiers(modifiers.toProto());
+        mFingerprint.recordPropertyUpdate(
+            3, checkNotNull(modifiers.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the maximum number of lines that can be represented by the {@link Text} element. If
+       * not defined, the {@link Text} element will be treated as a single-line element.
+       */
+      @NonNull
+      public Builder setMaxLines(@NonNull Int32Prop maxLines) {
+        mImpl.setMaxLines(maxLines.toProto());
+        mFingerprint.recordPropertyUpdate(
+            4, checkNotNull(maxLines.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+      /**
+       * Sets the maximum number of lines that can be represented by the {@link Text} element.
+       * If not defined, the {@link Text} element will be treated as a single-line element.
+       */
+      @NonNull
+      public Builder setMaxLines(@IntRange(from = 1) int maxLines) {
+        mImpl.setMaxLines(TypesProto.Int32Prop.newBuilder().setValue(maxLines));
+        return this;
+      }
+
+      /**
+       * Sets alignment of the text within its bounds. Note that a {@link Text} element will size
+       * itself to wrap its contents, so this option is meaningless for single-line text (for that,
+       * use alignment of the outer container). For multi-line text, however, this will set the
+       * alignment of lines relative to the {@link Text} element bounds. If not defined, defaults to
+       * TEXT_ALIGN_CENTER.
+       */
+      @NonNull
+      public Builder setMultilineAlignment(@NonNull TextAlignmentProp multilineAlignment) {
+        mImpl.setMultilineAlignment(multilineAlignment.toProto());
+        mFingerprint.recordPropertyUpdate(
+            5, checkNotNull(multilineAlignment.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+      /**
+       * Sets alignment of the text within its bounds. Note that a {@link Text} element will
+       * size itself to wrap its contents, so this option is meaningless for single-line text
+       * (for that, use alignment of the outer container). For multi-line text, however, this
+       * will set the alignment of lines relative to the {@link Text} element bounds. If not
+       * defined, defaults to TEXT_ALIGN_CENTER.
+       */
+      @NonNull
+      public Builder setMultilineAlignment(@TextAlignment int multilineAlignment) {
+        mImpl.setMultilineAlignment(
+                AlignmentProto.TextAlignmentProp.newBuilder()
+                        .setValue(
+                                AlignmentProto.TextAlignment.forNumber(
+                                        multilineAlignment)));
+        return this;
+      }
+
+      /**
+       * Sets how to handle text which overflows the bound of the {@link Text} element. A {@link
+       * Text} element will grow as large as possible inside its parent container (while still
+       * respecting max_lines); if it cannot grow large enough to render all of its text, the text
+       * which cannot fit inside its container will be truncated. If not defined, defaults to
+       * TEXT_OVERFLOW_TRUNCATE.
+       */
+      @NonNull
+      public Builder setOverflow(@NonNull TextOverflowProp overflow) {
+        mImpl.setOverflow(overflow.toProto());
+        mFingerprint.recordPropertyUpdate(
+            6, checkNotNull(overflow.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+      /**
+       * Sets how to handle text which overflows the bound of the {@link Text} element. A
+       * {@link Text} element will grow as large as possible inside its parent container
+       * (while still respecting max_lines); if it cannot grow large enough to render all of
+       * its text, the text which cannot fit inside its container will be truncated. If not
+       * defined, defaults to TEXT_OVERFLOW_TRUNCATE.
+       */
+      @NonNull
+      public Builder setOverflow(@TextOverflow int overflow) {
+        mImpl.setOverflow(
+                LayoutElementProto.TextOverflowProp.newBuilder()
+                        .setValue(LayoutElementProto.TextOverflow.forNumber(overflow)));
+        return this;
+      }
+
+      /**
+       * Sets the explicit height between lines of text. This is equivalent to the vertical distance
+       * between subsequent baselines. If not specified, defaults the font's recommended interline
+       * spacing.
+       */
+      @NonNull
+      public Builder setLineHeight(@NonNull SpProp lineHeight) {
+        mImpl.setLineHeight(lineHeight.toProto());
+        mFingerprint.recordPropertyUpdate(
+            7, checkNotNull(lineHeight.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public Text build() {
+        return new Text(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** An extensible {@code ContentScaleMode} property. */
+  public static final class ContentScaleModeProp {
+    private final LayoutElementProto.ContentScaleModeProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    ContentScaleModeProp(
+        LayoutElementProto.ContentScaleModeProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the value. Intended for testing purposes only. */
+    @ContentScaleMode
+    public int getValue() {
+      return mImpl.getValue().getNumber();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static ContentScaleModeProp fromProto(@NonNull LayoutElementProto.ContentScaleModeProp proto) {
+      return new ContentScaleModeProp(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.ContentScaleModeProp toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link ContentScaleModeProp} */
+    public static final class Builder {
+      private final LayoutElementProto.ContentScaleModeProp.Builder mImpl =
+          LayoutElementProto.ContentScaleModeProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-893830536);
+
+      public Builder() {}
+
+      /** Sets the value. */
+      @NonNull
+      public Builder setValue(@ContentScaleMode int value) {
+        mImpl.setValue(LayoutElementProto.ContentScaleMode.forNumber(value));
+        mFingerprint.recordPropertyUpdate(1, value);
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public ContentScaleModeProp build() {
+        return new ContentScaleModeProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** Filtering parameters used for images. This can be used to apply a color tint to images. */
+  public static final class ColorFilter {
+    private final LayoutElementProto.ColorFilter mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    ColorFilter(LayoutElementProto.ColorFilter impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the tint color to use. If specified, the image will be tinted, using SRC_IN blending
+     * (that is, all color information will be stripped from the target image, and only the alpha
+     * channel will be blended with the requested color).
+     *
+     * <p>Note that only Android image resources can be tinted; Inline images will not be tinted,
+     * and this property will have no effect. Intended for testing purposes only.
+     */
+    @Nullable
+    public ColorProp getTint() {
+      if (mImpl.hasTint()) {
+        return ColorProp.fromProto(mImpl.getTint());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static ColorFilter fromProto(@NonNull LayoutElementProto.ColorFilter proto) {
+      return new ColorFilter(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.ColorFilter toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link ColorFilter} */
+    public static final class Builder {
+      private final LayoutElementProto.ColorFilter.Builder mImpl =
+          LayoutElementProto.ColorFilter.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(181311326);
+
+      public Builder() {}
+
+      /**
+       * Sets the tint color to use. If specified, the image will be tinted, using SRC_IN blending
+       * (that is, all color information will be stripped from the target image, and only the alpha
+       * channel will be blended with the requested color).
+       *
+       * <p>Note that only Android image resources can be tinted; Inline images will not be tinted,
+       * and this property will have no effect.
+       */
+      @NonNull
+      public Builder setTint(@NonNull ColorProp tint) {
+        mImpl.setTint(tint.toProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(tint.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public ColorFilter build() {
+        return new ColorFilter(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * An image.
+   *
+   * <p>Images used in this element must exist in the resource bundle that corresponds to this
+   * layout. Images must have their dimension specified, and will be rendered at this width and
+   * height, regardless of their native dimension.
+   */
+  public static final class Image implements LayoutElement {
+    private final LayoutElementProto.Image mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    Image(LayoutElementProto.Image impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the resource_id of the image to render. This must exist in the supplied resource bundle.
+     * Intended for testing purposes only.
+     */
+    @Nullable
+    public StringProp getResourceId() {
+      if (mImpl.hasResourceId()) {
+        return StringProp.fromProto(mImpl.getResourceId());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the width of this image. If not defined, the image will not be rendered. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public ImageDimension getWidth() {
+      if (mImpl.hasWidth()) {
+        return DimensionBuilders.imageDimensionFromProto(mImpl.getWidth());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the height of this image. If not defined, the image will not be rendered. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public ImageDimension getHeight() {
+      if (mImpl.hasHeight()) {
+        return DimensionBuilders.imageDimensionFromProto(mImpl.getHeight());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets how to scale the image resource inside the bounds specified by width/height if its size
+     * does not match those bounds. Defaults to CONTENT_SCALE_MODE_FIT. Intended for testing
+     * purposes only.
+     */
+    @Nullable
+    public ContentScaleModeProp getContentScaleMode() {
+      if (mImpl.hasContentScaleMode()) {
+        return ContentScaleModeProp.fromProto(mImpl.getContentScaleMode());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public Modifiers getModifiers() {
+      if (mImpl.hasModifiers()) {
+        return Modifiers.fromProto(mImpl.getModifiers());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets filtering parameters for this image. If not specified, defaults to no filtering.
+     * Intended for testing purposes only.
+     */
+    @Nullable
+    public ColorFilter getColorFilter() {
+      if (mImpl.hasColorFilter()) {
+        return ColorFilter.fromProto(mImpl.getColorFilter());
+      } else {
+        return null;
+      }
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static Image fromProto(@NonNull LayoutElementProto.Image proto) {
+      return new Image(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.Image toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public LayoutElementProto.LayoutElement toLayoutElementProto() {
+      return LayoutElementProto.LayoutElement.newBuilder().setImage(mImpl).build();
+    }
+
+    /** Builder for {@link Image}. */
+    public static final class Builder implements LayoutElement.Builder {
+      private final LayoutElementProto.Image.Builder mImpl = LayoutElementProto.Image.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-543078544);
+
+      public Builder() {}
+
+      /**
+       * Sets the resource_id of the image to render. This must exist in the supplied resource
+       * bundle.
+       */
+      @NonNull
+      public Builder setResourceId(@NonNull StringProp resourceId) {
+        mImpl.setResourceId(resourceId.toProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(resourceId.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+      /**
+       * Sets the resource_id of the image to render. This must exist in the supplied resource
+       * bundle.
+       */
+      @NonNull
+      public Builder setResourceId(@NonNull String resourceId) {
+        mImpl.setResourceId(TypesProto.StringProp.newBuilder().setValue(resourceId));
+        return this;
+      }
+
+      /**
+       * Sets the width of this image. If not defined, the image will not be rendered.
+       */
+      @NonNull
+      public Builder setWidth(@NonNull ImageDimension width) {
+        mImpl.setWidth(width.toImageDimensionProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(width.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the height of this image. If not defined, the image will not be rendered.
+       */
+      @NonNull
+      public Builder setHeight(@NonNull ImageDimension height) {
+        mImpl.setHeight(height.toImageDimensionProto());
+        mFingerprint.recordPropertyUpdate(
+            3, checkNotNull(height.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets how to scale the image resource inside the bounds specified by width/height if its
+       * size does not match those bounds. Defaults to CONTENT_SCALE_MODE_FIT.
+       */
+      @NonNull
+      public Builder setContentScaleMode(@NonNull ContentScaleModeProp contentScaleMode) {
+        mImpl.setContentScaleMode(contentScaleMode.toProto());
+        mFingerprint.recordPropertyUpdate(
+            4, checkNotNull(contentScaleMode.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+      /**
+       * Sets how to scale the image resource inside the bounds specified by width/height if
+       * its size does not match those bounds. Defaults to CONTENT_SCALE_MODE_FIT.
+       */
+      @NonNull
+      public Builder setContentScaleMode(@ContentScaleMode int contentScaleMode) {
+        mImpl.setContentScaleMode(
+                LayoutElementProto.ContentScaleModeProp.newBuilder()
+                        .setValue(
+                                LayoutElementProto.ContentScaleMode.forNumber(
+                                        contentScaleMode)));
+        return this;
+      }
+
+      /** Sets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. */
+      @NonNull
+      public Builder setModifiers(@NonNull Modifiers modifiers) {
+        mImpl.setModifiers(modifiers.toProto());
+        mFingerprint.recordPropertyUpdate(
+            5, checkNotNull(modifiers.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Sets filtering parameters for this image. If not specified, defaults to no filtering. */
+      @NonNull
+      public Builder setColorFilter(@NonNull ColorFilter colorFilter) {
+        mImpl.setColorFilter(colorFilter.toProto());
+        mFingerprint.recordPropertyUpdate(
+            6, checkNotNull(colorFilter.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public Image build() {
+        return new Image(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** A simple spacer, typically used to provide padding between adjacent elements. */
+  public static final class Spacer implements LayoutElement {
+    private final LayoutElementProto.Spacer mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    Spacer(LayoutElementProto.Spacer impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the width of this {@link Spacer}. When this is added as the direct child of an {@link
+     * Arc}, this must be specified as an angular dimension, otherwise a linear dimension must be
+     * used. If not defined, defaults to 0. Intended for testing purposes only.
+     */
+    @Nullable
+    public SpacerDimension getWidth() {
+      if (mImpl.hasWidth()) {
+        return DimensionBuilders.spacerDimensionFromProto(mImpl.getWidth());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the height of this spacer. If not defined, defaults to 0. Intended for testing purposes
+     * only.
+     */
+    @Nullable
+    public SpacerDimension getHeight() {
+      if (mImpl.hasHeight()) {
+        return DimensionBuilders.spacerDimensionFromProto(mImpl.getHeight());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public Modifiers getModifiers() {
+      if (mImpl.hasModifiers()) {
+        return Modifiers.fromProto(mImpl.getModifiers());
+      } else {
+        return null;
+      }
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static Spacer fromProto(@NonNull LayoutElementProto.Spacer proto) {
+      return new Spacer(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.Spacer toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public LayoutElementProto.LayoutElement toLayoutElementProto() {
+      return LayoutElementProto.LayoutElement.newBuilder().setSpacer(mImpl).build();
+    }
+
+    /** Builder for {@link Spacer}. */
+    public static final class Builder implements LayoutElement.Builder {
+      private final LayoutElementProto.Spacer.Builder mImpl =
+          LayoutElementProto.Spacer.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-1748084575);
+
+      public Builder() {}
+
+      /**
+       * Sets the width of this {@link Spacer}. When this is added as the direct child of an {@link
+       * Arc}, this must be specified as an angular dimension, otherwise a linear dimension must be
+       * used. If not defined, defaults to 0.
+       */
+      @NonNull
+      public Builder setWidth(@NonNull SpacerDimension width) {
+        mImpl.setWidth(width.toSpacerDimensionProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(width.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the height of this spacer. If not defined, defaults to 0.
+       */
+      @NonNull
+      public Builder setHeight(@NonNull SpacerDimension height) {
+        mImpl.setHeight(height.toSpacerDimensionProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(height.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Sets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. */
+      @NonNull
+      public Builder setModifiers(@NonNull Modifiers modifiers) {
+        mImpl.setModifiers(modifiers.toProto());
+        mFingerprint.recordPropertyUpdate(
+            3, checkNotNull(modifiers.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+
+      @Override
+      @NonNull
+      public Spacer build() {
+        return new Spacer(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * A container which stacks all of its children on top of one another. This also allows to add a
+   * background color, or to have a border around them with some padding.
+   */
+  public static final class Box implements LayoutElement {
+    private final LayoutElementProto.Box mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    Box(LayoutElementProto.Box impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the child element(s) to wrap. Intended for testing purposes only. */
+    @NonNull
+    public List<LayoutElement> getContents() {
+      List<LayoutElement> list = new ArrayList<>();
+      for (LayoutElementProto.LayoutElement item : mImpl.getContentsList()) {
+        list.add(LayoutElementBuilders.layoutElementFromProto(item));
+      }
+      return Collections.unmodifiableList(list);
+    }
+
+    /**
+     * Gets the height of this {@link Box}. If not defined, this will size itself to fit all of its
+     * children (i.e. a WrappedDimension). Intended for testing purposes only.
+     */
+    @Nullable
+    public ContainerDimension getHeight() {
+      if (mImpl.hasHeight()) {
+        return DimensionBuilders.containerDimensionFromProto(mImpl.getHeight());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the width of this {@link Box}. If not defined, this will size itself to fit all of its
+     * children (i.e. a WrappedDimension). Intended for testing purposes only.
+     */
+    @Nullable
+    public ContainerDimension getWidth() {
+      if (mImpl.hasWidth()) {
+        return DimensionBuilders.containerDimensionFromProto(mImpl.getWidth());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the horizontal alignment of the element inside this {@link Box}. If not defined,
+     * defaults to HORIZONTAL_ALIGN_CENTER. Intended for testing purposes only.
+     */
+    @Nullable
+    public HorizontalAlignmentProp getHorizontalAlignment() {
+      if (mImpl.hasHorizontalAlignment()) {
+        return HorizontalAlignmentProp.fromProto(mImpl.getHorizontalAlignment());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the vertical alignment of the element inside this {@link Box}. If not defined, defaults
+     * to VERTICAL_ALIGN_CENTER. Intended for testing purposes only.
+     */
+    @Nullable
+    public VerticalAlignmentProp getVerticalAlignment() {
+      if (mImpl.hasVerticalAlignment()) {
+        return VerticalAlignmentProp.fromProto(mImpl.getVerticalAlignment());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public Modifiers getModifiers() {
+      if (mImpl.hasModifiers()) {
+        return Modifiers.fromProto(mImpl.getModifiers());
+      } else {
+        return null;
+      }
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static Box fromProto(@NonNull LayoutElementProto.Box proto) {
+      return new Box(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.Box toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public LayoutElementProto.LayoutElement toLayoutElementProto() {
+      return LayoutElementProto.LayoutElement.newBuilder().setBox(mImpl).build();
+    }
+
+    /** Builder for {@link Box}. */
+    public static final class Builder implements LayoutElement.Builder {
+      private final LayoutElementProto.Box.Builder mImpl = LayoutElementProto.Box.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-1881256071);
+
+      public Builder() {}
+
+      /** Adds one item to the child element(s) to wrap. */
+      @NonNull
+      public Builder addContent(@NonNull LayoutElement content) {
+        mImpl.addContents(content.toLayoutElementProto());
+        mFingerprint.addChildNode(checkNotNull(content.getFingerprint()));
+        return this;
+      }
+
+      /**
+       * Sets the height of this {@link Box}. If not defined, this will size itself to fit all of
+       * its children (i.e. a WrappedDimension).
+       */
+      @NonNull
+      public Builder setHeight(@NonNull ContainerDimension height) {
+        mImpl.setHeight(height.toContainerDimensionProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(height.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the width of this {@link Box}. If not defined, this will size itself to fit all of its
+       * children (i.e. a WrappedDimension).
+       */
+      @NonNull
+      public Builder setWidth(@NonNull ContainerDimension width) {
+        mImpl.setWidth(width.toContainerDimensionProto());
+        mFingerprint.recordPropertyUpdate(
+            3, checkNotNull(width.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the horizontal alignment of the element inside this {@link Box}. If not defined,
+       * defaults to HORIZONTAL_ALIGN_CENTER.
+       */
+      @NonNull
+      public Builder setHorizontalAlignment(@NonNull HorizontalAlignmentProp horizontalAlignment) {
+        mImpl.setHorizontalAlignment(horizontalAlignment.toProto());
+        mFingerprint.recordPropertyUpdate(
+            4, checkNotNull(horizontalAlignment.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the horizontal alignment of the element inside this {@link Box}. If not defined,
+       * defaults to HORIZONTAL_ALIGN_CENTER.
+       */
+      @NonNull
+      public Builder setHorizontalAlignment(@HorizontalAlignment int horizontalAlignment) {
+        mImpl.setHorizontalAlignment(
+                AlignmentProto.HorizontalAlignmentProp.newBuilder()
+                        .setValue(
+                                AlignmentProto.HorizontalAlignment.forNumber(
+                                        horizontalAlignment)));
+        return this;
+      }
+
+      /**
+       * Sets the vertical alignment of the element inside this {@link Box}. If not defined,
+       * defaults to VERTICAL_ALIGN_CENTER.
+       */
+      @NonNull
+      public Builder setVerticalAlignment(@NonNull VerticalAlignmentProp verticalAlignment) {
+        mImpl.setVerticalAlignment(verticalAlignment.toProto());
+        mFingerprint.recordPropertyUpdate(
+            5, checkNotNull(verticalAlignment.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+      /**
+       * Sets the vertical alignment of the element inside this {@link Box}. If not defined,
+       * defaults to VERTICAL_ALIGN_CENTER.
+       */
+      @NonNull
+      public Builder setVerticalAlignment(@VerticalAlignment int verticalAlignment) {
+        mImpl.setVerticalAlignment(
+                AlignmentProto.VerticalAlignmentProp.newBuilder()
+                        .setValue(
+                                AlignmentProto.VerticalAlignment.forNumber(
+                                        verticalAlignment)));
+        return this;
+      }
+
+      /** Sets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. */
+      @NonNull
+      public Builder setModifiers(@NonNull Modifiers modifiers) {
+        mImpl.setModifiers(modifiers.toProto());
+        mFingerprint.recordPropertyUpdate(
+            6, checkNotNull(modifiers.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+
+      @Override
+      @NonNull
+      public Box build() {
+        return new Box(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * A portion of text which can be added to a {@link Span}. Two different {@link SpanText} elements
+   * on the same line will be aligned to the same baseline, regardless of the size of each {@link
+   * SpanText}.
+   */
+  public static final class SpanText implements Span {
+    private final LayoutElementProto.SpanText mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    SpanText(LayoutElementProto.SpanText impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the text to render. Intended for testing purposes only. */
+    @Nullable
+    public StringProp getText() {
+      if (mImpl.hasText()) {
+        return StringProp.fromProto(mImpl.getText());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the style of font to use (size, bold etc). If not specified, defaults to the platform's
+     * default body font. Intended for testing purposes only.
+     */
+    @Nullable
+    public FontStyle getFontStyle() {
+      if (mImpl.hasFontStyle()) {
+        return FontStyle.fromProto(mImpl.getFontStyle());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public SpanModifiers getModifiers() {
+      if (mImpl.hasModifiers()) {
+        return SpanModifiers.fromProto(mImpl.getModifiers());
+      } else {
+        return null;
+      }
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static SpanText fromProto(@NonNull LayoutElementProto.SpanText proto) {
+      return new SpanText(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.SpanText toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public LayoutElementProto.Span toSpanProto() {
+      return LayoutElementProto.Span.newBuilder().setText(mImpl).build();
+    }
+
+    /** Builder for {@link SpanText}. */
+    public static final class Builder implements Span.Builder {
+      private final LayoutElementProto.SpanText.Builder mImpl =
+          LayoutElementProto.SpanText.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-221774557);
+
+      public Builder() {}
+
+      /**
+       * Sets the text to render.
+       */
+      @NonNull
+      public Builder setText(@NonNull StringProp text) {
+        mImpl.setText(text.toProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(text.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+      /** Sets the text to render. */
+      @NonNull
+      public Builder setText(@NonNull String text) {
+        mImpl.setText(TypesProto.StringProp.newBuilder().setValue(text));
+        return this;
+      }
+
+      /**
+       * Sets the style of font to use (size, bold etc). If not specified, defaults to the
+       * platform's default body font.
+       */
+      @NonNull
+      public Builder setFontStyle(@NonNull FontStyle fontStyle) {
+        mImpl.setFontStyle(fontStyle.toProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(fontStyle.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Sets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. */
+      @NonNull
+      public Builder setModifiers(@NonNull SpanModifiers modifiers) {
+        mImpl.setModifiers(modifiers.toProto());
+        mFingerprint.recordPropertyUpdate(
+            3, checkNotNull(modifiers.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public SpanText build() {
+        return new SpanText(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** An image which can be added to a {@link Span}. */
+  public static final class SpanImage implements Span {
+    private final LayoutElementProto.SpanImage mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    SpanImage(LayoutElementProto.SpanImage impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the resource_id of the image to render. This must exist in the supplied resource bundle.
+     * Intended for testing purposes only.
+     */
+    @Nullable
+    public StringProp getResourceId() {
+      if (mImpl.hasResourceId()) {
+        return StringProp.fromProto(mImpl.getResourceId());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the width of this image. If not defined, the image will not be rendered. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public DpProp getWidth() {
+      if (mImpl.hasWidth()) {
+        return DpProp.fromProto(mImpl.getWidth());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the height of this image. If not defined, the image will not be rendered. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public DpProp getHeight() {
+      if (mImpl.hasHeight()) {
+        return DpProp.fromProto(mImpl.getHeight());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public SpanModifiers getModifiers() {
+      if (mImpl.hasModifiers()) {
+        return SpanModifiers.fromProto(mImpl.getModifiers());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets alignment of this image within the line height of the surrounding {@link Spannable}. If
+     * undefined, defaults to SPAN_VERTICAL_ALIGN_BOTTOM. Intended for testing purposes only.
+     */
+    @Nullable
+    public SpanVerticalAlignmentProp getAlignment() {
+      if (mImpl.hasAlignment()) {
+        return SpanVerticalAlignmentProp.fromProto(mImpl.getAlignment());
+      } else {
+        return null;
+      }
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static SpanImage fromProto(@NonNull LayoutElementProto.SpanImage proto) {
+      return new SpanImage(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.SpanImage toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public LayoutElementProto.Span toSpanProto() {
+      return LayoutElementProto.Span.newBuilder().setImage(mImpl).build();
+    }
+
+    /** Builder for {@link SpanImage}. */
+    public static final class Builder implements Span.Builder {
+      private final LayoutElementProto.SpanImage.Builder mImpl =
+          LayoutElementProto.SpanImage.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(502289772);
+
+      public Builder() {}
+
+      /**
+       * Sets the resource_id of the image to render. This must exist in the supplied resource
+       * bundle.
+       */
+      @NonNull
+      public Builder setResourceId(@NonNull StringProp resourceId) {
+        mImpl.setResourceId(resourceId.toProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(resourceId.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+      /**
+       * Sets the resource_id of the image to render. This must exist in the supplied resource
+       * bundle.
+       */
+      @NonNull
+      public Builder setResourceId(@NonNull String resourceId) {
+        mImpl.setResourceId(TypesProto.StringProp.newBuilder().setValue(resourceId));
+        return this;
+      }
+
+      /**
+       * Sets the width of this image. If not defined, the image will not be rendered.
+       */
+      @NonNull
+      public Builder setWidth(@NonNull DpProp width) {
+        mImpl.setWidth(width.toProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(width.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the height of this image. If not defined, the image will not be rendered.
+       */
+      @NonNull
+      public Builder setHeight(@NonNull DpProp height) {
+        mImpl.setHeight(height.toProto());
+        mFingerprint.recordPropertyUpdate(
+            3, checkNotNull(height.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Sets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. */
+      @NonNull
+      public Builder setModifiers(@NonNull SpanModifiers modifiers) {
+        mImpl.setModifiers(modifiers.toProto());
+        mFingerprint.recordPropertyUpdate(
+            4, checkNotNull(modifiers.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets alignment of this image within the line height of the surrounding {@link Spannable}.
+       * If undefined, defaults to SPAN_VERTICAL_ALIGN_BOTTOM.
+       */
+      @NonNull
+      public Builder setAlignment(@NonNull SpanVerticalAlignmentProp alignment) {
+        mImpl.setAlignment(alignment.toProto());
+        mFingerprint.recordPropertyUpdate(
+            5, checkNotNull(alignment.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+      /**
+       * Sets alignment of this image within the line height of the surrounding {@link
+       * Spannable}. If undefined, defaults to SPAN_VERTICAL_ALIGN_BOTTOM.
+       */
+      @NonNull
+      public Builder setAlignment(@SpanVerticalAlignment int alignment) {
+        mImpl.setAlignment(
+                LayoutElementProto.SpanVerticalAlignmentProp.newBuilder()
+                        .setValue(
+                                LayoutElementProto.SpanVerticalAlignment.forNumber(
+                                        alignment)));
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public SpanImage build() {
+        return new SpanImage(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * Interface defining a single {@link Span}. Each {@link Span} forms part of a larger {@link
+   * Spannable} widget. At the moment, the only widgets which can be added to {@link Spannable}
+   * containers are {@link SpanText} and {@link SpanImage} elements.
+   */
+  public interface Span {
+    /**
+     * Get the protocol buffer representation of this object.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    LayoutElementProto.Span toSpanProto();
+
+    /**
+     * Get the fingerprint for this object or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    Fingerprint getFingerprint();
+
+    /** Builder to create {@link Span} objects. */
+    @SuppressLint("StaticFinalBuilder")
+    interface Builder {
+
+      /** Builds an instance with values accumulated in this Builder. */
+      @NonNull
+      Span build();
+    }
+  }
+
+  @NonNull
+  static Span spanFromProto(@NonNull LayoutElementProto.Span proto) {
+    if (proto.hasText()) {
+      return SpanText.fromProto(proto.getText());
+    }
+    if (proto.hasImage()) {
+      return SpanImage.fromProto(proto.getImage());
+    }
+    throw new IllegalStateException("Proto was not a recognised instance of Span");
+  }
+
+  /**
+   * A container of {@link Span} elements. Currently, this supports {@link SpanImage} and {@link
+   * SpanText} elements, where each individual {@link Span} can have different styling applied to it
+   * but the resulting text will flow naturally. This allows sections of a paragraph of text to have
+   * different styling applied to it, for example, making one or two words bold or italic.
+   */
+  public static final class Spannable implements LayoutElement {
+    private final LayoutElementProto.Spannable mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    Spannable(LayoutElementProto.Spannable impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the {@link Span} elements that form this {@link Spannable}. Intended for testing
+     * purposes only.
+     */
+    @NonNull
+    public List<Span> getSpans() {
+      List<Span> list = new ArrayList<>();
+      for (LayoutElementProto.Span item : mImpl.getSpansList()) {
+        list.add(LayoutElementBuilders.spanFromProto(item));
+      }
+      return Collections.unmodifiableList(list);
+    }
+
+    /**
+     * Gets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public Modifiers getModifiers() {
+      if (mImpl.hasModifiers()) {
+        return Modifiers.fromProto(mImpl.getModifiers());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the maximum number of lines that can be represented by the {@link Spannable} element. If
+     * not defined, the {@link Spannable} element will be treated as a single-line element. Intended
+     * for testing purposes only.
+     */
+    @Nullable
+    public Int32Prop getMaxLines() {
+      if (mImpl.hasMaxLines()) {
+        return Int32Prop.fromProto(mImpl.getMaxLines());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets alignment of the {@link Spannable} content within its bounds. Note that a {@link
+     * Spannable} element will size itself to wrap its contents, so this option is meaningless for
+     * single-line content (for that, use alignment of the outer container). For multi-line content,
+     * however, this will set the alignment of lines relative to the {@link Spannable} element
+     * bounds. If not defined, defaults to TEXT_ALIGN_CENTER. Intended for testing purposes only.
+     */
+    @Nullable
+    public HorizontalAlignmentProp getMultilineAlignment() {
+      if (mImpl.hasMultilineAlignment()) {
+        return HorizontalAlignmentProp.fromProto(mImpl.getMultilineAlignment());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets how to handle content which overflows the bound of the {@link Spannable} element. A
+     * {@link Spannable} element will grow as large as possible inside its parent container (while
+     * still respecting max_lines); if it cannot grow large enough to render all of its content, the
+     * content which cannot fit inside its container will be truncated. If not defined, defaults to
+     * TEXT_OVERFLOW_TRUNCATE. Intended for testing purposes only.
+     */
+    @Nullable
+    public TextOverflowProp getOverflow() {
+      if (mImpl.hasOverflow()) {
+        return TextOverflowProp.fromProto(mImpl.getOverflow());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the explicit height between lines of text. This is equivalent to the vertical distance
+     * between subsequent baselines. If not specified, defaults the font's recommended interline
+     * spacing. Intended for testing purposes only.
+     */
+    @Nullable
+    public SpProp getLineHeight() {
+      if (mImpl.hasLineHeight()) {
+        return SpProp.fromProto(mImpl.getLineHeight());
+      } else {
+        return null;
+      }
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static Spannable fromProto(@NonNull LayoutElementProto.Spannable proto) {
+      return new Spannable(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.Spannable toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public LayoutElementProto.LayoutElement toLayoutElementProto() {
+      return LayoutElementProto.LayoutElement.newBuilder().setSpannable(mImpl).build();
+    }
+
+    /** Builder for {@link Spannable}. */
+    public static final class Builder implements LayoutElement.Builder {
+      private final LayoutElementProto.Spannable.Builder mImpl =
+          LayoutElementProto.Spannable.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-1690284372);
+
+      public Builder() {}
+
+      /** Adds one item to the {@link Span} elements that form this {@link Spannable}. */
+      @NonNull
+      public Builder addSpan(@NonNull Span span) {
+        mImpl.addSpans(span.toSpanProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(span.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Sets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. */
+      @NonNull
+      public Builder setModifiers(@NonNull Modifiers modifiers) {
+        mImpl.setModifiers(modifiers.toProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(modifiers.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the maximum number of lines that can be represented by the {@link Spannable} element.
+       * If not defined, the {@link Spannable} element will be treated as a single-line element.
+       */
+      @NonNull
+      public Builder setMaxLines(@NonNull Int32Prop maxLines) {
+        mImpl.setMaxLines(maxLines.toProto());
+        mFingerprint.recordPropertyUpdate(
+            3, checkNotNull(maxLines.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+      /**
+       * Sets the maximum number of lines that can be represented by the {@link Spannable}
+       * element. If not defined, the {@link Spannable} element will be treated as a
+       * single-line element.
+       */
+      @NonNull
+      public Builder setMaxLines(@IntRange(from = 1) int maxLines) {
+        mImpl.setMaxLines(TypesProto.Int32Prop.newBuilder().setValue(maxLines));
+        return this;
+      }
+
+      /**
+       * Sets alignment of the {@link Spannable} content within its bounds. Note that a {@link
+       * Spannable} element will size itself to wrap its contents, so this option is meaningless for
+       * single-line content (for that, use alignment of the outer container). For multi-line
+       * content, however, this will set the alignment of lines relative to the {@link Spannable}
+       * element bounds. If not defined, defaults to TEXT_ALIGN_CENTER.
+       */
+      @NonNull
+      public Builder setMultilineAlignment(@NonNull HorizontalAlignmentProp multilineAlignment) {
+        mImpl.setMultilineAlignment(multilineAlignment.toProto());
+        mFingerprint.recordPropertyUpdate(
+            4, checkNotNull(multilineAlignment.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+      /**
+       * Sets alignment of the {@link Spannable} content within its bounds. Note that a {@link
+       * Spannable} element will size itself to wrap its contents, so this option is
+       * meaningless for single-line content (for that, use alignment of the outer container).
+       * For multi-line content, however, this will set the alignment of lines relative to the
+       * {@link Spannable} element bounds. If not defined, defaults to TEXT_ALIGN_CENTER.
+       */
+      @NonNull
+      public Builder setMultilineAlignment(@HorizontalAlignment int multilineAlignment) {
+        mImpl.setMultilineAlignment(
+                AlignmentProto.HorizontalAlignmentProp.newBuilder()
+                        .setValue(
+                                AlignmentProto.HorizontalAlignment.forNumber(
+                                        multilineAlignment)));
+        return this;
+      }
+
+      /**
+       * Sets how to handle content which overflows the bound of the {@link Spannable} element. A
+       * {@link Spannable} element will grow as large as possible inside its parent container (while
+       * still respecting max_lines); if it cannot grow large enough to render all of its content,
+       * the content which cannot fit inside its container will be truncated. If not defined,
+       * defaults to TEXT_OVERFLOW_TRUNCATE.
+       */
+      @NonNull
+      public Builder setOverflow(@NonNull TextOverflowProp overflow) {
+        mImpl.setOverflow(overflow.toProto());
+        mFingerprint.recordPropertyUpdate(
+            5, checkNotNull(overflow.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+      /**
+       * Sets how to handle content which overflows the bound of the {@link Spannable}
+       * element. A {@link Spannable} element will grow as large as possible inside its parent
+       * container (while still respecting max_lines); if it cannot grow large enough to
+       * render all of its content, the content which cannot fit inside its container will be
+       * truncated. If not defined, defaults to TEXT_OVERFLOW_TRUNCATE.
+       */
+      @NonNull
+      public Builder setOverflow(@TextOverflow int overflow) {
+        mImpl.setOverflow(
+                LayoutElementProto.TextOverflowProp.newBuilder()
+                        .setValue(LayoutElementProto.TextOverflow.forNumber(overflow)));
+        return this;
+      }
+
+      /**
+       * Sets the explicit height between lines of text. This is equivalent to the vertical distance
+       * between subsequent baselines. If not specified, defaults the font's recommended interline
+       * spacing.
+       */
+      @NonNull
+      public Builder setLineHeight(@NonNull SpProp lineHeight) {
+        mImpl.setLineHeight(lineHeight.toProto());
+        mFingerprint.recordPropertyUpdate(
+            7, checkNotNull(lineHeight.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public Spannable build() {
+        return new Spannable(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * A column of elements. Each child element will be laid out vertically, one after another (i.e.
+   * stacking down). This element will size itself to the smallest size required to hold all of its
+   * children (e.g. if it contains three elements sized 10x10, 20x20 and 30x30, the resulting column
+   * will be 30x60).
+   *
+   * <p>If specified, horizontal_alignment can be used to control the gravity inside the container,
+   * affecting the horizontal placement of children whose width are smaller than the resulting
+   * column width.
+   */
+  public static final class Column implements LayoutElement {
+    private final LayoutElementProto.Column mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    Column(LayoutElementProto.Column impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the list of child elements to place inside this {@link Column}. Intended for testing
+     * purposes only.
+     */
+    @NonNull
+    public List<LayoutElement> getContents() {
+      List<LayoutElement> list = new ArrayList<>();
+      for (LayoutElementProto.LayoutElement item : mImpl.getContentsList()) {
+        list.add(LayoutElementBuilders.layoutElementFromProto(item));
+      }
+      return Collections.unmodifiableList(list);
+    }
+
+    /**
+     * Gets the horizontal alignment of elements inside this column, if they are narrower than the
+     * resulting width of the column. If not defined, defaults to HORIZONTAL_ALIGN_CENTER. Intended
+     * for testing purposes only.
+     */
+    @Nullable
+    public HorizontalAlignmentProp getHorizontalAlignment() {
+      if (mImpl.hasHorizontalAlignment()) {
+        return HorizontalAlignmentProp.fromProto(mImpl.getHorizontalAlignment());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the width of this column. If not defined, this will size itself to fit all of its
+     * children (i.e. a WrappedDimension). Intended for testing purposes only.
+     */
+    @Nullable
+    public ContainerDimension getWidth() {
+      if (mImpl.hasWidth()) {
+        return DimensionBuilders.containerDimensionFromProto(mImpl.getWidth());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the height of this column. If not defined, this will size itself to fit all of its
+     * children (i.e. a WrappedDimension). Intended for testing purposes only.
+     */
+    @Nullable
+    public ContainerDimension getHeight() {
+      if (mImpl.hasHeight()) {
+        return DimensionBuilders.containerDimensionFromProto(mImpl.getHeight());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public Modifiers getModifiers() {
+      if (mImpl.hasModifiers()) {
+        return Modifiers.fromProto(mImpl.getModifiers());
+      } else {
+        return null;
+      }
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static Column fromProto(@NonNull LayoutElementProto.Column proto) {
+      return new Column(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.Column toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public LayoutElementProto.LayoutElement toLayoutElementProto() {
+      return LayoutElementProto.LayoutElement.newBuilder().setColumn(mImpl).build();
+    }
+
+    /** Builder for {@link Column}. */
+    public static final class Builder implements LayoutElement.Builder {
+      private final LayoutElementProto.Column.Builder mImpl =
+          LayoutElementProto.Column.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-1411218529);
+
+      public Builder() {}
+
+      /** Adds one item to the list of child elements to place inside this {@link Column}. */
+      @NonNull
+      public Builder addContent(@NonNull LayoutElement content) {
+        mImpl.addContents(content.toLayoutElementProto());
+        mFingerprint.addChildNode(checkNotNull(content.getFingerprint()));
+        return this;
+      }
+
+      /**
+       * Sets the horizontal alignment of elements inside this column, if they are narrower than the
+       * resulting width of the column. If not defined, defaults to HORIZONTAL_ALIGN_CENTER.
+       */
+      @NonNull
+      public Builder setHorizontalAlignment(@NonNull HorizontalAlignmentProp horizontalAlignment) {
+        mImpl.setHorizontalAlignment(horizontalAlignment.toProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(horizontalAlignment.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the horizontal alignment of elements inside this column, if they are narrower
+       * than the resulting width of the column. If not defined, defaults to
+       * HORIZONTAL_ALIGN_CENTER.
+       */
+      @NonNull
+      public Builder setHorizontalAlignment(@HorizontalAlignment int horizontalAlignment) {
+        mImpl.setHorizontalAlignment(
+                AlignmentProto.HorizontalAlignmentProp.newBuilder()
+                        .setValue(
+                                AlignmentProto.HorizontalAlignment.forNumber(
+                                        horizontalAlignment)));
+        return this;
+      }
+
+      /**
+       * Sets the width of this column. If not defined, this will size itself to fit all of its
+       * children (i.e. a WrappedDimension).
+       */
+      @NonNull
+      public Builder setWidth(@NonNull ContainerDimension width) {
+        mImpl.setWidth(width.toContainerDimensionProto());
+        mFingerprint.recordPropertyUpdate(
+            3, checkNotNull(width.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the height of this column. If not defined, this will size itself to fit all of its
+       * children (i.e. a WrappedDimension).
+       */
+      @NonNull
+      public Builder setHeight(@NonNull ContainerDimension height) {
+        mImpl.setHeight(height.toContainerDimensionProto());
+        mFingerprint.recordPropertyUpdate(
+            4, checkNotNull(height.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Sets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. */
+      @NonNull
+      public Builder setModifiers(@NonNull Modifiers modifiers) {
+        mImpl.setModifiers(modifiers.toProto());
+        mFingerprint.recordPropertyUpdate(
+            5, checkNotNull(modifiers.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+
+      @Override
+      @NonNull
+      public Column build() {
+        return new Column(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * A row of elements. Each child will be laid out horizontally, one after another (i.e. stacking
+   * to the right). This element will size itself to the smallest size required to hold all of its
+   * children (e.g. if it contains three elements sized 10x10, 20x20 and 30x30, the resulting row
+   * will be 60x30).
+   *
+   * <p>If specified, vertical_alignment can be used to control the gravity inside the container,
+   * affecting the vertical placement of children whose width are smaller than the resulting row
+   * height.
+   */
+  public static final class Row implements LayoutElement {
+    private final LayoutElementProto.Row mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    Row(LayoutElementProto.Row impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the list of child elements to place inside this {@link Row}. Intended for testing
+     * purposes only.
+     */
+    @NonNull
+    public List<LayoutElement> getContents() {
+      List<LayoutElement> list = new ArrayList<>();
+      for (LayoutElementProto.LayoutElement item : mImpl.getContentsList()) {
+        list.add(LayoutElementBuilders.layoutElementFromProto(item));
+      }
+      return Collections.unmodifiableList(list);
+    }
+
+    /**
+     * Gets the vertical alignment of elements inside this row, if they are narrower than the
+     * resulting height of the row. If not defined, defaults to VERTICAL_ALIGN_CENTER. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public VerticalAlignmentProp getVerticalAlignment() {
+      if (mImpl.hasVerticalAlignment()) {
+        return VerticalAlignmentProp.fromProto(mImpl.getVerticalAlignment());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the width of this row. If not defined, this will size itself to fit all of its children
+     * (i.e. a WrappedDimension). Intended for testing purposes only.
+     */
+    @Nullable
+    public ContainerDimension getWidth() {
+      if (mImpl.hasWidth()) {
+        return DimensionBuilders.containerDimensionFromProto(mImpl.getWidth());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the height of this row. If not defined, this will size itself to fit all of its children
+     * (i.e. a WrappedDimension). Intended for testing purposes only.
+     */
+    @Nullable
+    public ContainerDimension getHeight() {
+      if (mImpl.hasHeight()) {
+        return DimensionBuilders.containerDimensionFromProto(mImpl.getHeight());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public Modifiers getModifiers() {
+      if (mImpl.hasModifiers()) {
+        return Modifiers.fromProto(mImpl.getModifiers());
+      } else {
+        return null;
+      }
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static Row fromProto(@NonNull LayoutElementProto.Row proto) {
+      return new Row(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.Row toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public LayoutElementProto.LayoutElement toLayoutElementProto() {
+      return LayoutElementProto.LayoutElement.newBuilder().setRow(mImpl).build();
+    }
+
+    /** Builder for {@link Row}. */
+    public static final class Builder implements LayoutElement.Builder {
+      private final LayoutElementProto.Row.Builder mImpl = LayoutElementProto.Row.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(1537205448);
+
+      public Builder() {}
+
+      /** Adds one item to the list of child elements to place inside this {@link Row}. */
+      @NonNull
+      public Builder addContent(@NonNull LayoutElement content) {
+        mImpl.addContents(content.toLayoutElementProto());
+        mFingerprint.addChildNode(checkNotNull(content.getFingerprint()));
+        return this;
+      }
+
+      /**
+       * Sets the vertical alignment of elements inside this row, if they are narrower than the
+       * resulting height of the row. If not defined, defaults to VERTICAL_ALIGN_CENTER.
+       */
+      @NonNull
+      public Builder setVerticalAlignment(@NonNull VerticalAlignmentProp verticalAlignment) {
+        mImpl.setVerticalAlignment(verticalAlignment.toProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(verticalAlignment.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the vertical alignment of elements inside this row, if they are narrower than
+       * the resulting height of the row. If not defined, defaults to VERTICAL_ALIGN_CENTER.
+       */
+      @NonNull
+      public Builder setVerticalAlignment(@VerticalAlignment int verticalAlignment) {
+        mImpl.setVerticalAlignment(
+                AlignmentProto.VerticalAlignmentProp.newBuilder()
+                        .setValue(
+                                AlignmentProto.VerticalAlignment.forNumber(
+                                        verticalAlignment)));
+        return this;
+      }
+
+      /**
+       * Sets the width of this row. If not defined, this will size itself to fit all of its
+       * children (i.e. a WrappedDimension).
+       */
+      @NonNull
+      public Builder setWidth(@NonNull ContainerDimension width) {
+        mImpl.setWidth(width.toContainerDimensionProto());
+        mFingerprint.recordPropertyUpdate(
+            3, checkNotNull(width.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the height of this row. If not defined, this will size itself to fit all of its
+       * children (i.e. a WrappedDimension).
+       */
+      @NonNull
+      public Builder setHeight(@NonNull ContainerDimension height) {
+        mImpl.setHeight(height.toContainerDimensionProto());
+        mFingerprint.recordPropertyUpdate(
+            4, checkNotNull(height.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Sets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. */
+      @NonNull
+      public Builder setModifiers(@NonNull Modifiers modifiers) {
+        mImpl.setModifiers(modifiers.toProto());
+        mFingerprint.recordPropertyUpdate(
+            5, checkNotNull(modifiers.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+
+      @Override
+      @NonNull
+      public Row build() {
+        return new Row(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * An arc container. This container will fill itself to a circle, which fits inside its parent
+   * container, and all of its children will be placed on that circle. The fields anchor_angle and
+   * anchor_type can be used to specify where to draw children within this circle.
+   */
+  public static final class Arc implements LayoutElement {
+    private final LayoutElementProto.Arc mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    Arc(LayoutElementProto.Arc impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets contents of this container. Intended for testing purposes only. */
+    @NonNull
+    public List<ArcLayoutElement> getContents() {
+      List<ArcLayoutElement> list = new ArrayList<>();
+      for (LayoutElementProto.ArcLayoutElement item : mImpl.getContentsList()) {
+        list.add(LayoutElementBuilders.arcLayoutElementFromProto(item));
+      }
+      return Collections.unmodifiableList(list);
+    }
+
+    /**
+     * Gets the angle for the anchor, used with anchor_type to determine where to draw children.
+     * Note that 0 degrees is the 12 o clock position on a device, and the angle sweeps clockwise.
+     * If not defined, defaults to 0 degrees.
+     *
+     * <p>Values do not have to be clamped to the range 0-360; values less than 0 degrees will sweep
+     * anti-clockwise (i.e. -90 degrees is equivalent to 270 degrees), and values >360 will be be
+     * placed at X mod 360 degrees. Intended for testing purposes only.
+     */
+    @Nullable
+    public DegreesProp getAnchorAngle() {
+      if (mImpl.hasAnchorAngle()) {
+        return DegreesProp.fromProto(mImpl.getAnchorAngle());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets how to align the contents of this container relative to anchor_angle. If not defined,
+     * defaults to ARC_ANCHOR_CENTER. Intended for testing purposes only.
+     */
+    @Nullable
+    public ArcAnchorTypeProp getAnchorType() {
+      if (mImpl.hasAnchorType()) {
+        return ArcAnchorTypeProp.fromProto(mImpl.getAnchorType());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets vertical alignment of elements within the arc. If the {@link Arc}'s thickness is larger
+     * than the thickness of the element being drawn, this controls whether the element should be
+     * drawn towards the inner or outer edge of the arc, or drawn in the center. If not defined,
+     * defaults to VERTICAL_ALIGN_CENTER. Intended for testing purposes only.
+     */
+    @Nullable
+    public VerticalAlignmentProp getVerticalAlign() {
+      if (mImpl.hasVerticalAlign()) {
+        return VerticalAlignmentProp.fromProto(mImpl.getVerticalAlign());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public Modifiers getModifiers() {
+      if (mImpl.hasModifiers()) {
+        return Modifiers.fromProto(mImpl.getModifiers());
+      } else {
+        return null;
+      }
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static Arc fromProto(@NonNull LayoutElementProto.Arc proto) {
+      return new Arc(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.Arc toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public LayoutElementProto.LayoutElement toLayoutElementProto() {
+      return LayoutElementProto.LayoutElement.newBuilder().setArc(mImpl).build();
+    }
+
+    /** Builder for {@link Arc}. */
+    public static final class Builder implements LayoutElement.Builder {
+      private final LayoutElementProto.Arc.Builder mImpl = LayoutElementProto.Arc.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(299028337);
+
+      public Builder() {}
+
+      /** Adds one item to contents of this container. */
+      @NonNull
+      public Builder addContent(@NonNull ArcLayoutElement content) {
+        mImpl.addContents(content.toArcLayoutElementProto());
+        mFingerprint.addChildNode(checkNotNull(content.getFingerprint()));
+        return this;
+      }
+
+      /**
+       * Sets the angle for the anchor, used with anchor_type to determine where to draw children.
+       * Note that 0 degrees is the 12 o clock position on a device, and the angle sweeps clockwise.
+       * If not defined, defaults to 0 degrees.
+       *
+       * <p>Values do not have to be clamped to the range 0-360; values less than 0 degrees will
+       * sweep anti-clockwise (i.e. -90 degrees is equivalent to 270 degrees), and values >360 will
+       * be be placed at X mod 360 degrees.
+       */
+      @NonNull
+      public Builder setAnchorAngle(@NonNull DegreesProp anchorAngle) {
+        mImpl.setAnchorAngle(anchorAngle.toProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(anchorAngle.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets how to align the contents of this container relative to anchor_angle. If not defined,
+       * defaults to ARC_ANCHOR_CENTER.
+       */
+      @NonNull
+      public Builder setAnchorType(@NonNull ArcAnchorTypeProp anchorType) {
+        mImpl.setAnchorType(anchorType.toProto());
+        mFingerprint.recordPropertyUpdate(
+            3, checkNotNull(anchorType.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets how to align the contents of this container relative to anchor_angle. If not
+       * defined, defaults to ARC_ANCHOR_CENTER.
+       */
+      @NonNull
+      public Builder setAnchorType(@ArcAnchorType int anchorType) {
+        mImpl.setAnchorType(
+                AlignmentProto.ArcAnchorTypeProp.newBuilder()
+                        .setValue(AlignmentProto.ArcAnchorType.forNumber(anchorType)));
+        return this;
+      }
+
+      /**
+       * Sets vertical alignment of elements within the arc. If the {@link Arc}'s thickness is
+       * larger than the thickness of the element being drawn, this controls whether the element
+       * should be drawn towards the inner or outer edge of the arc, or drawn in the center. If not
+       * defined, defaults to VERTICAL_ALIGN_CENTER.
+       */
+      @NonNull
+      public Builder setVerticalAlign(@NonNull VerticalAlignmentProp verticalAlign) {
+        mImpl.setVerticalAlign(verticalAlign.toProto());
+        mFingerprint.recordPropertyUpdate(
+            4, checkNotNull(verticalAlign.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+      /**
+       * Sets vertical alignment of elements within the arc. If the {@link Arc}'s thickness is
+       * larger than the thickness of the element being drawn, this controls whether the
+       * element should be drawn towards the inner or outer edge of the arc, or drawn in the
+       * center. If not defined, defaults to VERTICAL_ALIGN_CENTER.
+       */
+      @NonNull
+      public Builder setVerticalAlign(@VerticalAlignment int verticalAlign) {
+        mImpl.setVerticalAlign(
+                AlignmentProto.VerticalAlignmentProp.newBuilder()
+                        .setValue(
+                                AlignmentProto.VerticalAlignment.forNumber(
+                                        verticalAlign)));
+        return this;
+      }
+
+      /** Sets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. */
+      @NonNull
+      public Builder setModifiers(@NonNull Modifiers modifiers) {
+        mImpl.setModifiers(modifiers.toProto());
+        mFingerprint.recordPropertyUpdate(
+            5, checkNotNull(modifiers.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+
+      @Override
+      @NonNull
+      public Arc build() {
+        return new Arc(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** A text element that can be used in an {@link Arc}. */
+  public static final class ArcText implements ArcLayoutElement {
+    private final LayoutElementProto.ArcText mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    ArcText(LayoutElementProto.ArcText impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the text to render. Intended for testing purposes only. */
+    @Nullable
+    public StringProp getText() {
+      if (mImpl.hasText()) {
+        return StringProp.fromProto(mImpl.getText());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the style of font to use (size, bold etc). If not specified, defaults to the platform's
+     * default body font. Intended for testing purposes only.
+     */
+    @Nullable
+    public FontStyle getFontStyle() {
+      if (mImpl.hasFontStyle()) {
+        return FontStyle.fromProto(mImpl.getFontStyle());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public ArcModifiers getModifiers() {
+      if (mImpl.hasModifiers()) {
+        return ArcModifiers.fromProto(mImpl.getModifiers());
+      } else {
+        return null;
+      }
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static ArcText fromProto(@NonNull LayoutElementProto.ArcText proto) {
+      return new ArcText(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.ArcText toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public LayoutElementProto.ArcLayoutElement toArcLayoutElementProto() {
+      return LayoutElementProto.ArcLayoutElement.newBuilder().setText(mImpl).build();
+    }
+
+    /** Builder for {@link ArcText}. */
+    public static final class Builder implements ArcLayoutElement.Builder {
+      private final LayoutElementProto.ArcText.Builder mImpl =
+          LayoutElementProto.ArcText.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(434391973);
+
+      public Builder() {}
+
+      /**
+       * Sets the text to render.
+       */
+      @NonNull
+      public Builder setText(@NonNull StringProp text) {
+        mImpl.setText(text.toProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(text.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Sets the text to render. */
+      @NonNull
+      public Builder setText(@NonNull String text) {
+        mImpl.setText(TypesProto.StringProp.newBuilder().setValue(text));
+        return this;
+      }
+
+      /**
+       * Sets the style of font to use (size, bold etc). If not specified, defaults to the
+       * platform's default body font.
+       */
+      @NonNull
+      public Builder setFontStyle(@NonNull FontStyle fontStyle) {
+        mImpl.setFontStyle(fontStyle.toProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(fontStyle.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Sets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. */
+      @NonNull
+      public Builder setModifiers(@NonNull ArcModifiers modifiers) {
+        mImpl.setModifiers(modifiers.toProto());
+        mFingerprint.recordPropertyUpdate(
+            3, checkNotNull(modifiers.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+
+      @Override
+      @NonNull
+      public ArcText build() {
+        return new ArcText(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** A line that can be used in an {@link Arc} and renders as a round progress bar. */
+  public static final class ArcLine implements ArcLayoutElement {
+    private final LayoutElementProto.ArcLine mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    ArcLine(LayoutElementProto.ArcLine impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the length of this line, in degrees. If not defined, defaults to 0. Intended for testing
+     * purposes only.
+     */
+    @Nullable
+    public DegreesProp getLength() {
+      if (mImpl.hasLength()) {
+        return DegreesProp.fromProto(mImpl.getLength());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the thickness of this line. If not defined, defaults to 0. Intended for testing purposes
+     * only.
+     */
+    @Nullable
+    public DpProp getThickness() {
+      if (mImpl.hasThickness()) {
+        return DpProp.fromProto(mImpl.getThickness());
+      } else {
+        return null;
+      }
+    }
+
+    /** Gets the color of this line. Intended for testing purposes only. */
+    @Nullable
+    public ColorProp getColor() {
+      if (mImpl.hasColor()) {
+        return ColorProp.fromProto(mImpl.getColor());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public ArcModifiers getModifiers() {
+      if (mImpl.hasModifiers()) {
+        return ArcModifiers.fromProto(mImpl.getModifiers());
+      } else {
+        return null;
+      }
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static ArcLine fromProto(@NonNull LayoutElementProto.ArcLine proto) {
+      return new ArcLine(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.ArcLine toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public LayoutElementProto.ArcLayoutElement toArcLayoutElementProto() {
+      return LayoutElementProto.ArcLayoutElement.newBuilder().setLine(mImpl).build();
+    }
+
+    /** Builder for {@link ArcLine}. */
+    public static final class Builder implements ArcLayoutElement.Builder {
+      private final LayoutElementProto.ArcLine.Builder mImpl =
+          LayoutElementProto.ArcLine.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-1371793535);
+
+      public Builder() {}
+
+      /**
+       * Sets the length of this line, in degrees. If not defined, defaults to 0.
+       */
+      @NonNull
+      public Builder setLength(@NonNull DegreesProp length) {
+        mImpl.setLength(length.toProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(length.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the thickness of this line. If not defined, defaults to 0.
+       */
+      @NonNull
+      public Builder setThickness(@NonNull DpProp thickness) {
+        mImpl.setThickness(thickness.toProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(thickness.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the color of this line.
+       */
+      @NonNull
+      public Builder setColor(@NonNull ColorProp color) {
+        mImpl.setColor(color.toProto());
+        mFingerprint.recordPropertyUpdate(
+            3, checkNotNull(color.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Sets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. */
+      @NonNull
+      public Builder setModifiers(@NonNull ArcModifiers modifiers) {
+        mImpl.setModifiers(modifiers.toProto());
+        mFingerprint.recordPropertyUpdate(
+            4, checkNotNull(modifiers.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+
+      @Override
+      @NonNull
+      public ArcLine build() {
+        return new ArcLine(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** A simple spacer used to provide padding between adjacent elements in an {@link Arc}. */
+  public static final class ArcSpacer implements ArcLayoutElement {
+    private final LayoutElementProto.ArcSpacer mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    ArcSpacer(LayoutElementProto.ArcSpacer impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the length of this spacer, in degrees. If not defined, defaults to 0. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public DegreesProp getLength() {
+      if (mImpl.hasLength()) {
+        return DegreesProp.fromProto(mImpl.getLength());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the thickness of this spacer, in DP. If not defined, defaults to 0. Intended for testing
+     * purposes only.
+     */
+    @Nullable
+    public DpProp getThickness() {
+      if (mImpl.hasThickness()) {
+        return DpProp.fromProto(mImpl.getThickness());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public ArcModifiers getModifiers() {
+      if (mImpl.hasModifiers()) {
+        return ArcModifiers.fromProto(mImpl.getModifiers());
+      } else {
+        return null;
+      }
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static ArcSpacer fromProto(@NonNull LayoutElementProto.ArcSpacer proto) {
+      return new ArcSpacer(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.ArcSpacer toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public LayoutElementProto.ArcLayoutElement toArcLayoutElementProto() {
+      return LayoutElementProto.ArcLayoutElement.newBuilder().setSpacer(mImpl).build();
+    }
+
+    /** Builder for {@link ArcSpacer}. */
+    public static final class Builder implements ArcLayoutElement.Builder {
+      private final LayoutElementProto.ArcSpacer.Builder mImpl =
+          LayoutElementProto.ArcSpacer.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-179760535);
+
+      public Builder() {}
+
+      /**
+       * Sets the length of this spacer, in degrees. If not defined, defaults to 0.
+       */
+      @NonNull
+      public Builder setLength(@NonNull DegreesProp length) {
+        mImpl.setLength(length.toProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(length.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the thickness of this spacer, in DP. If not defined, defaults to 0.
+       */
+      @NonNull
+      public Builder setThickness(@NonNull DpProp thickness) {
+        mImpl.setThickness(thickness.toProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(thickness.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Sets {@link androidx.wear.tiles.ModifiersBuilders.Modifiers} for this element. */
+      @NonNull
+      public Builder setModifiers(@NonNull ArcModifiers modifiers) {
+        mImpl.setModifiers(modifiers.toProto());
+        mFingerprint.recordPropertyUpdate(
+            3, checkNotNull(modifiers.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public ArcSpacer build() {
+        return new ArcSpacer(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** A container that allows a standard {@link LayoutElement} to be added to an {@link Arc}. */
+  public static final class ArcAdapter implements ArcLayoutElement {
+    private final LayoutElementProto.ArcAdapter mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    ArcAdapter(LayoutElementProto.ArcAdapter impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the element to adapt to an {@link Arc}. Intended for testing purposes only. */
+    @Nullable
+    public LayoutElement getContent() {
+      if (mImpl.hasContent()) {
+        return LayoutElementBuilders.layoutElementFromProto(mImpl.getContent());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets whether this adapter's contents should be rotated, according to its position in the arc
+     * or not. As an example, assume that an {@link Image} has been added to the arc, and ends up at
+     * the 3 o clock position. If rotate_contents = true, the image will be placed at the 3 o clock
+     * position, and will be rotated clockwise through 90 degrees. If rotate_contents = false, the
+     * image will be placed at the 3 o clock position, but itself will not be rotated. If not
+     * defined, defaults to false. Intended for testing purposes only.
+     */
+    @Nullable
+    public BoolProp getRotateContents() {
+      if (mImpl.hasRotateContents()) {
+        return BoolProp.fromProto(mImpl.getRotateContents());
+      } else {
+        return null;
+      }
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static ArcAdapter fromProto(@NonNull LayoutElementProto.ArcAdapter proto) {
+      return new ArcAdapter(proto, null);
+    }
+
+    @NonNull
+    LayoutElementProto.ArcAdapter toProto() {
+      return mImpl;
+    }
+
+    /** @hide */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public LayoutElementProto.ArcLayoutElement toArcLayoutElementProto() {
+      return LayoutElementProto.ArcLayoutElement.newBuilder().setAdapter(mImpl).build();
+    }
+
+    /** Builder for {@link ArcAdapter}. */
+    public static final class Builder implements ArcLayoutElement.Builder {
+      private final LayoutElementProto.ArcAdapter.Builder mImpl =
+          LayoutElementProto.ArcAdapter.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(1696473935);
+
+      public Builder() {}
+
+      /** Sets the element to adapt to an {@link Arc}. */
+      @NonNull
+      public Builder setContent(@NonNull LayoutElement content) {
+        mImpl.setContent(content.toLayoutElementProto());
+        mFingerprint.addChildNode(checkNotNull(content.getFingerprint()));
+        return this;
+      }
+
+      /**
+       * Sets whether this adapter's contents should be rotated, according to its position in the
+       * arc or not. As an example, assume that an {@link Image} has been added to the arc, and ends
+       * up at the 3 o clock position. If rotate_contents = true, the image will be placed at the 3
+       * o clock position, and will be rotated clockwise through 90 degrees. If rotate_contents =
+       * false, the image will be placed at the 3 o clock position, but itself will not be rotated.
+       * If not defined, defaults to false.
+       */
+      @NonNull
+      public Builder setRotateContents(@NonNull BoolProp rotateContents) {
+        mImpl.setRotateContents(rotateContents.toProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(rotateContents.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets whether this adapter's contents should be rotated, according to its position in
+       * the arc or not. As an example, assume that an {@link Image} has been added to the
+       * arc, and ends up at the 3 o clock position. If rotate_contents = true, the image will
+       * be placed at the 3 o clock position, and will be rotated clockwise through 90
+       * degrees. If rotate_contents = false, the image will be placed at the 3 o clock
+       * position, but itself will not be rotated. If not defined, defaults to false.
+       */
+      @SuppressLint("MissingGetterMatchingBuilder")
+      @NonNull
+      public Builder setRotateContents(boolean rotateContents) {
+        mImpl.setRotateContents(TypesProto.BoolProp.newBuilder().setValue(rotateContents));
+        return this;
+      }
+
+      @Override
+      @NonNull
+      public ArcAdapter build() {
+        return new ArcAdapter(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * Interface defining the root of all layout elements. This exists to act as a holder for all of
+   * the actual layout elements above.
+   */
+  public interface LayoutElement {
+    /**
+     * Get the protocol buffer representation of this object.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    LayoutElementProto.LayoutElement toLayoutElementProto();
+
+    /**
+     * Get the fingerprint for this object or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    Fingerprint getFingerprint();
+
+    /** Builder to create {@link LayoutElement} objects. */
+    @SuppressLint("StaticFinalBuilder")
+    interface Builder {
+
+      /** Builds an instance with values accumulated in this Builder. */
+      @NonNull
+      LayoutElement build();
+    }
+  }
+
+  @NonNull
+  static LayoutElement layoutElementFromProto(@NonNull LayoutElementProto.LayoutElement proto) {
+    if (proto.hasColumn()) {
+      return Column.fromProto(proto.getColumn());
+    }
+    if (proto.hasRow()) {
+      return Row.fromProto(proto.getRow());
+    }
+    if (proto.hasBox()) {
+      return Box.fromProto(proto.getBox());
+    }
+    if (proto.hasSpacer()) {
+      return Spacer.fromProto(proto.getSpacer());
+    }
+    if (proto.hasText()) {
+      return Text.fromProto(proto.getText());
+    }
+    if (proto.hasImage()) {
+      return Image.fromProto(proto.getImage());
+    }
+    if (proto.hasArc()) {
+      return Arc.fromProto(proto.getArc());
+    }
+    if (proto.hasSpannable()) {
+      return Spannable.fromProto(proto.getSpannable());
+    }
+    throw new IllegalStateException("Proto was not a recognised instance of LayoutElement");
+  }
+
+  /**
+   * Interface defining the root of all elements that can be used in an {@link Arc}. This exists to
+   * act as a holder for all of the actual arc layout elements above.
+   */
+  public interface ArcLayoutElement {
+    /**
+     * Get the protocol buffer representation of this object.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    LayoutElementProto.ArcLayoutElement toArcLayoutElementProto();
+
+    /**
+     * Get the fingerprint for this object or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    Fingerprint getFingerprint();
+
+    /** Builder to create {@link ArcLayoutElement} objects. */
+    @SuppressLint("StaticFinalBuilder")
+    interface Builder {
+
+      /** Builds an instance with values accumulated in this Builder. */
+      @NonNull
+      ArcLayoutElement build();
+    }
+  }
+
+  @NonNull
+  static ArcLayoutElement arcLayoutElementFromProto(
+      @NonNull LayoutElementProto.ArcLayoutElement proto) {
+    if (proto.hasText()) {
+      return ArcText.fromProto(proto.getText());
+    }
+    if (proto.hasLine()) {
+      return ArcLine.fromProto(proto.getLine());
+    }
+    if (proto.hasSpacer()) {
+      return ArcSpacer.fromProto(proto.getSpacer());
+    }
+    if (proto.hasAdapter()) {
+      return ArcAdapter.fromProto(proto.getAdapter());
+    }
+    throw new IllegalStateException("Proto was not a recognised instance of ArcLayoutElement");
+  }
+
+  /** A complete layout. */
+  public static final class Layout {
+    private final LayoutElementProto.Layout mImpl;
+
+    private Layout(LayoutElementProto.Layout impl) {
+      this.mImpl = impl;
+    }
+
+    /** Gets the root element in the layout. Intended for testing purposes only. */
+    @Nullable
+    public LayoutElement getRoot() {
+      if (mImpl.hasRoot()) {
+        return LayoutElementBuilders.layoutElementFromProto(mImpl.getRoot());
+      } else {
+        return null;
+      }
+    }
+
+    /** Creates a {@link Layout} object containing the given layout element. */
+    @NonNull
+    public static Layout fromLayoutElement(@NonNull LayoutElement layoutElement) {
+      return new Builder().setRoot(layoutElement).build();
+    }
+
+    /** Converts to byte array representation. */
+    @NonNull
+    @ProtoLayoutExperimental
+    public byte[] toByteArray() {
+      return mImpl.toByteArray();
+    }
+
+    /** Converts from byte array representation. */
+    @SuppressWarnings("ProtoParseWithRegistry")
+    @Nullable
+    @ProtoLayoutExperimental
+    public static Layout fromByteArray(@NonNull byte[] byteArray) {
+      try {
+        return fromProto(LayoutElementProto.Layout.parseFrom(byteArray));
+      } catch (InvalidProtocolBufferException e) {
+        return null;
+      }
+    }
+
+    @NonNull
+    static Layout fromProto(@NonNull LayoutElementProto.Layout proto) {
+      return new Layout(proto);
+    }
+
+    /**
+     * Returns the internal proto instance.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public LayoutElementProto.Layout toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link Layout} */
+    public static final class Builder {
+      private final LayoutElementProto.Layout.Builder mImpl =
+          LayoutElementProto.Layout.newBuilder();
+
+      public Builder() {}
+
+      /** Sets the root element in the layout. */
+      @NonNull
+      public Builder setRoot(@NonNull LayoutElement root) {
+        mImpl.setRoot(root.toLayoutElementProto());
+        @Nullable Fingerprint fingerprint = root.getFingerprint();
+        if (fingerprint != null) {
+          mImpl.setFingerprint(
+              TreeFingerprint.newBuilder().setRoot(fingerprintToProto(fingerprint)));
+        }
+        return this;
+      }
+
+      private static FingerprintProto.NodeFingerprint fingerprintToProto(Fingerprint fingerprint) {
+        FingerprintProto.NodeFingerprint.Builder builder =
+            FingerprintProto.NodeFingerprint.newBuilder();
+        if (fingerprint.selfTypeValue() != 0) {
+          builder.setSelfTypeValue(fingerprint.selfTypeValue());
+        }
+        if (fingerprint.selfPropsValue() != 0) {
+          builder.setSelfPropsValue(fingerprint.selfPropsValue());
+        }
+        if (fingerprint.childNodesValue() != 0) {
+          builder.setChildNodesValue(fingerprint.childNodesValue());
+        }
+        for (Fingerprint childNode : fingerprint.childNodes()) {
+          builder.addChildNodes(fingerprintToProto(childNode));
+        }
+        return builder.build();
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public Layout build() {
+        return Layout.fromProto(mImpl.build());
+      }
+    }
+  }
+
+  /**
+   * The horizontal alignment of an element within its container.
+   *
+   * @hide
+   */
+  @RestrictTo(RestrictTo.Scope.LIBRARY)
+  @IntDef({
+    HORIZONTAL_ALIGN_UNDEFINED,
+    HORIZONTAL_ALIGN_LEFT,
+    HORIZONTAL_ALIGN_CENTER,
+    HORIZONTAL_ALIGN_RIGHT,
+    HORIZONTAL_ALIGN_START,
+    HORIZONTAL_ALIGN_END
+  })
+  @Retention(RetentionPolicy.SOURCE)
+  public @interface HorizontalAlignment {}
+
+  /** Horizontal alignment is undefined. */
+  public static final int HORIZONTAL_ALIGN_UNDEFINED = 0;
+
+  /** Horizontally align to the left. */
+  public static final int HORIZONTAL_ALIGN_LEFT = 1;
+
+  /** Horizontally align to center. */
+  public static final int HORIZONTAL_ALIGN_CENTER = 2;
+
+  /** Horizontally align to the right. */
+  public static final int HORIZONTAL_ALIGN_RIGHT = 3;
+
+  /** Horizontally align to the content start (left in LTR layouts, right in RTL layouts). */
+  public static final int HORIZONTAL_ALIGN_START = 4;
+
+  /** Horizontally align to the content end (right in LTR layouts, left in RTL layouts). */
+  public static final int HORIZONTAL_ALIGN_END = 5;
+
+  /**
+   * The vertical alignment of an element within its container.
+   *
+   * @hide
+   */
+  @RestrictTo(RestrictTo.Scope.LIBRARY)
+  @IntDef({
+    VERTICAL_ALIGN_UNDEFINED,
+    VERTICAL_ALIGN_TOP,
+    VERTICAL_ALIGN_CENTER,
+    VERTICAL_ALIGN_BOTTOM
+  })
+  @Retention(RetentionPolicy.SOURCE)
+  public @interface VerticalAlignment {}
+
+  /** Vertical alignment is undefined. */
+  public static final int VERTICAL_ALIGN_UNDEFINED = 0;
+
+  /** Vertically align to the top. */
+  public static final int VERTICAL_ALIGN_TOP = 1;
+
+  /** Vertically align to center. */
+  public static final int VERTICAL_ALIGN_CENTER = 2;
+
+  /** Vertically align to the bottom. */
+  public static final int VERTICAL_ALIGN_BOTTOM = 3;
+
+  /**
+   * Alignment of a text element.
+   *
+   * @hide
+   */
+  @RestrictTo(RestrictTo.Scope.LIBRARY)
+  @IntDef({TEXT_ALIGN_UNDEFINED, TEXT_ALIGN_START, TEXT_ALIGN_CENTER, TEXT_ALIGN_END})
+  @Retention(RetentionPolicy.SOURCE)
+  public @interface TextAlignment {}
+
+  /** Alignment is undefined. */
+  public static final int TEXT_ALIGN_UNDEFINED = 0;
+
+  /**
+   * Align to the "start" of the {@link androidx.wear.tiles.LayoutElementBuilders.Text} element
+   * (left in LTR layouts, right in RTL layouts).
+   */
+  public static final int TEXT_ALIGN_START = 1;
+
+  /** Align to the center of the {@link androidx.wear.tiles.LayoutElementBuilders.Text} element. */
+  public static final int TEXT_ALIGN_CENTER = 2;
+
+  /**
+   * Align to the "end" of the {@link androidx.wear.tiles.LayoutElementBuilders.Text} element (right
+   * in LTR layouts, left in RTL layouts).
+   */
+  public static final int TEXT_ALIGN_END = 3;
+
+  /**
+   * The anchor position of an {@link androidx.wear.tiles.LayoutElementBuilders.Arc}'s elements.
+   * This is used to specify how elements added to an {@link
+   * androidx.wear.tiles.LayoutElementBuilders.Arc} should be laid out with respect to anchor_angle.
+   *
+   * <p>As an example, assume that the following diagrams are wrapped to an arc, and each represents
+   * an {@link androidx.wear.tiles.LayoutElementBuilders.Arc} element containing a single {@link
+   * androidx.wear.tiles.LayoutElementBuilders.Text} element. The {@link
+   * androidx.wear.tiles.LayoutElementBuilders.Text} element's anchor_angle is "0" for all cases.
+   *
+   * <pre>{@code
+   * ARC_ANCHOR_START:
+   * -180                                0                                    180
+   *                                     Hello World!
+   *
+   *
+   * ARC_ANCHOR_CENTER:
+   * -180                                0                                    180
+   *                                Hello World!
+   *
+   * ARC_ANCHOR_END:
+   * -180                                0                                    180
+   *                          Hello World!
+   *
+   * }</pre>
+   *
+   * @hide
+   */
+  @RestrictTo(RestrictTo.Scope.LIBRARY)
+  @IntDef({ARC_ANCHOR_UNDEFINED, ARC_ANCHOR_START, ARC_ANCHOR_CENTER, ARC_ANCHOR_END})
+  @Retention(RetentionPolicy.SOURCE)
+  public @interface ArcAnchorType {}
+
+  /** Anchor position is undefined. */
+  public static final int ARC_ANCHOR_UNDEFINED = 0;
+
+  /**
+   * Anchor at the start of the elements. This will cause elements added to an arc to begin at the
+   * given anchor_angle, and sweep around to the right.
+   */
+  public static final int ARC_ANCHOR_START = 1;
+
+  /**
+   * Anchor at the center of the elements. This will cause the center of the whole set of elements
+   * added to an arc to be pinned at the given anchor_angle.
+   */
+  public static final int ARC_ANCHOR_CENTER = 2;
+
+  /**
+   * Anchor at the end of the elements. This will cause the set of elements inside the arc to end at
+   * the specified anchor_angle, i.e. all elements should be to the left of anchor_angle.
+   */
+  public static final int ARC_ANCHOR_END = 3;
+
+  /** An extensible {@code HorizontalAlignment} property. */
+  public static final class HorizontalAlignmentProp {
+    private final AlignmentProto.HorizontalAlignmentProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    HorizontalAlignmentProp(
+        AlignmentProto.HorizontalAlignmentProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the value. Intended for testing purposes only. */
+    @HorizontalAlignment
+    public int getValue() {
+      return mImpl.getValue().getNumber();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static HorizontalAlignmentProp fromProto(
+        @NonNull AlignmentProto.HorizontalAlignmentProp proto) {
+      return new HorizontalAlignmentProp(proto, null);
+    }
+
+    @NonNull
+    AlignmentProto.HorizontalAlignmentProp toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link HorizontalAlignmentProp} */
+    public static final class Builder {
+      private final AlignmentProto.HorizontalAlignmentProp.Builder mImpl =
+          AlignmentProto.HorizontalAlignmentProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-384830516);
+
+      public Builder() {}
+
+      /** Sets the value. */
+      @NonNull
+      public Builder setValue(@HorizontalAlignment int value) {
+        mImpl.setValue(AlignmentProto.HorizontalAlignment.forNumber(value));
+        mFingerprint.recordPropertyUpdate(1, value);
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public HorizontalAlignmentProp build() {
+        return new HorizontalAlignmentProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** An extensible {@code VerticalAlignment} property. */
+  public static final class VerticalAlignmentProp {
+    private final AlignmentProto.VerticalAlignmentProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    VerticalAlignmentProp(
+        AlignmentProto.VerticalAlignmentProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the value. Intended for testing purposes only. */
+    @VerticalAlignment
+    public int getValue() {
+      return mImpl.getValue().getNumber();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static VerticalAlignmentProp fromProto(@NonNull AlignmentProto.VerticalAlignmentProp proto) {
+      return new VerticalAlignmentProp(proto, null);
+    }
+
+    @NonNull
+    AlignmentProto.VerticalAlignmentProp toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link VerticalAlignmentProp} */
+    public static final class Builder {
+      private final AlignmentProto.VerticalAlignmentProp.Builder mImpl =
+          AlignmentProto.VerticalAlignmentProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(1443510393);
+
+      public Builder() {}
+
+      /** Sets the value. */
+      @NonNull
+      public Builder setValue(@VerticalAlignment int value) {
+        mImpl.setValue(AlignmentProto.VerticalAlignment.forNumber(value));
+        mFingerprint.recordPropertyUpdate(1, value);
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public VerticalAlignmentProp build() {
+        return new VerticalAlignmentProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** An extensible {@code TextAlignment} property. */
+  public static final class TextAlignmentProp {
+    private final AlignmentProto.TextAlignmentProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    TextAlignmentProp(
+        AlignmentProto.TextAlignmentProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the value. Intended for testing purposes only. */
+    @TextAlignment
+    public int getValue() {
+      return mImpl.getValue().getNumber();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static TextAlignmentProp fromProto(@NonNull AlignmentProto.TextAlignmentProp proto) {
+      return new TextAlignmentProp(proto, null);
+    }
+
+    @NonNull
+    AlignmentProto.TextAlignmentProp toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link TextAlignmentProp} */
+    public static final class Builder {
+      private final AlignmentProto.TextAlignmentProp.Builder mImpl =
+          AlignmentProto.TextAlignmentProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(797507251);
+
+      public Builder() {}
+
+      /** Sets the value. */
+      @NonNull
+      public Builder setValue(@TextAlignment int value) {
+        mImpl.setValue(AlignmentProto.TextAlignment.forNumber(value));
+        mFingerprint.recordPropertyUpdate(1, value);
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public TextAlignmentProp build() {
+        return new TextAlignmentProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** An extensible {@code ArcAnchorType} property. */
+  public static final class ArcAnchorTypeProp {
+    private final AlignmentProto.ArcAnchorTypeProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    ArcAnchorTypeProp(
+        AlignmentProto.ArcAnchorTypeProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the value. Intended for testing purposes only. */
+    @ArcAnchorType
+    public int getValue() {
+      return mImpl.getValue().getNumber();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static ArcAnchorTypeProp fromProto(@NonNull AlignmentProto.ArcAnchorTypeProp proto) {
+      return new ArcAnchorTypeProp(proto, null);
+    }
+
+    @NonNull
+    AlignmentProto.ArcAnchorTypeProp toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link ArcAnchorTypeProp} */
+    public static final class Builder {
+      private final AlignmentProto.ArcAnchorTypeProp.Builder mImpl =
+          AlignmentProto.ArcAnchorTypeProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(1193249074);
+
+      public Builder() {}
+
+      /** Sets the value. */
+      @NonNull
+      public Builder setValue(@ArcAnchorType int value) {
+        mImpl.setValue(AlignmentProto.ArcAnchorType.forNumber(value));
+        mFingerprint.recordPropertyUpdate(1, value);
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public ArcAnchorTypeProp build() {
+        return new ArcAnchorTypeProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** Font styles, currently set up to match Wear's font styling. */
+  public static class FontStyles {
+    private static final int LARGE_SCREEN_WIDTH_DP = 210;
+
+    private FontStyles() {
+    }
+
+    private static boolean isLargeScreen(@NonNull DeviceParameters deviceParameters) {
+      return deviceParameters.getScreenWidthDp() >= LARGE_SCREEN_WIDTH_DP;
+    }
+
+    /** Font style for large display text. */
+    @NonNull
+    public static FontStyle.Builder display1(@NonNull DeviceParameters deviceParameters) {
+      return new FontStyle.Builder()
+              .setWeight(FONT_WEIGHT_BOLD)
+              .setSize(DimensionBuilders.sp(isLargeScreen(deviceParameters) ? 54 : 50));
+    }
+
+    /** Font style for medium display text. */
+    @NonNull
+    public static FontStyle.Builder display2(@NonNull DeviceParameters deviceParameters) {
+      return new FontStyle.Builder()
+              .setWeight(FONT_WEIGHT_BOLD)
+              .setSize(DimensionBuilders.sp(isLargeScreen(deviceParameters) ? 44 : 40));
+    }
+
+    /** Font style for small display text. */
+    @NonNull
+    public static FontStyle.Builder display3(@NonNull DeviceParameters deviceParameters) {
+      return new FontStyle.Builder()
+              .setWeight(FONT_WEIGHT_BOLD)
+              .setSize(DimensionBuilders.sp(isLargeScreen(deviceParameters) ? 34 : 30));
+    }
+
+    /** Font style for large title text. */
+    @NonNull
+    public static FontStyle.Builder title1(@NonNull DeviceParameters deviceParameters) {
+      return new FontStyle.Builder()
+              .setWeight(FONT_WEIGHT_BOLD)
+              .setSize(DimensionBuilders.sp(isLargeScreen(deviceParameters) ? 26 : 24));
+    }
+
+    /** Font style for medium title text. */
+    @NonNull
+    public static FontStyle.Builder title2(@NonNull DeviceParameters deviceParameters) {
+      return new FontStyle.Builder()
+              .setWeight(FONT_WEIGHT_BOLD)
+              .setSize(DimensionBuilders.sp(isLargeScreen(deviceParameters) ? 22 : 20));
+    }
+
+    /** Font style for small title text. */
+    @NonNull
+    public static FontStyle.Builder title3(@NonNull DeviceParameters deviceParameters) {
+      return new FontStyle.Builder()
+              .setWeight(FONT_WEIGHT_BOLD)
+              .setSize(DimensionBuilders.sp(isLargeScreen(deviceParameters) ? 18 : 16));
+    }
+
+    /** Font style for large body text. */
+    @NonNull
+    public static FontStyle.Builder body1(@NonNull DeviceParameters deviceParameters) {
+      return new FontStyle.Builder()
+              .setSize(DimensionBuilders.sp(isLargeScreen(deviceParameters) ? 18 : 16));
+    }
+
+    /** Font style for medium body text. */
+    @NonNull
+    public static FontStyle.Builder body2(@NonNull DeviceParameters deviceParameters) {
+      return new FontStyle.Builder()
+              .setSize(DimensionBuilders.sp(isLargeScreen(deviceParameters) ? 16 : 14));
+    }
+
+    /** Font style for button text. */
+    @NonNull
+    public static FontStyle.Builder button(@NonNull DeviceParameters deviceParameters) {
+      return new FontStyle.Builder()
+              .setWeight(FONT_WEIGHT_BOLD)
+              .setSize(DimensionBuilders.sp(isLargeScreen(deviceParameters) ? 16 : 14));
+    }
+
+    /** Font style for large caption text. */
+    @NonNull
+    public static FontStyle.Builder caption1(@NonNull DeviceParameters deviceParameters) {
+      return new FontStyle.Builder()
+              .setSize(DimensionBuilders.sp(isLargeScreen(deviceParameters) ? 16 : 14));
+    }
+
+    /** Font style for medium caption text. */
+    @NonNull
+    public static FontStyle.Builder caption2(@NonNull DeviceParameters deviceParameters) {
+      return new FontStyle.Builder()
+              .setSize(DimensionBuilders.sp(isLargeScreen(deviceParameters) ? 14 : 12));
+    }
+  }
+
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ModifiersBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ModifiersBuilders.java
new file mode 100644
index 0000000..cc326a4
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ModifiersBuilders.java
@@ -0,0 +1,1117 @@
+/*
+ * Copyright 2021-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.wear.protolayout;
+
+import static androidx.annotation.Dimension.DP;
+import static androidx.wear.protolayout.expression.Preconditions.checkNotNull;
+
+import android.annotation.SuppressLint;
+import androidx.annotation.ColorInt;
+import androidx.annotation.Dimension;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.protolayout.expression.DynamicBuilders;
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString;
+import androidx.wear.protolayout.expression.Fingerprint;
+import androidx.wear.protolayout.proto.ColorProto;
+import androidx.wear.protolayout.proto.DimensionProto;
+import androidx.wear.protolayout.proto.ModifiersProto;
+import androidx.wear.protolayout.proto.TypesProto;
+import androidx.wear.protolayout.ActionBuilders.Action;
+import androidx.wear.protolayout.ColorBuilders.ColorProp;
+import androidx.wear.protolayout.DimensionBuilders.DpProp;
+import androidx.wear.protolayout.TypeBuilders.BoolProp;
+import androidx.wear.protolayout.protobuf.ByteString;
+import java.util.Arrays;
+
+/** Builders for modifiers for composable layout elements. */
+public final class ModifiersBuilders {
+  private ModifiersBuilders() {}
+
+  /**
+   * A modifier for an element which can have associated Actions for click events. When an element
+   * with a ClickableModifier is clicked it will fire the associated action.
+   */
+  public static final class Clickable {
+    private final ModifiersProto.Clickable mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    Clickable(ModifiersProto.Clickable impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the ID associated with this action. Intended for testing purposes only. */
+    @NonNull
+    public String getId() {
+      return mImpl.getId();
+    }
+
+    /**
+     * Gets the action to perform when the element this modifier is attached to is clicked. Intended
+     * for testing purposes only.
+     */
+    @Nullable
+    public Action getOnClick() {
+      if (mImpl.hasOnClick()) {
+        return ActionBuilders.actionFromProto(mImpl.getOnClick());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static Clickable fromProto(@NonNull ModifiersProto.Clickable proto) {
+      return new Clickable(proto, null);
+    }
+
+    /**
+     * Returns the internal proto instance.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public ModifiersProto.Clickable toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link Clickable} */
+    public static final class Builder {
+      private final ModifiersProto.Clickable.Builder mImpl = ModifiersProto.Clickable.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(595587995);
+
+      public Builder() {}
+
+      /** Sets the ID associated with this action. */
+      @NonNull
+      public Builder setId(@NonNull String id) {
+        mImpl.setId(id);
+        mFingerprint.recordPropertyUpdate(1, id.hashCode());
+        return this;
+      }
+
+      /** Sets the action to perform when the element this modifier is attached to is clicked. */
+      @NonNull
+      public Builder setOnClick(@NonNull Action onClick) {
+        mImpl.setOnClick(onClick.toActionProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(onClick.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public Clickable build() {
+        return new Clickable(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * A modifier for an element which has accessibility semantics associated with it. This should
+   * generally be used sparingly, and in most cases should only be applied to the top-level layout
+   * element or to Clickables.
+   */
+  public static final class Semantics {
+    private final ModifiersProto.Semantics mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    Semantics(ModifiersProto.Semantics impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the content description associated with this element. This will be dictated when the
+     * element is focused by the screen reader. Intended for testing purposes only.
+     */
+    @NonNull
+    public String getContentDescription() {
+      if (mImpl.hasContentDescription()) {
+        return mImpl.getContentDescription().getValue();
+      }
+      return mImpl.getObsoleteContentDescription();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static Semantics fromProto(@NonNull ModifiersProto.Semantics proto) {
+      return new Semantics(proto, null);
+    }
+
+    @NonNull
+    ModifiersProto.Semantics toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link Semantics} */
+    public static final class Builder {
+      private final ModifiersProto.Semantics.Builder mImpl = ModifiersProto.Semantics.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-1479823155);
+
+      public Builder() {}
+
+      /**
+       * Sets the content description associated with this element. This will be dictated when the
+       * element is focused by the screen reader.
+       */
+      @NonNull
+      public Builder setContentDescription(@NonNull String contentDescription) {
+        mImpl.setObsoleteContentDescription(contentDescription);
+        mImpl.mergeContentDescription(
+            TypesProto.StringProp.newBuilder().setValue(contentDescription).build());
+        mFingerprint.recordPropertyUpdate(4, contentDescription.hashCode());
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public Semantics build() {
+        return new Semantics(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** A modifier to apply padding around an element. */
+  public static final class Padding {
+    private final ModifiersProto.Padding mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    Padding(ModifiersProto.Padding impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the padding on the end of the content, depending on the layout direction, in DP and the
+     * value of "rtl_aware". Intended for testing purposes only.
+     */
+    @Nullable
+    public DpProp getEnd() {
+      if (mImpl.hasEnd()) {
+        return DpProp.fromProto(mImpl.getEnd());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the padding on the start of the content, depending on the layout direction, in DP and
+     * the value of "rtl_aware". Intended for testing purposes only.
+     */
+    @Nullable
+    public DpProp getStart() {
+      if (mImpl.hasStart()) {
+        return DpProp.fromProto(mImpl.getStart());
+      } else {
+        return null;
+      }
+    }
+
+    /** Gets the padding at the top, in DP. Intended for testing purposes only. */
+    @Nullable
+    public DpProp getTop() {
+      if (mImpl.hasTop()) {
+        return DpProp.fromProto(mImpl.getTop());
+      } else {
+        return null;
+      }
+    }
+
+    /** Gets the padding at the bottom, in DP. Intended for testing purposes only. */
+    @Nullable
+    public DpProp getBottom() {
+      if (mImpl.hasBottom()) {
+        return DpProp.fromProto(mImpl.getBottom());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets whether the start/end padding is aware of RTL support. If true, the values for start/end
+     * will follow the layout direction (i.e. start will refer to the right hand side of the
+     * container if the device is using an RTL locale). If false, start/end will always map to
+     * left/right, accordingly. Intended for testing purposes only.
+     */
+    @Nullable
+    public BoolProp getRtlAware() {
+      if (mImpl.hasRtlAware()) {
+        return BoolProp.fromProto(mImpl.getRtlAware());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static Padding fromProto(@NonNull ModifiersProto.Padding proto) {
+      return new Padding(proto, null);
+    }
+
+    @NonNull
+    ModifiersProto.Padding toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link Padding} */
+    public static final class Builder {
+      private final ModifiersProto.Padding.Builder mImpl = ModifiersProto.Padding.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-1120275440);
+
+      public Builder() {}
+
+      /**
+       * Sets the padding on the end of the content, depending on the layout direction, in DP and
+       * the value of "rtl_aware".
+       */
+      @NonNull
+      public Builder setEnd(@NonNull DpProp end) {
+        mImpl.setEnd(end.toProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(end.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the padding on the start of the content, depending on the layout direction, in DP and
+       * the value of "rtl_aware".
+       */
+      @NonNull
+      public Builder setStart(@NonNull DpProp start) {
+        mImpl.setStart(start.toProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(start.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the padding at the top, in DP.
+       */
+      @NonNull
+      public Builder setTop(@NonNull DpProp top) {
+        mImpl.setTop(top.toProto());
+        mFingerprint.recordPropertyUpdate(
+            3, checkNotNull(top.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the padding at the bottom, in DP.
+       */
+      @NonNull
+      public Builder setBottom(@NonNull DpProp bottom) {
+        mImpl.setBottom(bottom.toProto());
+        mFingerprint.recordPropertyUpdate(
+            4, checkNotNull(bottom.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets whether the start/end padding is aware of RTL support. If true, the values for
+       * start/end will follow the layout direction (i.e. start will refer to the right hand side of
+       * the container if the device is using an RTL locale). If false, start/end will always map to
+       * left/right, accordingly.
+       */
+      @NonNull
+      public Builder setRtlAware(@NonNull BoolProp rtlAware) {
+        mImpl.setRtlAware(rtlAware.toProto());
+        mFingerprint.recordPropertyUpdate(
+            5, checkNotNull(rtlAware.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets whether the start/end padding is aware of RTL support. If true, the values for
+       * start/end will follow the layout direction (i.e. start will refer to the right hand side of
+       * the container if the device is using an RTL locale). If false, start/end will always map to
+       * left/right, accordingly.
+       */
+      @SuppressLint("MissingGetterMatchingBuilder")
+      @NonNull
+      public Builder setRtlAware(boolean rtlAware) {
+        mImpl.setRtlAware(TypesProto.BoolProp.newBuilder().setValue(rtlAware));
+        mFingerprint.recordPropertyUpdate(5, Boolean.hashCode(rtlAware));
+        return this;
+      }
+
+      /**
+       * Sets the padding for all sides of the content, in DP.
+       */
+      @NonNull
+      @SuppressLint("MissingGetterMatchingBuilder")
+      public Builder setAll(@NonNull DpProp value) {
+        return setStart(value).setEnd(value).setTop(value).setBottom(value);
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public Padding build() {
+        return new Padding(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** A modifier to apply a border around an element. */
+  public static final class Border {
+    private final ModifiersProto.Border mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    Border(ModifiersProto.Border impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the width of the border, in DP. Intended for testing purposes only. */
+    @Nullable
+    public DpProp getWidth() {
+      if (mImpl.hasWidth()) {
+        return DpProp.fromProto(mImpl.getWidth());
+      } else {
+        return null;
+      }
+    }
+
+    /** Gets the color of the border. Intended for testing purposes only. */
+    @Nullable
+    public ColorProp getColor() {
+      if (mImpl.hasColor()) {
+        return ColorProp.fromProto(mImpl.getColor());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static Border fromProto(@NonNull ModifiersProto.Border proto) {
+      return new Border(proto, null);
+    }
+
+    @NonNull
+    ModifiersProto.Border toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link Border} */
+    public static final class Builder {
+      private final ModifiersProto.Border.Builder mImpl = ModifiersProto.Border.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(2085330827);
+
+      public Builder() {}
+
+      /**
+       * Sets the width of the border, in DP.
+       */
+      @NonNull
+      public Builder setWidth(@NonNull DpProp width) {
+        mImpl.setWidth(width.toProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(width.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the color of the border.
+       */
+      @NonNull
+      public Builder setColor(@NonNull ColorProp color) {
+        mImpl.setColor(color.toProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(color.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public Border build() {
+        return new Border(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** The corner of a {@link androidx.wear.tiles.LayoutElementBuilders.Box} element. */
+  public static final class Corner {
+    private final ModifiersProto.Corner mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    Corner(ModifiersProto.Corner impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /** Gets the radius of the corner in DP. Intended for testing purposes only. */
+    @Nullable
+    public DpProp getRadius() {
+      if (mImpl.hasRadius()) {
+        return DpProp.fromProto(mImpl.getRadius());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static Corner fromProto(@NonNull ModifiersProto.Corner proto) {
+      return new Corner(proto, null);
+    }
+
+    @NonNull
+    ModifiersProto.Corner toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link Corner} */
+    public static final class Builder {
+      private final ModifiersProto.Corner.Builder mImpl = ModifiersProto.Corner.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-623478338);
+
+      public Builder() {}
+
+      /**
+       * Sets the radius of the corner in DP.
+       */
+      @NonNull
+      public Builder setRadius(@NonNull DpProp radius) {
+        mImpl.setRadius(radius.toProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(radius.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public Corner build() {
+        return new Corner(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /** A modifier to apply a background to an element. */
+  public static final class Background {
+    private final ModifiersProto.Background mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    Background(ModifiersProto.Background impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the background color for this element. If not defined, defaults to being transparent.
+     * Intended for testing purposes only.
+     */
+    @Nullable
+    public ColorProp getColor() {
+      if (mImpl.hasColor()) {
+        return ColorProp.fromProto(mImpl.getColor());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the corner properties of this element. This only affects the drawing of this element if
+     * it has a background color or border. If not defined, defaults to having a square corner.
+     * Intended for testing purposes only.
+     */
+    @Nullable
+    public Corner getCorner() {
+      if (mImpl.hasCorner()) {
+        return Corner.fromProto(mImpl.getCorner());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static Background fromProto(@NonNull ModifiersProto.Background proto) {
+      return new Background(proto, null);
+    }
+
+    @NonNull
+    ModifiersProto.Background toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link Background} */
+    public static final class Builder {
+      private final ModifiersProto.Background.Builder mImpl =
+          ModifiersProto.Background.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(374507572);
+
+      public Builder() {}
+
+      /**
+       * Sets the background color for this element. If not defined, defaults to being transparent.
+       */
+      @NonNull
+      public Builder setColor(@NonNull ColorProp color) {
+        mImpl.setColor(color.toProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(color.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the corner properties of this element. This only affects the drawing of this element
+       * if it has a background color or border. If not defined, defaults to having a square corner.
+       */
+      @NonNull
+      public Builder setCorner(@NonNull Corner corner) {
+        mImpl.setCorner(corner.toProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(corner.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public Background build() {
+        return new Background(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * Metadata about an element. For use by libraries building higher-level components only. This can
+   * be used to track component metadata.
+   */
+  public static final class ElementMetadata {
+    private final ModifiersProto.ElementMetadata mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    ElementMetadata(
+        ModifiersProto.ElementMetadata impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets property describing the element with which it is associated. For use by libraries
+     * building higher-level components only. This can be used to track component metadata.
+     */
+    @NonNull
+    public byte[] getTagData() {
+      return mImpl.getTagData().toByteArray();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static ElementMetadata fromProto(@NonNull ModifiersProto.ElementMetadata proto) {
+      return new ElementMetadata(proto, null);
+    }
+
+    /**
+     * Returns the internal proto instance.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public ModifiersProto.ElementMetadata toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link ElementMetadata} */
+    public static final class Builder {
+      private final ModifiersProto.ElementMetadata.Builder mImpl =
+          ModifiersProto.ElementMetadata.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-589294723);
+
+      public Builder() {}
+
+      /**
+       * Sets property describing the element with which it is associated. For use by libraries
+       * building higher-level components only. This can be used to track component metadata.
+       */
+      @NonNull
+      public Builder setTagData(@NonNull byte[] tagData) {
+        mImpl.setTagData(ByteString.copyFrom(tagData));
+        mFingerprint.recordPropertyUpdate(1, Arrays.hashCode(tagData));
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public ElementMetadata build() {
+        return new ElementMetadata(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * {@link Modifiers} for an element. These may change the way they are drawn (e.g. {@link Padding}
+   * or {@link Background}), or change their behaviour (e.g. {@link Clickable}, or {@link
+   * Semantics}).
+   */
+  public static final class Modifiers {
+    private final ModifiersProto.Modifiers mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    Modifiers(ModifiersProto.Modifiers impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the clickable property of the modified element. It allows its wrapped element to have
+     * actions associated with it, which will be executed when the element is tapped. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public Clickable getClickable() {
+      if (mImpl.hasClickable()) {
+        return Clickable.fromProto(mImpl.getClickable());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the semantics of the modified element. This can be used to add metadata to the modified
+     * element (eg. screen reader content descriptions). Intended for testing purposes only.
+     */
+    @Nullable
+    public Semantics getSemantics() {
+      if (mImpl.hasSemantics()) {
+        return Semantics.fromProto(mImpl.getSemantics());
+      } else {
+        return null;
+      }
+    }
+
+    /** Gets the padding of the modified element. Intended for testing purposes only. */
+    @Nullable
+    public Padding getPadding() {
+      if (mImpl.hasPadding()) {
+        return Padding.fromProto(mImpl.getPadding());
+      } else {
+        return null;
+      }
+    }
+
+    /** Gets the border of the modified element. Intended for testing purposes only. */
+    @Nullable
+    public Border getBorder() {
+      if (mImpl.hasBorder()) {
+        return Border.fromProto(mImpl.getBorder());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the background (with optional corner radius) of the modified element. Intended for
+     * testing purposes only.
+     */
+    @Nullable
+    public Background getBackground() {
+      if (mImpl.hasBackground()) {
+        return Background.fromProto(mImpl.getBackground());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets metadata about an element. For use by libraries building higher-level components only.
+     * This can be used to track component metadata.
+     */
+    @Nullable
+    public ElementMetadata getMetadata() {
+      if (mImpl.hasMetadata()) {
+        return ElementMetadata.fromProto(mImpl.getMetadata());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    /**
+     * Creates a new wrapper instance from the proto. Intended for testing purposes only. An object
+     * created using this method can't be added to any other wrapper.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public static Modifiers fromProto(@NonNull ModifiersProto.Modifiers proto) {
+      return new Modifiers(proto, null);
+    }
+
+    /**
+     * Returns the internal proto instance.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public ModifiersProto.Modifiers toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link Modifiers} */
+    public static final class Builder {
+      private final ModifiersProto.Modifiers.Builder mImpl = ModifiersProto.Modifiers.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-170942531);
+
+      public Builder() {}
+
+      /**
+       * Sets the clickable property of the modified element. It allows its wrapped element to have
+       * actions associated with it, which will be executed when the element is tapped.
+       */
+      @NonNull
+      public Builder setClickable(@NonNull Clickable clickable) {
+        mImpl.setClickable(clickable.toProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(clickable.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets the semantics of the modified element. This can be used to add metadata to the
+       * modified element (eg. screen reader content descriptions).
+       */
+      @NonNull
+      public Builder setSemantics(@NonNull Semantics semantics) {
+        mImpl.setSemantics(semantics.toProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(semantics.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Sets the padding of the modified element. */
+      @NonNull
+      public Builder setPadding(@NonNull Padding padding) {
+        mImpl.setPadding(padding.toProto());
+        mFingerprint.recordPropertyUpdate(
+            3, checkNotNull(padding.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Sets the border of the modified element. */
+      @NonNull
+      public Builder setBorder(@NonNull Border border) {
+        mImpl.setBorder(border.toProto());
+        mFingerprint.recordPropertyUpdate(
+            4, checkNotNull(border.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Sets the background (with optional corner radius) of the modified element. */
+      @NonNull
+      public Builder setBackground(@NonNull Background background) {
+        mImpl.setBackground(background.toProto());
+        mFingerprint.recordPropertyUpdate(
+            5, checkNotNull(background.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets metadata about an element. For use by libraries building higher-level components only.
+       * This can be used to track component metadata.
+       */
+      @NonNull
+      public Builder setMetadata(@NonNull ElementMetadata metadata) {
+        mImpl.setMetadata(metadata.toProto());
+        mFingerprint.recordPropertyUpdate(
+            6, checkNotNull(metadata.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public Modifiers build() {
+        return new Modifiers(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * {@link Modifiers} that can be used with ArcLayoutElements. These may change the way they are
+   * drawn, or change their behaviour.
+   */
+  public static final class ArcModifiers {
+    private final ModifiersProto.ArcModifiers mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    ArcModifiers(ModifiersProto.ArcModifiers impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets allows its wrapped element to have actions associated with it, which will be executed
+     * when the element is tapped. Intended for testing purposes only.
+     */
+    @Nullable
+    public Clickable getClickable() {
+      if (mImpl.hasClickable()) {
+        return Clickable.fromProto(mImpl.getClickable());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets adds metadata for the modified element, for example, screen reader content descriptions.
+     * Intended for testing purposes only.
+     */
+    @Nullable
+    public Semantics getSemantics() {
+      if (mImpl.hasSemantics()) {
+        return Semantics.fromProto(mImpl.getSemantics());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static ArcModifiers fromProto(@NonNull ModifiersProto.ArcModifiers proto) {
+      return new ArcModifiers(proto, null);
+    }
+
+    /**
+     * Returns the internal proto instance.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public ModifiersProto.ArcModifiers toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link ArcModifiers} */
+    public static final class Builder {
+      private final ModifiersProto.ArcModifiers.Builder mImpl =
+          ModifiersProto.ArcModifiers.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-1648736168);
+
+      public Builder() {}
+
+      /**
+       * Sets allows its wrapped element to have actions associated with it, which will be executed
+       * when the element is tapped.
+       */
+      @NonNull
+      public Builder setClickable(@NonNull Clickable clickable) {
+        mImpl.setClickable(clickable.toProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(clickable.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /**
+       * Sets adds metadata for the modified element, for example, screen reader content
+       * descriptions.
+       */
+      @NonNull
+      public Builder setSemantics(@NonNull Semantics semantics) {
+        mImpl.setSemantics(semantics.toProto());
+        mFingerprint.recordPropertyUpdate(
+            2, checkNotNull(semantics.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public ArcModifiers build() {
+        return new ArcModifiers(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * {@link Modifiers} that can be used with {@link androidx.wear.tiles.LayoutElementBuilders.Span}
+   * elements. These may change the way they are drawn, or change their behaviour.
+   */
+  public static final class SpanModifiers {
+    private final ModifiersProto.SpanModifiers mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    SpanModifiers(ModifiersProto.SpanModifiers impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets allows its wrapped element to have actions associated with it, which will be executed
+     * when the element is tapped. Intended for testing purposes only.
+     */
+    @Nullable
+    public Clickable getClickable() {
+      if (mImpl.hasClickable()) {
+        return Clickable.fromProto(mImpl.getClickable());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static SpanModifiers fromProto(@NonNull ModifiersProto.SpanModifiers proto) {
+      return new SpanModifiers(proto, null);
+    }
+
+    /**
+     * Returns the internal proto instance.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public ModifiersProto.SpanModifiers toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link SpanModifiers} */
+    public static final class Builder {
+      private final ModifiersProto.SpanModifiers.Builder mImpl =
+          ModifiersProto.SpanModifiers.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-1318656482);
+
+      public Builder() {}
+
+      /**
+       * Sets allows its wrapped element to have actions associated with it, which will be executed
+       * when the element is tapped.
+       */
+      @NonNull
+      public Builder setClickable(@NonNull Clickable clickable) {
+        mImpl.setClickable(clickable.toProto());
+        mFingerprint.recordPropertyUpdate(
+            1, checkNotNull(clickable.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public SpanModifiers build() {
+        return new SpanModifiers(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ResourceBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ResourceBuilders.java
new file mode 100644
index 0000000..8e23d72
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ResourceBuilders.java
@@ -0,0 +1,414 @@
+/*
+ * Copyright 2021-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.wear.protolayout;
+
+import static androidx.annotation.Dimension.PX;
+
+import android.annotation.SuppressLint;
+import androidx.annotation.Dimension;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.protolayout.expression.ProtoLayoutExperimental;
+import androidx.wear.protolayout.proto.ResourceProto;
+import androidx.wear.protolayout.protobuf.ByteString;
+import androidx.wear.protolayout.protobuf.InvalidProtocolBufferException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/** Builders for the resources for a layout. */
+public final class ResourceBuilders {
+  private ResourceBuilders() {}
+
+  /**
+   * Format describing the contents of an image data byte array.
+   *
+   * @hide
+   */
+  @RestrictTo(RestrictTo.Scope.LIBRARY)
+  @IntDef({IMAGE_FORMAT_UNDEFINED, IMAGE_FORMAT_RGB_565})
+  @Retention(RetentionPolicy.SOURCE)
+  public @interface ImageFormat {}
+
+  /** An undefined image format. */
+  public static final int IMAGE_FORMAT_UNDEFINED = 0;
+
+  /**
+   * An image format where each pixel is stored on 2 bytes, with red using 5 bits, green using 6
+   * bits and blue using 5 bits of precision.
+   */
+  public static final int IMAGE_FORMAT_RGB_565 = 1;
+
+  /** An image resource which maps to an Android drawable by resource ID. */
+  public static final class AndroidImageResourceByResId {
+    private final ResourceProto.AndroidImageResourceByResId mImpl;
+
+    private AndroidImageResourceByResId(ResourceProto.AndroidImageResourceByResId impl) {
+      this.mImpl = impl;
+    }
+
+    /**
+     * Gets the Android resource ID of this image. This must refer to a drawable under R.drawable.
+     * Intended for testing purposes only.
+     */
+    @DrawableRes
+    public int getResourceId() {
+      return mImpl.getResourceId();
+    }
+
+    @NonNull
+    static AndroidImageResourceByResId fromProto(
+        @NonNull ResourceProto.AndroidImageResourceByResId proto) {
+      return new AndroidImageResourceByResId(proto);
+    }
+
+    @NonNull
+    ResourceProto.AndroidImageResourceByResId toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link AndroidImageResourceByResId} */
+    public static final class Builder {
+      private final ResourceProto.AndroidImageResourceByResId.Builder mImpl =
+          ResourceProto.AndroidImageResourceByResId.newBuilder();
+
+      public Builder() {}
+
+      /**
+       * Sets the Android resource ID of this image. This must refer to a drawable under R.drawable.
+       */
+      @NonNull
+      public Builder setResourceId(@DrawableRes int resourceId) {
+        mImpl.setResourceId(resourceId);
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public AndroidImageResourceByResId build() {
+        return AndroidImageResourceByResId.fromProto(mImpl.build());
+      }
+    }
+  }
+
+  /**
+   * An image resource whose data is fully inlined, with no dependency on a system or app resource.
+   */
+  public static final class InlineImageResource {
+    private final ResourceProto.InlineImageResource mImpl;
+
+    private InlineImageResource(ResourceProto.InlineImageResource impl) {
+      this.mImpl = impl;
+    }
+
+    /** Gets the byte array representing the image. Intended for testing purposes only. */
+    @NonNull
+    public byte[] getData() {
+      return mImpl.getData().toByteArray();
+    }
+
+    /**
+     * Gets the native width of the image, in pixels. Only required for formats (e.g.
+     * IMAGE_FORMAT_RGB_565) where the image data does not include size. Intended for testing
+     * purposes only.
+     */
+    @Dimension(unit = PX)
+    public int getWidthPx() {
+      return mImpl.getWidthPx();
+    }
+
+    /**
+     * Gets the native height of the image, in pixels. Only required for formats (e.g.
+     * IMAGE_FORMAT_RGB_565) where the image data does not include size. Intended for testing
+     * purposes only.
+     */
+    @Dimension(unit = PX)
+    public int getHeightPx() {
+      return mImpl.getHeightPx();
+    }
+
+    /**
+     * Gets the format of the byte array data representing the image. May be left unspecified or set
+     * to IMAGE_FORMAT_UNDEFINED in which case the platform will attempt to extract this from the
+     * raw image data. If the platform does not support the format, the image will not be decoded or
+     * displayed. Intended for testing purposes only.
+     */
+    @ImageFormat
+    public int getFormat() {
+      return mImpl.getFormat().getNumber();
+    }
+
+    @NonNull
+    static InlineImageResource fromProto(@NonNull ResourceProto.InlineImageResource proto) {
+      return new InlineImageResource(proto);
+    }
+
+    @NonNull
+    ResourceProto.InlineImageResource toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link InlineImageResource} */
+    public static final class Builder {
+      private final ResourceProto.InlineImageResource.Builder mImpl =
+          ResourceProto.InlineImageResource.newBuilder();
+
+      public Builder() {}
+
+      /** Sets the byte array representing the image. */
+      @NonNull
+      public Builder setData(@NonNull byte[] data) {
+        mImpl.setData(ByteString.copyFrom(data));
+        return this;
+      }
+
+      /**
+       * Sets the native width of the image, in pixels. Only required for formats (e.g.
+       * IMAGE_FORMAT_RGB_565) where the image data does not include size.
+       */
+      @NonNull
+      public Builder setWidthPx(@Dimension(unit = PX) int widthPx) {
+        mImpl.setWidthPx(widthPx);
+        return this;
+      }
+
+      /**
+       * Sets the native height of the image, in pixels. Only required for formats (e.g.
+       * IMAGE_FORMAT_RGB_565) where the image data does not include size.
+       */
+      @NonNull
+      public Builder setHeightPx(@Dimension(unit = PX) int heightPx) {
+        mImpl.setHeightPx(heightPx);
+        return this;
+      }
+
+      /**
+       * Sets the format of the byte array data representing the image. May be left unspecified or
+       * set to IMAGE_FORMAT_UNDEFINED in which case the platform will attempt to extract this from
+       * the raw image data. If the platform does not support the format, the image will not be
+       * decoded or displayed.
+       */
+      @NonNull
+      public Builder setFormat(@ImageFormat int format) {
+        mImpl.setFormat(ResourceProto.ImageFormat.forNumber(format));
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public InlineImageResource build() {
+        return InlineImageResource.fromProto(mImpl.build());
+      }
+    }
+  }
+
+  /**
+   * An image resource, which can be used by layouts. This holds multiple underlying resource types,
+   * which the underlying runtime will pick according to what it thinks is appropriate.
+   */
+  public static final class ImageResource {
+    private final ResourceProto.ImageResource mImpl;
+
+    private ImageResource(ResourceProto.ImageResource impl) {
+      this.mImpl = impl;
+    }
+
+    /**
+     * Gets an image resource that maps to an Android drawable by resource ID. Intended for testing
+     * purposes only.
+     */
+    @Nullable
+    public AndroidImageResourceByResId getAndroidResourceByResId() {
+      if (mImpl.hasAndroidResourceByResId()) {
+        return AndroidImageResourceByResId.fromProto(mImpl.getAndroidResourceByResId());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets an image resource that contains the image data inline. Intended for testing purposes
+     * only.
+     */
+    @Nullable
+    public InlineImageResource getInlineResource() {
+      if (mImpl.hasInlineResource()) {
+        return InlineImageResource.fromProto(mImpl.getInlineResource());
+      } else {
+        return null;
+      }
+    }
+
+    @NonNull
+    static ImageResource fromProto(@NonNull ResourceProto.ImageResource proto) {
+      return new ImageResource(proto);
+    }
+
+    @NonNull
+    ResourceProto.ImageResource toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link ImageResource} */
+    public static final class Builder {
+      private final ResourceProto.ImageResource.Builder mImpl =
+          ResourceProto.ImageResource.newBuilder();
+
+      public Builder() {}
+
+      /** Sets an image resource that maps to an Android drawable by resource ID. */
+      @NonNull
+      public Builder setAndroidResourceByResId(
+          @NonNull AndroidImageResourceByResId androidResourceByResId) {
+        mImpl.setAndroidResourceByResId(androidResourceByResId.toProto());
+        return this;
+      }
+
+      /** Sets an image resource that contains the image data inline. */
+      @NonNull
+      public Builder setInlineResource(@NonNull InlineImageResource inlineResource) {
+        mImpl.setInlineResource(inlineResource.toProto());
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public ImageResource build() {
+        return ImageResource.fromProto(mImpl.build());
+      }
+    }
+  }
+
+  /** The resources for a layout. */
+  public static final class Resources {
+    private final ResourceProto.Resources mImpl;
+
+    private Resources(ResourceProto.Resources impl) {
+      this.mImpl = impl;
+    }
+
+    /**
+     * Gets the version of this {@link Resources} instance.
+     *
+     * <p>Each tile specifies the version of resources it requires. After fetching a tile, the
+     * renderer will use the resources version specified by the tile to separately fetch the
+     * resources.
+     *
+     * <p>This value must match the version of the resources required by the tile for the tile to
+     * render successfully, and must match the resource version specified in {@link
+     * androidx.wear.tiles.RequestBuilders.ResourcesRequest} which triggered this request. Intended
+     * for testing purposes only.
+     */
+    @NonNull
+    public String getVersion() {
+      return mImpl.getVersion();
+    }
+
+    /**
+     * Gets a map of resource_ids to images, which can be used by layouts. Intended for testing
+     * purposes only.
+     */
+    @NonNull
+    public Map<String, ImageResource> getIdToImageMapping() {
+      Map<String, ImageResource> map = new HashMap<>();
+      for (Entry<String, ResourceProto.ImageResource> entry : mImpl.getIdToImageMap().entrySet()) {
+        map.put(entry.getKey(), ImageResource.fromProto(entry.getValue()));
+      }
+      return Collections.unmodifiableMap(map);
+    }
+
+    /** Converts to byte array representation. */
+    @NonNull
+    @ProtoLayoutExperimental
+    public byte[] toByteArray() {
+      return mImpl.toByteArray();
+    }
+
+    /** Converts from byte array representation. */
+    @SuppressWarnings("ProtoParseWithRegistry")
+    @Nullable
+    @ProtoLayoutExperimental
+    public static Resources fromByteArray(@NonNull byte[] byteArray) {
+      try {
+        return fromProto(ResourceProto.Resources.parseFrom(byteArray));
+      } catch (InvalidProtocolBufferException e) {
+        return null;
+      }
+    }
+
+    @NonNull
+    static Resources fromProto(@NonNull ResourceProto.Resources proto) {
+      return new Resources(proto);
+    }
+
+    /**
+     * Returns the internal proto instance.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public ResourceProto.Resources toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link Resources} */
+    public static final class Builder {
+      private final ResourceProto.Resources.Builder mImpl = ResourceProto.Resources.newBuilder();
+
+      public Builder() {}
+
+      /**
+       * Sets the version of this {@link Resources} instance.
+       *
+       * <p>Each tile specifies the version of resources it requires. After fetching a tile, the
+       * renderer will use the resources version specified by the tile to separately fetch the
+       * resources.
+       *
+       * <p>This value must match the version of the resources required by the tile for the tile to
+       * render successfully, and must match the resource version specified in {@link
+       * androidx.wear.tiles.RequestBuilders.ResourcesRequest} which triggered this request.
+       */
+      @NonNull
+      public Builder setVersion(@NonNull String version) {
+        mImpl.setVersion(version);
+        return this;
+      }
+
+      /** Adds an entry into a map of resource_ids to images, which can be used by layouts. */
+      @SuppressLint("MissingGetterMatchingBuilder")
+      @NonNull
+      public Builder addIdToImageMapping(@NonNull String id, @NonNull ImageResource image) {
+        mImpl.putIdToImage(id, image.toProto());
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public Resources build() {
+        return Resources.fromProto(mImpl.build());
+      }
+    }
+  }
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java
new file mode 100644
index 0000000..ae4e25b
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java
@@ -0,0 +1,143 @@
+/*
+ * 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.wear.protolayout;
+
+import static androidx.wear.protolayout.expression.Preconditions.checkNotNull;
+
+import android.annotation.SuppressLint;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.protolayout.expression.Fingerprint;
+import androidx.wear.protolayout.expression.ProtoLayoutExperimental;
+import androidx.wear.protolayout.expression.StateEntryBuilders;
+import androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue;
+import androidx.wear.protolayout.expression.proto.StateEntryProto;
+import androidx.wear.protolayout.proto.StateProto;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/** Builders for state of a layout. */
+public final class StateBuilders {
+  private StateBuilders() {}
+
+  /**
+   * {@link State} information.
+   *
+   * @since 1.0
+   */
+  public static final class State {
+    private final StateProto.State mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    State(StateProto.State impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the ID of the clickable that was last clicked.
+     *
+     * @since 1.0
+     */
+    @NonNull
+    public String getLastClickableId() {
+      return mImpl.getLastClickableId();
+    }
+
+    /**
+     * Gets any shared state between the provider and renderer.
+     *
+     * @since 1.2
+     */
+    @NonNull
+    public Map<String, StateEntryValue> getIdToValueMapping() {
+      Map<String, StateEntryValue> map = new HashMap<>();
+      for (Entry<String, StateEntryProto.StateEntryValue> entry :
+          mImpl.getIdToValueMap().entrySet()) {
+        map.put(entry.getKey(), StateEntryBuilders.stateEntryValueFromProto(entry.getValue()));
+      }
+      return Collections.unmodifiableMap(map);
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    /**
+     * Creates a new wrapper instance from the proto. Intended for testing purposes only. An object
+     * created using this method can't be added to any other wrapper.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public static State fromProto(@NonNull StateProto.State proto) {
+      return new State(proto, null);
+    }
+
+    /**
+     * Returns the internal proto instance.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public StateProto.State toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link State} */
+    public static final class Builder {
+      private final StateProto.State.Builder mImpl = StateProto.State.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-688813584);
+
+      public Builder() {}
+
+      /**
+       * Adds an entry into any shared state between the provider and renderer.
+       * @param id The key for the state item. This can be used when referring to this state item.
+       * @param value The value of the state item.
+       * @since 1.2
+       */
+      @SuppressLint("MissingGetterMatchingBuilder")
+        @NonNull
+      public Builder addIdToValueMapping(@NonNull String id, @NonNull StateEntryValue value) {
+        mImpl.putIdToValue(id, value.toStateEntryValueProto());
+        mFingerprint.recordPropertyUpdate(
+            id.hashCode(), checkNotNull(value.getFingerprint()).aggregateValueAsInt());
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public State build() {
+        return new State(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TimelineBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TimelineBuilders.java
new file mode 100644
index 0000000..047e0a9
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TimelineBuilders.java
@@ -0,0 +1,297 @@
+/*
+ * 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.wear.protolayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.protolayout.LayoutElementBuilders.Layout;
+import androidx.wear.protolayout.proto.TimelineProto;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Builders for a timeline with entries representing content that should be displayed within given
+ * time intervals.
+ */
+public final class TimelineBuilders {
+  private TimelineBuilders() {}
+
+  /**
+   * A time interval, typically used to describe the validity period of a {@link TimelineEntry}.
+   *
+   * @since 1.0
+   */
+  public static final class TimeInterval {
+    private final TimelineProto.TimeInterval mImpl;
+
+    TimeInterval(TimelineProto.TimeInterval impl) {
+      this.mImpl = impl;
+    }
+
+    /**
+     * Gets starting point of the time interval, in milliseconds since the Unix epoch.
+     *
+     * @since 1.0
+     */
+    public long getStartMillis() {
+      return mImpl.getStartMillis();
+    }
+
+    /**
+     * Gets end point of the time interval, in milliseconds since the Unix epoch.
+     *
+     * @since 1.0
+     */
+    public long getEndMillis() {
+      return mImpl.getEndMillis();
+    }
+
+    @NonNull
+    static TimeInterval fromProto(@NonNull TimelineProto.TimeInterval proto) {
+      return new TimeInterval(proto);
+    }
+
+    @NonNull
+    TimelineProto.TimeInterval toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link TimeInterval} */
+    public static final class Builder {
+      private final TimelineProto.TimeInterval.Builder mImpl =
+          TimelineProto.TimeInterval.newBuilder();
+
+      public Builder() {}
+
+      /**
+       * Sets starting point of the time interval, in milliseconds since the Unix epoch.
+       *
+       * @since 1.0
+       */
+      @NonNull
+      public Builder setStartMillis(long startMillis) {
+        mImpl.setStartMillis(startMillis);
+        return this;
+      }
+
+      /**
+       * Sets end point of the time interval, in milliseconds since the Unix epoch.
+       *
+       * @since 1.0
+       */
+      @NonNull
+      public Builder setEndMillis(long endMillis) {
+        mImpl.setEndMillis(endMillis);
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public TimeInterval build() {
+        return TimeInterval.fromProto(mImpl.build());
+      }
+    }
+  }
+
+  /**
+   * One piece of renderable content along with the time that it is valid for.
+   *
+   * @since 1.0
+   */
+  public static final class TimelineEntry {
+    private final TimelineProto.TimelineEntry mImpl;
+
+    TimelineEntry(TimelineProto.TimelineEntry impl) {
+      this.mImpl = impl;
+    }
+
+    /**
+     * Gets the validity period for this timeline entry.
+     *
+     * @since 1.0
+     */
+    @Nullable
+    public TimeInterval getValidity() {
+      if (mImpl.hasValidity()) {
+        return TimeInterval.fromProto(mImpl.getValidity());
+      } else {
+        return null;
+      }
+    }
+
+    /**
+     * Gets the contents of this timeline entry.
+     *
+     * @since 1.0
+     */
+    @Nullable
+    public Layout getLayout() {
+      if (mImpl.hasLayout()) {
+        return Layout.fromProto(mImpl.getLayout());
+      } else {
+        return null;
+      }
+    }
+
+    /** Returns the {@link TimelineEntry} object containing the given layout element. */
+    @NonNull
+    public static TimelineEntry fromLayoutElement(
+        @NonNull LayoutElementBuilders.LayoutElement layoutElement) {
+      return new Builder().setLayout(Layout.fromLayoutElement(layoutElement)).build();
+    }
+
+    @NonNull
+    static TimelineEntry fromProto(@NonNull TimelineProto.TimelineEntry proto) {
+      return new TimelineEntry(proto);
+    }
+
+    @NonNull
+    TimelineProto.TimelineEntry toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link TimelineEntry} */
+    public static final class Builder {
+      private final TimelineProto.TimelineEntry.Builder mImpl =
+          TimelineProto.TimelineEntry.newBuilder();
+
+      public Builder() {}
+
+      /**
+       * Sets the validity period for this timeline entry.
+       *
+       * @since 1.0
+       */
+      @NonNull
+      public Builder setValidity(@NonNull TimeInterval validity) {
+        mImpl.setValidity(validity.toProto());
+        return this;
+      }
+
+      /**
+       * Sets the contents of this timeline entry.
+       *
+       * @since 1.0
+       */
+      @NonNull
+      public Builder setLayout(@NonNull Layout layout) {
+        mImpl.setLayout(layout.toProto());
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public TimelineEntry build() {
+        return TimelineEntry.fromProto(mImpl.build());
+      }
+    }
+  }
+
+  /**
+   * A collection of {@link TimelineEntry} items.
+   *
+   * <p>{@link TimelineEntry} items can be used to update a layout on-screen at known times, without
+   * having to explicitly update a layout. This allows for cases where, say, a calendar can be used
+   * to show the next event, and automatically switch to showing the next event when one has passed.
+   *
+   * <p>The active {@link TimelineEntry} is switched, at most, once a minute. In the case where the
+   * validity periods of {@link TimelineEntry} items overlap, the item with the shortest* validity
+   * period will be shown. This allows a layout provider to show a "default" layout, and override it
+   * at set points without having to explicitly insert the default layout between the "override"
+   * layout.
+   *
+   * @since 1.0
+   */
+  public static final class Timeline {
+    private final TimelineProto.Timeline mImpl;
+
+    Timeline(TimelineProto.Timeline impl) {
+      this.mImpl = impl;
+    }
+
+    /**
+     * Gets the entries in a timeline.
+     *
+     * @since 1.0
+     */
+    @NonNull
+    public List<TimelineEntry> getTimelineEntries() {
+      List<TimelineEntry> list = new ArrayList<>();
+      for (TimelineProto.TimelineEntry item : mImpl.getTimelineEntriesList()) {
+        list.add(TimelineEntry.fromProto(item));
+      }
+      return Collections.unmodifiableList(list);
+    }
+
+    /** Returns the {@link Timeline} object containing the given layout element. */
+    @NonNull
+    public static Timeline fromLayoutElement(
+        @NonNull LayoutElementBuilders.LayoutElement layoutElement) {
+      return new Builder().addTimelineEntry(TimelineEntry.fromLayoutElement(layoutElement)).build();
+    }
+
+    /**
+     * Creates a new wrapper instance from the proto. An object created using this method can't
+     * be added to any other wrapper.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public static Timeline fromProto(@NonNull TimelineProto.Timeline proto) {
+      return new Timeline(proto);
+    }
+
+    /**
+     * Returns the internal proto instance.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public TimelineProto.Timeline toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link Timeline} */
+    public static final class Builder {
+      private final TimelineProto.Timeline.Builder mImpl = TimelineProto.Timeline.newBuilder();
+
+      public Builder() {}
+
+      /**
+       * Adds one item to the entries in a timeline.
+       *
+       * @since 1.0
+       */
+      @NonNull
+      public Builder addTimelineEntry(@NonNull TimelineEntry timelineEntry) {
+        mImpl.addTimelineEntries(timelineEntry.toProto());
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public Timeline build() {
+        return Timeline.fromProto(mImpl.build());
+      }
+    }
+  }
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TypeBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TypeBuilders.java
new file mode 100644
index 0000000..7c83687b
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TypeBuilders.java
@@ -0,0 +1,316 @@
+/*
+ * 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.wear.protolayout;
+
+import android.annotation.SuppressLint;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.protolayout.expression.Fingerprint;
+import androidx.wear.protolayout.proto.TypesProto;
+
+/** Builders for extensible primitive types used by layout elements. */
+public final class TypeBuilders {
+  private TypeBuilders() {}
+
+  /**
+   * An int32 type.
+   *
+   * @since 1.0
+   */
+  public static final class Int32Prop {
+    private final TypesProto.Int32Prop mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    Int32Prop(TypesProto.Int32Prop impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the value.
+     *
+     * @since 1.0
+     */
+    public int getValue() {
+      return mImpl.getValue();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static Int32Prop fromProto(@NonNull TypesProto.Int32Prop proto) {
+      return new Int32Prop(proto, null);
+    }
+
+    @NonNull
+    TypesProto.Int32Prop toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link Int32Prop} */
+    public static final class Builder {
+      private final TypesProto.Int32Prop.Builder mImpl = TypesProto.Int32Prop.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-1360212989);
+
+      public Builder() {}
+
+      /**
+       * Sets the value.
+       *
+       * @since 1.0
+       */
+      @NonNull
+      public Builder setValue(int value) {
+        mImpl.setValue(value);
+        mFingerprint.recordPropertyUpdate(1, value);
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public Int32Prop build() {
+        return new Int32Prop(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * A string type.
+   *
+   * @since 1.0
+   */
+  public static final class StringProp {
+    private final TypesProto.StringProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    StringProp(TypesProto.StringProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the value.
+     *
+     * @since 1.0
+     */
+    @NonNull
+    public String getValue() {
+      return mImpl.getValue();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static StringProp fromProto(@NonNull TypesProto.StringProp proto) {
+      return new StringProp(proto, null);
+    }
+
+    @NonNull
+    TypesProto.StringProp toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link StringProp} */
+    public static final class Builder {
+      private final TypesProto.StringProp.Builder mImpl = TypesProto.StringProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(327834307);
+
+      public Builder() {}
+
+      /**
+       * Sets the value.
+       *
+       * @since 1.0
+       */
+      @NonNull
+      public Builder setValue(@NonNull String value) {
+        mImpl.setValue(value);
+        mFingerprint.recordPropertyUpdate(1, value.hashCode());
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public StringProp build() {
+        return new StringProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * A float type.
+   *
+   * @since 1.0
+   */
+  public static final class FloatProp {
+    private final TypesProto.FloatProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    FloatProp(TypesProto.FloatProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the value.
+     *
+     * @since 1.0
+     */
+    public float getValue() {
+      return mImpl.getValue();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static FloatProp fromProto(@NonNull TypesProto.FloatProp proto) {
+      return new FloatProp(proto, null);
+    }
+
+    @NonNull
+    TypesProto.FloatProp toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link FloatProp} */
+    public static final class Builder {
+      private final TypesProto.FloatProp.Builder mImpl = TypesProto.FloatProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(-641088370);
+
+      public Builder() {}
+
+      /**
+       * Sets the value.
+       *
+       * @since 1.0
+       */
+      @NonNull
+      public Builder setValue(float value) {
+        mImpl.setValue(value);
+        mFingerprint.recordPropertyUpdate(1, Float.floatToIntBits(value));
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public FloatProp build() {
+        return new FloatProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+
+  /**
+   * A boolean type.
+   *
+   * @since 1.0
+   */
+  public static final class BoolProp {
+    private final TypesProto.BoolProp mImpl;
+    @Nullable private final Fingerprint mFingerprint;
+
+    BoolProp(TypesProto.BoolProp impl, @Nullable Fingerprint fingerprint) {
+      this.mImpl = impl;
+      this.mFingerprint = fingerprint;
+    }
+
+    /**
+     * Gets the value.
+     *
+     * @since 1.0
+     */
+    public boolean getValue() {
+      return mImpl.getValue();
+    }
+
+    /**
+     * Get the fingerprint for this object, or null if unknown.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public Fingerprint getFingerprint() {
+      return mFingerprint;
+    }
+
+    @NonNull
+    static BoolProp fromProto(@NonNull TypesProto.BoolProp proto) {
+      return new BoolProp(proto, null);
+    }
+
+    @NonNull
+    TypesProto.BoolProp toProto() {
+      return mImpl;
+    }
+
+    /** Builder for {@link BoolProp} */
+    public static final class Builder {
+      private final TypesProto.BoolProp.Builder mImpl = TypesProto.BoolProp.newBuilder();
+      private final Fingerprint mFingerprint = new Fingerprint(1691257528);
+
+      public Builder() {}
+
+      /**
+       * Sets the value.
+       *
+       * @since 1.0
+       */
+      @SuppressLint("MissingGetterMatchingBuilder")
+      @NonNull
+      public Builder setValue(boolean value) {
+        mImpl.setValue(value);
+        mFingerprint.recordPropertyUpdate(1, Boolean.hashCode(value));
+        return this;
+      }
+
+      /** Builds an instance from accumulated values. */
+      @NonNull
+      public BoolProp build() {
+        return new BoolProp(mImpl.build(), mFingerprint);
+      }
+    }
+  }
+}
diff --git a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/ActionBuildersTest.java b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/ActionBuildersTest.java
new file mode 100644
index 0000000..fa3e089
--- /dev/null
+++ b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/ActionBuildersTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.wear.protolayout.expression.StateEntryBuilders;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class ActionBuildersTest {
+    @Test
+    public void setStateAction() {
+        String key = "key";
+        StateEntryBuilders.StateEntryValue value = StateEntryBuilders.StateEntryValue.fromString(
+                "value");
+        ActionBuilders.SetStateAction setStateAction = new ActionBuilders.SetStateAction.Builder()
+                .setTargetKey(key).setValue(value).build();
+
+        assertThat(setStateAction.getTargetKey()).isEqualTo(key);
+        assertThat(setStateAction.getValue().toStateEntryValueProto()).isEqualTo(
+                value.toStateEntryValueProto());
+    }
+}
diff --git a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/StateBuildersTest.java b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/StateBuildersTest.java
new file mode 100644
index 0000000..3337f82
--- /dev/null
+++ b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/StateBuildersTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.wear.protolayout.expression.StateEntryBuilders;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class StateBuildersTest {
+    @Test
+    public void emptyState() {
+        StateBuilders.State state = new StateBuilders.State.Builder().build();
+
+        assertThat(state.getIdToValueMapping()).isEmpty();
+    }
+
+    @Test
+    public void additionalState() {
+        StateEntryBuilders.StateEntryValue boolValue =
+                StateEntryBuilders.StateEntryValue.fromBool(true);
+        StateEntryBuilders.StateEntryValue stringValue =
+                StateEntryBuilders.StateEntryValue.fromString("string");
+        StateBuilders.State state = new StateBuilders.State.Builder()
+                .addIdToValueMapping("boolValue", boolValue)
+                .addIdToValueMapping("stringValue", stringValue)
+                .build();
+
+        assertThat(state.getIdToValueMapping()).hasSize(2);
+        assertThat(state.getIdToValueMapping().get("boolValue").toStateEntryValueProto()).isEqualTo(
+                boolValue.toStateEntryValueProto());
+        assertThat(
+                state.getIdToValueMapping().get("stringValue").toStateEntryValueProto()).isEqualTo(
+                stringValue.toStateEntryValueProto());
+    }
+}
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/TileRendererInternal.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/TileRendererInternal.java
index 07e6ea9..f0938c10 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/TileRendererInternal.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/TileRendererInternal.java
@@ -68,6 +68,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.StyleRes;
 import androidx.core.content.ContextCompat;
+import androidx.wear.protolayout.proto.AlignmentProto;
 import androidx.wear.tiles.TileService;
 import androidx.wear.protolayout.proto.ActionProto.Action;
 import androidx.wear.protolayout.proto.ActionProto.AndroidActivity;
@@ -84,7 +85,7 @@
 import androidx.wear.protolayout.proto.DimensionProto.SpacerDimension;
 import androidx.wear.protolayout.proto.DimensionProto.WrappedDimensionProp;
 import androidx.wear.protolayout.proto.LayoutElementProto.Arc;
-import androidx.wear.protolayout.proto.LayoutElementProto.ArcAnchorTypeProp;
+import androidx.wear.protolayout.proto.AlignmentProto.ArcAnchorTypeProp;
 import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement;
 import androidx.wear.protolayout.proto.LayoutElementProto.ArcLine;
 import androidx.wear.protolayout.proto.LayoutElementProto.ArcSpacer;
@@ -94,7 +95,7 @@
 import androidx.wear.protolayout.proto.LayoutElementProto.ContentScaleMode;
 import androidx.wear.protolayout.proto.LayoutElementProto.FontStyle;
 import androidx.wear.protolayout.proto.LayoutElementProto.FontVariant;
-import androidx.wear.protolayout.proto.LayoutElementProto.HorizontalAlignmentProp;
+import androidx.wear.protolayout.proto.AlignmentProto.HorizontalAlignmentProp;
 import androidx.wear.protolayout.proto.LayoutElementProto.Image;
 import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
 import androidx.wear.protolayout.proto.LayoutElementProto.LayoutElement;
@@ -106,9 +107,9 @@
 import androidx.wear.protolayout.proto.LayoutElementProto.SpanVerticalAlignmentProp;
 import androidx.wear.protolayout.proto.LayoutElementProto.Spannable;
 import androidx.wear.protolayout.proto.LayoutElementProto.Text;
-import androidx.wear.protolayout.proto.LayoutElementProto.TextAlignmentProp;
+import androidx.wear.protolayout.proto.AlignmentProto.TextAlignmentProp;
 import androidx.wear.protolayout.proto.LayoutElementProto.TextOverflowProp;
-import androidx.wear.protolayout.proto.LayoutElementProto.VerticalAlignmentProp;
+import androidx.wear.protolayout.proto.AlignmentProto.VerticalAlignmentProp;
 import androidx.wear.protolayout.proto.ModifiersProto.ArcModifiers;
 import androidx.wear.protolayout.proto.ModifiersProto.Background;
 import androidx.wear.protolayout.proto.ModifiersProto.Border;
@@ -680,7 +681,7 @@
         }
 
         if (modifiers.hasSemantics()) {
-            applyAudibleParams(view, modifiers.getSemantics().getContentDescription());
+            applyAudibleParams(view, modifiers.getSemantics().getObsoleteContentDescription());
         }
 
         if (modifiers.hasPadding()) {
@@ -721,7 +722,7 @@
         }
 
         if (modifiers.hasSemantics()) {
-            applyAudibleParams(view, modifiers.getSemantics().getContentDescription());
+            applyAudibleParams(view, modifiers.getSemantics().getObsoleteContentDescription());
         }
 
         return view;
@@ -751,6 +752,7 @@
                 return null;
             case TEXT_OVERFLOW_ELLIPSIZE_END:
                 return TruncateAt.END;
+            case TEXT_OVERFLOW_MARQUEE:
             case TEXT_OVERFLOW_UNDEFINED:
             case UNRECOGNIZED:
                 return TEXT_OVERFLOW_DEFAULT;
@@ -1253,10 +1255,11 @@
             Log.wtf(TAG, "Exception tinting image " + protoResId, ex);
         }
 
-        if (image.hasFilter()) {
-            if (image.getFilter().hasTint() && canImageBeTinted) {
+        if (image.hasColorFilter()) {
+            if (image.getColorFilter().hasTint() && canImageBeTinted) {
                 // Only allow tinting for Android images.
-                ColorStateList tint = ColorStateList.valueOf(image.getFilter().getTint().getArgb());
+                ColorStateList tint =
+                        ColorStateList.valueOf(image.getColorFilter().getTint().getArgb());
                 imageView.setImageTintList(tint);
 
                 // SRC_IN throws away the colours in the drawable that we're tinting. Effectively,
diff --git a/wear/tiles/tiles-renderer/src/test/java/androidx/wear/tiles/renderer/internal/TileRendererInternalTest.java b/wear/tiles/tiles-renderer/src/test/java/androidx/wear/tiles/renderer/internal/TileRendererInternalTest.java
index 01c37cf..8f65bd4f 100644
--- a/wear/tiles/tiles-renderer/src/test/java/androidx/wear/tiles/renderer/internal/TileRendererInternalTest.java
+++ b/wear/tiles/tiles-renderer/src/test/java/androidx/wear/tiles/renderer/internal/TileRendererInternalTest.java
@@ -67,8 +67,8 @@
 import androidx.wear.protolayout.proto.LayoutElementProto.Box;
 import androidx.wear.protolayout.proto.LayoutElementProto.Column;
 import androidx.wear.protolayout.proto.LayoutElementProto.FontStyle;
-import androidx.wear.protolayout.proto.LayoutElementProto.HorizontalAlignment;
-import androidx.wear.protolayout.proto.LayoutElementProto.HorizontalAlignmentProp;
+import androidx.wear.protolayout.proto.AlignmentProto.HorizontalAlignment;
+import androidx.wear.protolayout.proto.AlignmentProto.HorizontalAlignmentProp;
 import androidx.wear.protolayout.proto.LayoutElementProto.Image;
 import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
 import androidx.wear.protolayout.proto.LayoutElementProto.LayoutElement;
@@ -79,8 +79,8 @@
 import androidx.wear.protolayout.proto.LayoutElementProto.SpanText;
 import androidx.wear.protolayout.proto.LayoutElementProto.Spannable;
 import androidx.wear.protolayout.proto.LayoutElementProto.Text;
-import androidx.wear.protolayout.proto.LayoutElementProto.VerticalAlignment;
-import androidx.wear.protolayout.proto.LayoutElementProto.VerticalAlignmentProp;
+import androidx.wear.protolayout.proto.AlignmentProto.VerticalAlignment;
+import androidx.wear.protolayout.proto.AlignmentProto.VerticalAlignmentProp;
 import androidx.wear.protolayout.proto.ModifiersProto.Border;
 import androidx.wear.protolayout.proto.ModifiersProto.Clickable;
 import androidx.wear.protolayout.proto.ModifiersProto.Modifiers;
@@ -176,8 +176,9 @@
                         .setText(StringProp.newBuilder().setValue(textContents))
                         .setModifiers(Modifiers.newBuilder()
                                 .setSemantics(Semantics.newBuilder()
-                                        .setContentDescription("Hello World Text Element"))))
-                        .build();
+                                        .setObsoleteContentDescription(
+                                                "Hello World Text Element"))))
+                .build();
 
         FrameLayout rootLayout = inflateProto(root);
 
diff --git a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/LayoutElementBuilders.java b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/LayoutElementBuilders.java
index 3a811ba..58b2dee 100644
--- a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/LayoutElementBuilders.java
+++ b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/LayoutElementBuilders.java
@@ -27,6 +27,7 @@
 import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.protolayout.proto.AlignmentProto;
 import androidx.wear.tiles.ColorBuilders.ColorProp;
 import androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters;
 import androidx.wear.tiles.DimensionBuilders.ContainerDimension;
@@ -341,9 +342,9 @@
 
     /** An extensible {@code HorizontalAlignment} property. */
     public static final class HorizontalAlignmentProp {
-        private final LayoutElementProto.HorizontalAlignmentProp mImpl;
+        private final AlignmentProto.HorizontalAlignmentProp mImpl;
 
-        private HorizontalAlignmentProp(LayoutElementProto.HorizontalAlignmentProp impl) {
+        private HorizontalAlignmentProp(AlignmentProto.HorizontalAlignmentProp impl) {
             this.mImpl = impl;
         }
 
@@ -357,28 +358,28 @@
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         public static HorizontalAlignmentProp fromProto(
-                @NonNull LayoutElementProto.HorizontalAlignmentProp proto) {
+                @NonNull AlignmentProto.HorizontalAlignmentProp proto) {
             return new HorizontalAlignmentProp(proto);
         }
 
         /** @hide */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
-        public LayoutElementProto.HorizontalAlignmentProp toProto() {
+        public AlignmentProto.HorizontalAlignmentProp toProto() {
             return mImpl;
         }
 
         /** Builder for {@link HorizontalAlignmentProp} */
         public static final class Builder {
-            private final LayoutElementProto.HorizontalAlignmentProp.Builder mImpl =
-                    LayoutElementProto.HorizontalAlignmentProp.newBuilder();
+            private final AlignmentProto.HorizontalAlignmentProp.Builder mImpl =
+                    AlignmentProto.HorizontalAlignmentProp.newBuilder();
 
             public Builder() {}
 
             /** Sets the value. */
             @NonNull
             public Builder setValue(@HorizontalAlignment int value) {
-                mImpl.setValue(LayoutElementProto.HorizontalAlignment.forNumber(value));
+                mImpl.setValue(AlignmentProto.HorizontalAlignment.forNumber(value));
                 return this;
             }
 
@@ -392,9 +393,9 @@
 
     /** An extensible {@code VerticalAlignment} property. */
     public static final class VerticalAlignmentProp {
-        private final LayoutElementProto.VerticalAlignmentProp mImpl;
+        private final AlignmentProto.VerticalAlignmentProp mImpl;
 
-        private VerticalAlignmentProp(LayoutElementProto.VerticalAlignmentProp impl) {
+        private VerticalAlignmentProp(AlignmentProto.VerticalAlignmentProp impl) {
             this.mImpl = impl;
         }
 
@@ -408,28 +409,28 @@
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         public static VerticalAlignmentProp fromProto(
-                @NonNull LayoutElementProto.VerticalAlignmentProp proto) {
+                @NonNull AlignmentProto.VerticalAlignmentProp proto) {
             return new VerticalAlignmentProp(proto);
         }
 
         /** @hide */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
-        public LayoutElementProto.VerticalAlignmentProp toProto() {
+        public AlignmentProto.VerticalAlignmentProp toProto() {
             return mImpl;
         }
 
         /** Builder for {@link VerticalAlignmentProp} */
         public static final class Builder {
-            private final LayoutElementProto.VerticalAlignmentProp.Builder mImpl =
-                    LayoutElementProto.VerticalAlignmentProp.newBuilder();
+            private final AlignmentProto.VerticalAlignmentProp.Builder mImpl =
+                    AlignmentProto.VerticalAlignmentProp.newBuilder();
 
             public Builder() {}
 
             /** Sets the value. */
             @NonNull
             public Builder setValue(@VerticalAlignment int value) {
-                mImpl.setValue(LayoutElementProto.VerticalAlignment.forNumber(value));
+                mImpl.setValue(AlignmentProto.VerticalAlignment.forNumber(value));
                 return this;
             }
 
@@ -840,9 +841,9 @@
 
     /** An extensible {@code TextAlignment} property. */
     public static final class TextAlignmentProp {
-        private final LayoutElementProto.TextAlignmentProp mImpl;
+        private final AlignmentProto.TextAlignmentProp mImpl;
 
-        private TextAlignmentProp(LayoutElementProto.TextAlignmentProp impl) {
+        private TextAlignmentProp(AlignmentProto.TextAlignmentProp impl) {
             this.mImpl = impl;
         }
 
@@ -856,28 +857,28 @@
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         public static TextAlignmentProp fromProto(
-                @NonNull LayoutElementProto.TextAlignmentProp proto) {
+                @NonNull AlignmentProto.TextAlignmentProp proto) {
             return new TextAlignmentProp(proto);
         }
 
         /** @hide */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
-        public LayoutElementProto.TextAlignmentProp toProto() {
+        public AlignmentProto.TextAlignmentProp toProto() {
             return mImpl;
         }
 
         /** Builder for {@link TextAlignmentProp} */
         public static final class Builder {
-            private final LayoutElementProto.TextAlignmentProp.Builder mImpl =
-                    LayoutElementProto.TextAlignmentProp.newBuilder();
+            private final AlignmentProto.TextAlignmentProp.Builder mImpl =
+                    AlignmentProto.TextAlignmentProp.newBuilder();
 
             public Builder() {}
 
             /** Sets the value. */
             @NonNull
             public Builder setValue(@TextAlignment int value) {
-                mImpl.setValue(LayoutElementProto.TextAlignment.forNumber(value));
+                mImpl.setValue(AlignmentProto.TextAlignment.forNumber(value));
                 return this;
             }
 
@@ -942,9 +943,9 @@
 
     /** An extensible {@code ArcAnchorType} property. */
     public static final class ArcAnchorTypeProp {
-        private final LayoutElementProto.ArcAnchorTypeProp mImpl;
+        private final AlignmentProto.ArcAnchorTypeProp mImpl;
 
-        private ArcAnchorTypeProp(LayoutElementProto.ArcAnchorTypeProp impl) {
+        private ArcAnchorTypeProp(AlignmentProto.ArcAnchorTypeProp impl) {
             this.mImpl = impl;
         }
 
@@ -958,28 +959,28 @@
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         public static ArcAnchorTypeProp fromProto(
-                @NonNull LayoutElementProto.ArcAnchorTypeProp proto) {
+                @NonNull AlignmentProto.ArcAnchorTypeProp proto) {
             return new ArcAnchorTypeProp(proto);
         }
 
         /** @hide */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
-        public LayoutElementProto.ArcAnchorTypeProp toProto() {
+        public AlignmentProto.ArcAnchorTypeProp toProto() {
             return mImpl;
         }
 
         /** Builder for {@link ArcAnchorTypeProp} */
         public static final class Builder {
-            private final LayoutElementProto.ArcAnchorTypeProp.Builder mImpl =
-                    LayoutElementProto.ArcAnchorTypeProp.newBuilder();
+            private final AlignmentProto.ArcAnchorTypeProp.Builder mImpl =
+                    AlignmentProto.ArcAnchorTypeProp.newBuilder();
 
             public Builder() {}
 
             /** Sets the value. */
             @NonNull
             public Builder setValue(@ArcAnchorType int value) {
-                mImpl.setValue(LayoutElementProto.ArcAnchorType.forNumber(value));
+                mImpl.setValue(AlignmentProto.ArcAnchorType.forNumber(value));
                 return this;
             }
 
@@ -1195,9 +1196,9 @@
             @NonNull
             public Builder setMultilineAlignment(@TextAlignment int multilineAlignment) {
                 mImpl.setMultilineAlignment(
-                        LayoutElementProto.TextAlignmentProp.newBuilder()
+                        AlignmentProto.TextAlignmentProp.newBuilder()
                                 .setValue(
-                                        LayoutElementProto.TextAlignment.forNumber(
+                                        AlignmentProto.TextAlignment.forNumber(
                                                 multilineAlignment)));
                 return this;
             }
@@ -1452,8 +1453,8 @@
          */
         @Nullable
         public ColorFilter getColorFilter() {
-            if (mImpl.hasFilter()) {
-                return ColorFilter.fromProto(mImpl.getFilter());
+            if (mImpl.hasColorFilter()) {
+                return ColorFilter.fromProto(mImpl.getColorFilter());
             } else {
                 return null;
             }
@@ -1556,7 +1557,7 @@
              */
             @NonNull
             public Builder setColorFilter(@NonNull ColorFilter filter) {
-                mImpl.setFilter(filter.toProto());
+                mImpl.setColorFilter(filter.toProto());
                 return this;
             }
 
@@ -1836,9 +1837,9 @@
             @NonNull
             public Builder setHorizontalAlignment(@HorizontalAlignment int horizontalAlignment) {
                 mImpl.setHorizontalAlignment(
-                        LayoutElementProto.HorizontalAlignmentProp.newBuilder()
+                        AlignmentProto.HorizontalAlignmentProp.newBuilder()
                                 .setValue(
-                                        LayoutElementProto.HorizontalAlignment.forNumber(
+                                        AlignmentProto.HorizontalAlignment.forNumber(
                                                 horizontalAlignment)));
                 return this;
             }
@@ -1859,9 +1860,9 @@
             @NonNull
             public Builder setVerticalAlignment(@VerticalAlignment int verticalAlignment) {
                 mImpl.setVerticalAlignment(
-                        LayoutElementProto.VerticalAlignmentProp.newBuilder()
+                        AlignmentProto.VerticalAlignmentProp.newBuilder()
                                 .setValue(
-                                        LayoutElementProto.VerticalAlignment.forNumber(
+                                        AlignmentProto.VerticalAlignment.forNumber(
                                                 verticalAlignment)));
                 return this;
             }
@@ -2396,9 +2397,9 @@
             @NonNull
             public Builder setMultilineAlignment(@HorizontalAlignment int multilineAlignment) {
                 mImpl.setMultilineAlignment(
-                        LayoutElementProto.HorizontalAlignmentProp.newBuilder()
+                        AlignmentProto.HorizontalAlignmentProp.newBuilder()
                                 .setValue(
-                                        LayoutElementProto.HorizontalAlignment.forNumber(
+                                        AlignmentProto.HorizontalAlignment.forNumber(
                                                 multilineAlignment)));
                 return this;
             }
@@ -2586,9 +2587,9 @@
             @NonNull
             public Builder setHorizontalAlignment(@HorizontalAlignment int horizontalAlignment) {
                 mImpl.setHorizontalAlignment(
-                        LayoutElementProto.HorizontalAlignmentProp.newBuilder()
+                        AlignmentProto.HorizontalAlignmentProp.newBuilder()
                                 .setValue(
-                                        LayoutElementProto.HorizontalAlignment.forNumber(
+                                        AlignmentProto.HorizontalAlignment.forNumber(
                                                 horizontalAlignment)));
                 return this;
             }
@@ -2762,9 +2763,9 @@
             @NonNull
             public Builder setVerticalAlignment(@VerticalAlignment int verticalAlignment) {
                 mImpl.setVerticalAlignment(
-                        LayoutElementProto.VerticalAlignmentProp.newBuilder()
+                        AlignmentProto.VerticalAlignmentProp.newBuilder()
                                 .setValue(
-                                        LayoutElementProto.VerticalAlignment.forNumber(
+                                        AlignmentProto.VerticalAlignment.forNumber(
                                                 verticalAlignment)));
                 return this;
             }
@@ -2951,8 +2952,8 @@
             @NonNull
             public Builder setAnchorType(@ArcAnchorType int anchorType) {
                 mImpl.setAnchorType(
-                        LayoutElementProto.ArcAnchorTypeProp.newBuilder()
-                                .setValue(LayoutElementProto.ArcAnchorType.forNumber(anchorType)));
+                        AlignmentProto.ArcAnchorTypeProp.newBuilder()
+                                .setValue(AlignmentProto.ArcAnchorType.forNumber(anchorType)));
                 return this;
             }
 
@@ -2976,9 +2977,9 @@
             @NonNull
             public Builder setVerticalAlign(@VerticalAlignment int verticalAlign) {
                 mImpl.setVerticalAlign(
-                        LayoutElementProto.VerticalAlignmentProp.newBuilder()
+                        AlignmentProto.VerticalAlignmentProp.newBuilder()
                                 .setValue(
-                                        LayoutElementProto.VerticalAlignment.forNumber(
+                                        AlignmentProto.VerticalAlignment.forNumber(
                                                 verticalAlign)));
                 return this;
             }
diff --git a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/ModifiersBuilders.java b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/ModifiersBuilders.java
index b3ced75..29fdc7e 100644
--- a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/ModifiersBuilders.java
+++ b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/ModifiersBuilders.java
@@ -127,7 +127,7 @@
          */
         @NonNull
         public String getContentDescription() {
-            return mImpl.getContentDescription();
+            return mImpl.getObsoleteContentDescription();
         }
 
         /** @hide */
@@ -157,7 +157,7 @@
              */
             @NonNull
             public Builder setContentDescription(@NonNull String contentDescription) {
-                mImpl.setContentDescription(contentDescription);
+                mImpl.setObsoleteContentDescription(contentDescription);
                 return this;
             }
 
diff --git a/wear/watchface/watchface-client/api/current.ignore b/wear/watchface/watchface-client/api/current.ignore
index 43ce51a..8c1c73c 100644
--- a/wear/watchface/watchface-client/api/current.ignore
+++ b/wear/watchface/watchface-client/api/current.ignore
@@ -1,3 +1,23 @@
 // Baseline format: 1.0
 AddedAbstractMethod: androidx.wear.watchface.client.WatchFaceMetadataClient#getUserStyleFlavors():
     Added method androidx.wear.watchface.client.WatchFaceMetadataClient.getUserStyleFlavors()
+
+
+ChangedThrows: androidx.wear.watchface.client.HeadlessWatchFaceClient#getComplicationSlotsState():
+    Method androidx.wear.watchface.client.HeadlessWatchFaceClient.getComplicationSlotsState added thrown exception android.os.RemoteException
+ChangedThrows: androidx.wear.watchface.client.HeadlessWatchFaceClient#getPreviewReferenceInstant():
+    Method androidx.wear.watchface.client.HeadlessWatchFaceClient.getPreviewReferenceInstant added thrown exception android.os.RemoteException
+ChangedThrows: androidx.wear.watchface.client.HeadlessWatchFaceClient#getUserStyleSchema():
+    Method androidx.wear.watchface.client.HeadlessWatchFaceClient.getUserStyleSchema added thrown exception android.os.RemoteException
+ChangedThrows: androidx.wear.watchface.client.InteractiveWatchFaceClient#getComplicationSlotsState():
+    Method androidx.wear.watchface.client.InteractiveWatchFaceClient.getComplicationSlotsState added thrown exception android.os.RemoteException
+ChangedThrows: androidx.wear.watchface.client.InteractiveWatchFaceClient#getContentDescriptionLabels():
+    Method androidx.wear.watchface.client.InteractiveWatchFaceClient.getContentDescriptionLabels added thrown exception android.os.RemoteException
+ChangedThrows: androidx.wear.watchface.client.InteractiveWatchFaceClient#getInstanceId():
+    Method androidx.wear.watchface.client.InteractiveWatchFaceClient.getInstanceId added thrown exception android.os.RemoteException
+ChangedThrows: androidx.wear.watchface.client.InteractiveWatchFaceClient#getOverlayStyle():
+    Method androidx.wear.watchface.client.InteractiveWatchFaceClient.getOverlayStyle added thrown exception android.os.RemoteException
+ChangedThrows: androidx.wear.watchface.client.InteractiveWatchFaceClient#getPreviewReferenceInstant():
+    Method androidx.wear.watchface.client.InteractiveWatchFaceClient.getPreviewReferenceInstant added thrown exception android.os.RemoteException
+ChangedThrows: androidx.wear.watchface.client.InteractiveWatchFaceClient#getUserStyleSchema():
+    Method androidx.wear.watchface.client.InteractiveWatchFaceClient.getUserStyleSchema added thrown exception android.os.RemoteException
diff --git a/wear/watchface/watchface-client/api/current.txt b/wear/watchface/watchface-client/api/current.txt
index 40bcc03..4e6173b 100644
--- a/wear/watchface/watchface-client/api/current.txt
+++ b/wear/watchface/watchface-client/api/current.txt
@@ -106,10 +106,10 @@
   public interface HeadlessWatchFaceClient extends java.lang.AutoCloseable {
     method @AnyThread public void addClientDisconnectListener(androidx.wear.watchface.client.HeadlessWatchFaceClient.ClientDisconnectListener listener, java.util.concurrent.Executor executor);
     method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
-    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotState> getComplicationSlotsState();
-    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.time.Instant getPreviewReferenceInstant();
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotState> getComplicationSlotsState() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.time.Instant getPreviewReferenceInstant() throws android.os.RemoteException;
     method public default androidx.wear.watchface.style.UserStyleFlavors getUserStyleFlavors();
-    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws android.os.RemoteException;
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public default byte[] getUserStyleSchemaDigestHash() throws android.os.RemoteException;
     method @AnyThread public boolean isConnectionAlive();
     method @AnyThread public void removeClientDisconnectListener(androidx.wear.watchface.client.HeadlessWatchFaceClient.ClientDisconnectListener listener);
@@ -135,12 +135,13 @@
     method @AnyThread public void addClientDisconnectListener(androidx.wear.watchface.client.InteractiveWatchFaceClient.ClientDisconnectListener listener, java.util.concurrent.Executor executor);
     method public void addOnWatchFaceReadyListener(java.util.concurrent.Executor executor, androidx.wear.watchface.client.InteractiveWatchFaceClient.OnWatchFaceReadyListener listener);
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public default Integer? getComplicationIdAt(@Px int x, @Px int y) throws android.os.RemoteException;
-    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotState> getComplicationSlotsState();
-    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.List<androidx.wear.watchface.ContentDescriptionLabel> getContentDescriptionLabels();
-    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public String getInstanceId();
-    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 @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotState> getComplicationSlotsState() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.List<androidx.wear.watchface.ContentDescriptionLabel> getContentDescriptionLabels() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public String getInstanceId() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public default androidx.wear.watchface.client.OverlayStyle getOverlayStyle() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.time.Instant getPreviewReferenceInstant() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws android.os.RemoteException;
+    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..b48fa19 100644
--- a/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
@@ -110,10 +110,10 @@
   public interface HeadlessWatchFaceClient extends java.lang.AutoCloseable {
     method @AnyThread public void addClientDisconnectListener(androidx.wear.watchface.client.HeadlessWatchFaceClient.ClientDisconnectListener listener, java.util.concurrent.Executor executor);
     method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
-    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotState> getComplicationSlotsState();
-    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.time.Instant getPreviewReferenceInstant();
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotState> getComplicationSlotsState() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.time.Instant getPreviewReferenceInstant() throws android.os.RemoteException;
     method public default androidx.wear.watchface.style.UserStyleFlavors getUserStyleFlavors();
-    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws android.os.RemoteException;
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public default byte[] getUserStyleSchemaDigestHash() throws android.os.RemoteException;
     method @AnyThread public boolean isConnectionAlive();
     method @AnyThread public void removeClientDisconnectListener(androidx.wear.watchface.client.HeadlessWatchFaceClient.ClientDisconnectListener listener);
@@ -140,12 +140,13 @@
     method @androidx.wear.watchface.client.WatchFaceClientExperimental public default void addOnWatchFaceColorsListener(java.util.concurrent.Executor executor, java.util.function.Consumer<androidx.wear.watchface.WatchFaceColors> listener);
     method public void addOnWatchFaceReadyListener(java.util.concurrent.Executor executor, androidx.wear.watchface.client.InteractiveWatchFaceClient.OnWatchFaceReadyListener listener);
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public default Integer? getComplicationIdAt(@Px int x, @Px int y) throws android.os.RemoteException;
-    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotState> getComplicationSlotsState();
-    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.List<androidx.wear.watchface.ContentDescriptionLabel> getContentDescriptionLabels();
-    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public String getInstanceId();
-    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 @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotState> getComplicationSlotsState() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.List<androidx.wear.watchface.ContentDescriptionLabel> getContentDescriptionLabels() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public String getInstanceId() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public default androidx.wear.watchface.client.OverlayStyle getOverlayStyle() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.time.Instant getPreviewReferenceInstant() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws android.os.RemoteException;
+    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.ignore b/wear/watchface/watchface-client/api/restricted_current.ignore
index 43ce51a..8c1c73c 100644
--- a/wear/watchface/watchface-client/api/restricted_current.ignore
+++ b/wear/watchface/watchface-client/api/restricted_current.ignore
@@ -1,3 +1,23 @@
 // Baseline format: 1.0
 AddedAbstractMethod: androidx.wear.watchface.client.WatchFaceMetadataClient#getUserStyleFlavors():
     Added method androidx.wear.watchface.client.WatchFaceMetadataClient.getUserStyleFlavors()
+
+
+ChangedThrows: androidx.wear.watchface.client.HeadlessWatchFaceClient#getComplicationSlotsState():
+    Method androidx.wear.watchface.client.HeadlessWatchFaceClient.getComplicationSlotsState added thrown exception android.os.RemoteException
+ChangedThrows: androidx.wear.watchface.client.HeadlessWatchFaceClient#getPreviewReferenceInstant():
+    Method androidx.wear.watchface.client.HeadlessWatchFaceClient.getPreviewReferenceInstant added thrown exception android.os.RemoteException
+ChangedThrows: androidx.wear.watchface.client.HeadlessWatchFaceClient#getUserStyleSchema():
+    Method androidx.wear.watchface.client.HeadlessWatchFaceClient.getUserStyleSchema added thrown exception android.os.RemoteException
+ChangedThrows: androidx.wear.watchface.client.InteractiveWatchFaceClient#getComplicationSlotsState():
+    Method androidx.wear.watchface.client.InteractiveWatchFaceClient.getComplicationSlotsState added thrown exception android.os.RemoteException
+ChangedThrows: androidx.wear.watchface.client.InteractiveWatchFaceClient#getContentDescriptionLabels():
+    Method androidx.wear.watchface.client.InteractiveWatchFaceClient.getContentDescriptionLabels added thrown exception android.os.RemoteException
+ChangedThrows: androidx.wear.watchface.client.InteractiveWatchFaceClient#getInstanceId():
+    Method androidx.wear.watchface.client.InteractiveWatchFaceClient.getInstanceId added thrown exception android.os.RemoteException
+ChangedThrows: androidx.wear.watchface.client.InteractiveWatchFaceClient#getOverlayStyle():
+    Method androidx.wear.watchface.client.InteractiveWatchFaceClient.getOverlayStyle added thrown exception android.os.RemoteException
+ChangedThrows: androidx.wear.watchface.client.InteractiveWatchFaceClient#getPreviewReferenceInstant():
+    Method androidx.wear.watchface.client.InteractiveWatchFaceClient.getPreviewReferenceInstant added thrown exception android.os.RemoteException
+ChangedThrows: androidx.wear.watchface.client.InteractiveWatchFaceClient#getUserStyleSchema():
+    Method androidx.wear.watchface.client.InteractiveWatchFaceClient.getUserStyleSchema added thrown exception android.os.RemoteException
diff --git a/wear/watchface/watchface-client/api/restricted_current.txt b/wear/watchface/watchface-client/api/restricted_current.txt
index 40bcc03..4e6173b 100644
--- a/wear/watchface/watchface-client/api/restricted_current.txt
+++ b/wear/watchface/watchface-client/api/restricted_current.txt
@@ -106,10 +106,10 @@
   public interface HeadlessWatchFaceClient extends java.lang.AutoCloseable {
     method @AnyThread public void addClientDisconnectListener(androidx.wear.watchface.client.HeadlessWatchFaceClient.ClientDisconnectListener listener, java.util.concurrent.Executor executor);
     method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
-    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotState> getComplicationSlotsState();
-    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.time.Instant getPreviewReferenceInstant();
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotState> getComplicationSlotsState() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.time.Instant getPreviewReferenceInstant() throws android.os.RemoteException;
     method public default androidx.wear.watchface.style.UserStyleFlavors getUserStyleFlavors();
-    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws android.os.RemoteException;
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public default byte[] getUserStyleSchemaDigestHash() throws android.os.RemoteException;
     method @AnyThread public boolean isConnectionAlive();
     method @AnyThread public void removeClientDisconnectListener(androidx.wear.watchface.client.HeadlessWatchFaceClient.ClientDisconnectListener listener);
@@ -135,12 +135,13 @@
     method @AnyThread public void addClientDisconnectListener(androidx.wear.watchface.client.InteractiveWatchFaceClient.ClientDisconnectListener listener, java.util.concurrent.Executor executor);
     method public void addOnWatchFaceReadyListener(java.util.concurrent.Executor executor, androidx.wear.watchface.client.InteractiveWatchFaceClient.OnWatchFaceReadyListener listener);
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public default Integer? getComplicationIdAt(@Px int x, @Px int y) throws android.os.RemoteException;
-    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotState> getComplicationSlotsState();
-    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.List<androidx.wear.watchface.ContentDescriptionLabel> getContentDescriptionLabels();
-    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public String getInstanceId();
-    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 @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotState> getComplicationSlotsState() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.List<androidx.wear.watchface.ContentDescriptionLabel> getContentDescriptionLabels() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public String getInstanceId() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public default androidx.wear.watchface.client.OverlayStyle getOverlayStyle() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.time.Instant getPreviewReferenceInstant() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws android.os.RemoteException;
+    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/DeviceConfig.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/DeviceConfig.kt
index 220def1..bc924c6 100644
--- a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/DeviceConfig.kt
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/DeviceConfig.kt
@@ -16,10 +16,9 @@
 
 package androidx.wear.watchface.client
 
+import androidx.wear.watchface.data.DeviceConfig as WireDeviceConfig
 import androidx.annotation.RestrictTo
 
-typealias WireDeviceConfig = androidx.wear.watchface.data.DeviceConfig
-
 /**
  * Describes the hardware configuration of the device the watch face is running on.
  *
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-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
index 91bc15b..2bc3ba1 100644
--- a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
@@ -502,6 +502,7 @@
                             it.value.asWireComplicationData()
                         )
                     },
+                    null,
                     null
                 ),
                 object : IPendingInteractiveWatchFace.Stub() {
diff --git a/wear/watchface/watchface-complications-data-source/api/current.ignore b/wear/watchface/watchface-complications-data-source/api/current.ignore
new file mode 100644
index 0000000..bc01d17
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-source/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedClass: androidx.wear.watchface.complications.datasource.ComplicationDataTimelineKt:
+    Removed class androidx.wear.watchface.complications.datasource.ComplicationDataTimelineKt
diff --git a/wear/watchface/watchface-complications-data-source/api/current.txt b/wear/watchface/watchface-complications-data-source/api/current.txt
index 0aaf430..73e5e61 100644
--- a/wear/watchface/watchface-complications-data-source/api/current.txt
+++ b/wear/watchface/watchface-complications-data-source/api/current.txt
@@ -51,9 +51,6 @@
     property public final java.util.Collection<androidx.wear.watchface.complications.datasource.TimelineEntry> timelineEntries;
   }
 
-  public final class ComplicationDataTimelineKt {
-  }
-
   public final class ComplicationRequest {
     ctor public ComplicationRequest(int complicationInstanceId, androidx.wear.watchface.complications.data.ComplicationType complicationType, boolean immediateResponseRequired);
     ctor @Deprecated public ComplicationRequest(int complicationInstanceId, androidx.wear.watchface.complications.data.ComplicationType complicationType);
diff --git a/wear/watchface/watchface-complications-data-source/api/public_plus_experimental_current.txt b/wear/watchface/watchface-complications-data-source/api/public_plus_experimental_current.txt
index 0aaf430..73e5e61 100644
--- a/wear/watchface/watchface-complications-data-source/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-complications-data-source/api/public_plus_experimental_current.txt
@@ -51,9 +51,6 @@
     property public final java.util.Collection<androidx.wear.watchface.complications.datasource.TimelineEntry> timelineEntries;
   }
 
-  public final class ComplicationDataTimelineKt {
-  }
-
   public final class ComplicationRequest {
     ctor public ComplicationRequest(int complicationInstanceId, androidx.wear.watchface.complications.data.ComplicationType complicationType, boolean immediateResponseRequired);
     ctor @Deprecated public ComplicationRequest(int complicationInstanceId, androidx.wear.watchface.complications.data.ComplicationType complicationType);
diff --git a/wear/watchface/watchface-complications-data-source/api/restricted_current.ignore b/wear/watchface/watchface-complications-data-source/api/restricted_current.ignore
new file mode 100644
index 0000000..bc01d17
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-source/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedClass: androidx.wear.watchface.complications.datasource.ComplicationDataTimelineKt:
+    Removed class androidx.wear.watchface.complications.datasource.ComplicationDataTimelineKt
diff --git a/wear/watchface/watchface-complications-data-source/api/restricted_current.txt b/wear/watchface/watchface-complications-data-source/api/restricted_current.txt
index 0aaf430..73e5e61 100644
--- a/wear/watchface/watchface-complications-data-source/api/restricted_current.txt
+++ b/wear/watchface/watchface-complications-data-source/api/restricted_current.txt
@@ -51,9 +51,6 @@
     property public final java.util.Collection<androidx.wear.watchface.complications.datasource.TimelineEntry> timelineEntries;
   }
 
-  public final class ComplicationDataTimelineKt {
-  }
-
   public final class ComplicationRequest {
     ctor public ComplicationRequest(int complicationInstanceId, androidx.wear.watchface.complications.data.ComplicationType complicationType, boolean immediateResponseRequired);
     ctor @Deprecated public ComplicationRequest(int complicationInstanceId, androidx.wear.watchface.complications.data.ComplicationType complicationType);
diff --git a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
index e5d3585..442d906 100644
--- a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
+++ b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
@@ -43,6 +43,8 @@
 import java.util.concurrent.CountDownLatch
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.android.asCoroutineDispatcher
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
 
 /**
@@ -491,14 +493,12 @@
             complicationInstanceId: Int,
         ) {
             mainThreadCoroutineScope.launch {
-                data.collect { evaluatedData ->
-                    if (evaluatedData == null) return@collect // Ignore pre-evaluation.
-                    close() // Doing one-off evaluation, the service will be re-invoked.
-                    iComplicationManager.updateComplicationData(
-                        complicationInstanceId,
-                        evaluatedData
-                    )
-                }
+                // Doing one-off evaluation, the service will be re-invoked.
+                iComplicationManager.updateComplicationData(
+                    complicationInstanceId,
+                    data.filterNotNull().first()
+                )
+                close()
             }
         }
 
diff --git a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt
index 868b87e..68018b6 100644
--- a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt
+++ b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt
@@ -16,13 +16,11 @@
 
 package androidx.wear.watchface.complications.datasource
 
+import android.support.wearable.complications.ComplicationData as WireComplicationData
 import androidx.wear.watchface.complications.data.ComplicationData
 import androidx.wear.watchface.complications.data.NoDataComplicationData
 import java.time.Instant
 
-/** The wire format for [ComplicationData]. */
-internal typealias WireComplicationData = android.support.wearable.complications.ComplicationData
-
 /**
  * A time interval, typically used to describe the validity period of a [TimelineEntry].
  *
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
index ef21c7a..41954c9 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
@@ -2199,38 +2199,45 @@
             TYPE_WEIGHTED_ELEMENTS to setOf(),
         )
 
+        private val COMMON_OPTIONAL_FIELDS: Array<String> = arrayOf(
+            FIELD_TAP_ACTION,
+            FIELD_CONTENT_DESCRIPTION,
+            FIELD_DATA_SOURCE,
+            FIELD_PERSISTENCE_POLICY,
+            FIELD_DISPLAY_POLICY,
+            FIELD_TIMELINE_START_TIME,
+            FIELD_TIMELINE_END_TIME,
+            FIELD_START_TIME,
+            FIELD_END_TIME,
+            FIELD_TIMELINE_ENTRIES,
+            FIELD_TIMELINE_ENTRY_TYPE,
+        )
+
         // Used for validation. OPTIONAL_FIELDS[i] is a list containing all the fields which are
         // valid but not required for type i.
         private val OPTIONAL_FIELDS: Map<Int, Set<String>> = mapOf(
             TYPE_NOT_CONFIGURED to setOf(),
             TYPE_EMPTY to setOf(),
             TYPE_SHORT_TEXT to setOf(
+                *COMMON_OPTIONAL_FIELDS,
                 FIELD_SHORT_TITLE,
                 FIELD_ICON,
                 FIELD_ICON_BURN_IN_PROTECTION,
                 FIELD_SMALL_IMAGE,
                 FIELD_SMALL_IMAGE_BURN_IN_PROTECTION,
                 FIELD_IMAGE_STYLE,
-                FIELD_TAP_ACTION,
-                FIELD_CONTENT_DESCRIPTION,
-                FIELD_DATA_SOURCE,
-                FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY,
             ),
             TYPE_LONG_TEXT to setOf(
+                *COMMON_OPTIONAL_FIELDS,
                 FIELD_LONG_TITLE,
                 FIELD_ICON,
                 FIELD_ICON_BURN_IN_PROTECTION,
                 FIELD_SMALL_IMAGE,
                 FIELD_SMALL_IMAGE_BURN_IN_PROTECTION,
                 FIELD_IMAGE_STYLE,
-                FIELD_TAP_ACTION,
-                FIELD_CONTENT_DESCRIPTION,
-                FIELD_DATA_SOURCE,
-                FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY,
             ),
             TYPE_RANGED_VALUE to setOf(
+                *COMMON_OPTIONAL_FIELDS,
                 FIELD_SHORT_TEXT,
                 FIELD_SHORT_TITLE,
                 FIELD_ICON,
@@ -2238,39 +2245,23 @@
                 FIELD_SMALL_IMAGE,
                 FIELD_SMALL_IMAGE_BURN_IN_PROTECTION,
                 FIELD_IMAGE_STYLE,
-                FIELD_TAP_ACTION,
-                FIELD_CONTENT_DESCRIPTION,
-                FIELD_DATA_SOURCE,
                 FIELD_COLOR_RAMP,
                 FIELD_COLOR_RAMP_INTERPOLATED,
-                FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY,
                 FIELD_VALUE_TYPE,
             ),
             TYPE_ICON to setOf(
-                FIELD_TAP_ACTION,
+                *COMMON_OPTIONAL_FIELDS,
                 FIELD_ICON_BURN_IN_PROTECTION,
-                FIELD_CONTENT_DESCRIPTION,
-                FIELD_DATA_SOURCE,
-                FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY,
             ),
             TYPE_SMALL_IMAGE to setOf(
-                FIELD_TAP_ACTION,
+                *COMMON_OPTIONAL_FIELDS,
                 FIELD_SMALL_IMAGE_BURN_IN_PROTECTION,
-                FIELD_CONTENT_DESCRIPTION,
-                FIELD_DATA_SOURCE,
-                FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY,
             ),
             TYPE_LARGE_IMAGE to setOf(
-                FIELD_TAP_ACTION,
-                FIELD_CONTENT_DESCRIPTION,
-                FIELD_DATA_SOURCE,
-                FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY,
+                *COMMON_OPTIONAL_FIELDS,
             ),
             TYPE_NO_PERMISSION to setOf(
+                *COMMON_OPTIONAL_FIELDS,
                 FIELD_SHORT_TEXT,
                 FIELD_SHORT_TITLE,
                 FIELD_ICON,
@@ -2278,20 +2269,14 @@
                 FIELD_SMALL_IMAGE,
                 FIELD_SMALL_IMAGE_BURN_IN_PROTECTION,
                 FIELD_IMAGE_STYLE,
-                FIELD_CONTENT_DESCRIPTION,
-                FIELD_DATA_SOURCE,
-                FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY,
             ),
             TYPE_NO_DATA to setOf(
+                *COMMON_OPTIONAL_FIELDS,
                 FIELD_COLOR_RAMP,
                 FIELD_COLOR_RAMP_INTERPOLATED,
-                FIELD_DATA_SOURCE,
-                FIELD_DISPLAY_POLICY,
                 FIELD_ELEMENT_BACKGROUND_COLOR,
                 FIELD_ELEMENT_COLORS,
                 FIELD_ELEMENT_WEIGHTS,
-                FIELD_END_TIME,
                 FIELD_ICON,
                 FIELD_ICON_BURN_IN_PROTECTION,
                 FIELD_IMAGE_STYLE,
@@ -2300,21 +2285,14 @@
                 FIELD_LONG_TEXT,
                 FIELD_MAX_VALUE,
                 FIELD_MIN_VALUE,
-                FIELD_PERSISTENCE_POLICY,
                 FIELD_PLACEHOLDER_FIELDS,
                 FIELD_PLACEHOLDER_TYPE,
                 FIELD_SMALL_IMAGE,
                 FIELD_SMALL_IMAGE_BURN_IN_PROTECTION,
                 FIELD_SHORT_TITLE,
                 FIELD_SHORT_TEXT,
-                FIELD_START_TIME,
-                FIELD_TAP_ACTION,
                 FIELD_TAP_ACTION_LOST,
                 FIELD_TARGET_VALUE,
-                FIELD_TIMELINE_START_TIME,
-                FIELD_TIMELINE_END_TIME,
-                FIELD_TIMELINE_ENTRIES,
-                FIELD_TIMELINE_ENTRY_TYPE,
                 FIELD_VALUE,
                 FIELD_VALUE_EXPRESSION,
                 FIELD_VALUE_TYPE,
@@ -2324,24 +2302,16 @@
                 EXP_FIELD_PROTO_LAYOUT_AMBIENT,
                 EXP_FIELD_PROTO_LAYOUT_INTERACTIVE,
                 EXP_FIELD_PROTO_LAYOUT_RESOURCES,
-                FIELD_CONTENT_DESCRIPTION,
             ),
             EXP_TYPE_PROTO_LAYOUT to setOf(
-                FIELD_TAP_ACTION,
-                FIELD_CONTENT_DESCRIPTION,
-                FIELD_DATA_SOURCE,
-                FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY,
+                *COMMON_OPTIONAL_FIELDS,
             ),
             EXP_TYPE_LIST to setOf(
-                FIELD_TAP_ACTION,
+                *COMMON_OPTIONAL_FIELDS,
                 EXP_FIELD_LIST_STYLE_HINT,
-                FIELD_CONTENT_DESCRIPTION,
-                FIELD_DATA_SOURCE,
-                FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY,
             ),
             TYPE_GOAL_PROGRESS to setOf(
+                *COMMON_OPTIONAL_FIELDS,
                 FIELD_SHORT_TEXT,
                 FIELD_SHORT_TITLE,
                 FIELD_ICON,
@@ -2349,15 +2319,11 @@
                 FIELD_SMALL_IMAGE,
                 FIELD_SMALL_IMAGE_BURN_IN_PROTECTION,
                 FIELD_IMAGE_STYLE,
-                FIELD_TAP_ACTION,
-                FIELD_CONTENT_DESCRIPTION,
-                FIELD_DATA_SOURCE,
                 FIELD_COLOR_RAMP,
                 FIELD_COLOR_RAMP_INTERPOLATED,
-                FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY,
             ),
             TYPE_WEIGHTED_ELEMENTS to setOf(
+                *COMMON_OPTIONAL_FIELDS,
                 FIELD_SHORT_TEXT,
                 FIELD_SHORT_TITLE,
                 FIELD_ICON,
@@ -2365,11 +2331,6 @@
                 FIELD_SMALL_IMAGE,
                 FIELD_SMALL_IMAGE_BURN_IN_PROTECTION,
                 FIELD_IMAGE_STYLE,
-                FIELD_TAP_ACTION,
-                FIELD_CONTENT_DESCRIPTION,
-                FIELD_DATA_SOURCE,
-                FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY,
             ),
         )
 
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java
index 0666f3f..58f43c9 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java
@@ -287,7 +287,8 @@
 
     private CharSequence mDependentTextCache;
 
-    public ComplicationText(@Nullable CharSequence surroundingText,
+    private ComplicationText(
+            @Nullable CharSequence surroundingText,
             @Nullable TimeDependentText timeDependentText,
             @Nullable StringExpression stringExpression) {
         mSurroundingText = surroundingText;
@@ -296,6 +297,20 @@
         checkFields();
     }
 
+    public ComplicationText(@NonNull CharSequence surroundingText) {
+        this(surroundingText, /* timeDependentText = */ null, /* stringExpression = */ null);
+    }
+
+    public ComplicationText(
+            @NonNull CharSequence surroundingText, @NonNull TimeDependentText timeDependentText) {
+        this(surroundingText, timeDependentText, /* stringExpression = */ null);
+    }
+
+    public ComplicationText(
+            @NonNull CharSequence surroundingText, @NonNull StringExpression stringExpression) {
+        this(surroundingText, /* timeDependentText = */ null, stringExpression);
+    }
+
     public ComplicationText(@NonNull StringExpression stringExpression) {
         this(/* surroundingText = */ null, /* timeDependentText = */ null, stringExpression);
     }
@@ -375,6 +390,7 @@
             }
         }
 
+        @SuppressLint("SyntheticAccessor")
         Object readResolve() {
             return new ComplicationText(mSurroundingText, mTimeDependentText, mStringExpression);
         }
@@ -585,8 +601,7 @@
      */
     @NonNull
     public static ComplicationText plainText(@NonNull CharSequence text) {
-        return new ComplicationText(
-                text, /* timeDependentText= */ null, /* stringExpression= */ null);
+        return new ComplicationText(text);
     }
 
     /**
@@ -767,8 +782,7 @@
                             mReferencePeriodEndMillis,
                             mStyle,
                             showNowText,
-                            mMinimumUnit),
-                    /* stringExpression= */ null);
+                            mMinimumUnit));
         }
 
         /** Returns the default value for the 'show now text' option for the given {@code style}. */
@@ -856,8 +870,7 @@
         @SuppressLint("SyntheticAccessor")
         public ComplicationText build() {
             return new ComplicationText(
-                    mSurroundingText, new TimeFormatText(mFormat, mStyle, mTimeZone),
-                    /* stringExpression= */ null);
+                    mSurroundingText, new TimeFormatText(mFormat, mStyle, mTimeZone));
         }
     }
 }
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..afa5db9 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
@@ -16,9 +16,11 @@
 
 package androidx.wear.watchface.complications.data
 
+import android.support.wearable.complications.ComplicationData as WireComplicationData
 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 +67,7 @@
             listeners[listener] = CoroutineScope(executor.asCoroutineDispatcher()).apply {
                 launch {
                     data.collect {
-                        if (it != null) listener(it)
+                        if (it != null) listener.accept(it)
                     }
                 }
             }
@@ -90,4 +92,4 @@
     }
 }
 
-private typealias Listener = (WireComplicationData) -> Unit
+private typealias Listener = Consumer<WireComplicationData>
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
index 7dbbbac..90dc690 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
@@ -18,6 +18,8 @@
 
 package androidx.wear.watchface.complications.data
 
+import android.support.wearable.complications.ComplicationData as WireComplicationData
+import android.support.wearable.complications.ComplicationData.Builder as WireComplicationDataBuilder
 import android.app.PendingIntent
 import android.content.ComponentName
 import android.graphics.Color
@@ -37,13 +39,6 @@
 import androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element
 import java.time.Instant
 
-/** The wire format for [ComplicationData]. */
-internal typealias WireComplicationData = android.support.wearable.complications.ComplicationData
-
-/** The builder for [WireComplicationData]. */
-internal typealias WireComplicationDataBuilder =
-    android.support.wearable.complications.ComplicationData.Builder
-
 internal const val TAG = "Data.kt"
 
 /**
@@ -141,18 +136,23 @@
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public abstract fun asWireComplicationData(): WireComplicationData
+    public fun asWireComplicationData(): WireComplicationData {
+        cachedWireComplicationData?.let { return it }
+        return createWireComplicationDataBuilder()
+            .apply { fillWireComplicationDataBuilder(this) }
+            .build()
+            .also { cachedWireComplicationData = it }
+    }
 
     internal fun createWireComplicationDataBuilder(): WireComplicationDataBuilder =
         cachedWireComplicationData?.let {
             WireComplicationDataBuilder(it)
-        } ?: WireComplicationDataBuilder(type.toWireComplicationType()).apply {
-            setDataSource(dataSource)
-            setPersistencePolicy(persistencePolicy)
-            setDisplayPolicy(displayPolicy)
-        }
+        } ?: WireComplicationDataBuilder(type.toWireComplicationType())
 
     internal open fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+        builder.setDataSource(dataSource)
+        builder.setPersistencePolicy(persistencePolicy)
+        builder.setDisplayPolicy(displayPolicy)
     }
 
     /**
@@ -177,6 +177,13 @@
         other is ComplicationData &&
             asWireComplicationData() == other.asWireComplicationData()
 
+    /**
+     * Similar to [equals], but avoids comparing evaluated fields (if expressions exist).
+     * @hide
+     */
+    infix fun equalsUnevaluated(other: ComplicationData): Boolean =
+        asWireComplicationData() equalsUnevaluated other.asWireComplicationData()
+
     override fun hashCode(): Int = asWireComplicationData().hashCode()
 
     /**
@@ -189,6 +196,14 @@
         internal var persistencePolicy = ComplicationPersistencePolicies.CACHING_ALLOWED
         internal var displayPolicy = ComplicationDisplayPolicies.ALWAYS_DISPLAY
 
+        @Suppress("NewApi")
+        internal fun setCommon(data: WireComplicationData) = apply {
+            setCachedWireComplicationData(data)
+            setDataSource(data.dataSource)
+            setPersistencePolicy(data.persistencePolicy)
+            setDisplayPolicy(data.displayPolicy)
+        }
+
         /**
          * Sets the [ComponentName] of the ComplicationDataSourceService that provided this
          * ComplicationData, if any.
@@ -292,21 +307,15 @@
             else -> null
         }
 
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    override fun asWireComplicationData(): WireComplicationData {
-        cachedWireComplicationData?.let {
-            return it
+    override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+        super.fillWireComplicationDataBuilder(builder)
+        if (placeholder == null) {
+            builder.setPlaceholder(null)
+        } else {
+            val placeholderBuilder = placeholder.createWireComplicationDataBuilder()
+            placeholder.fillWireComplicationDataBuilder(placeholderBuilder)
+            builder.setPlaceholder(placeholderBuilder.build())
         }
-        return createWireComplicationDataBuilder().apply {
-            if (placeholder == null) {
-                setPlaceholder(null)
-            } else {
-                val builder = placeholder.createWireComplicationDataBuilder()
-                placeholder.fillWireComplicationDataBuilder(builder)
-                setPlaceholder(builder.build())
-            }
-        }.build().also { cachedWireComplicationData = it }
     }
 
     override fun toString(): String {
@@ -338,9 +347,8 @@
     persistencePolicy = ComplicationPersistencePolicies.CACHING_ALLOWED,
     displayPolicy = ComplicationDisplayPolicies.ALWAYS_DISPLAY
 ) {
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    override fun asWireComplicationData(): WireComplicationData = asPlainWireComplicationData(type)
+    // Always empty.
+    override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {}
 
     override fun toString(): String {
         return "EmptyComplicationData()"
@@ -368,9 +376,8 @@
     persistencePolicy = ComplicationPersistencePolicies.CACHING_ALLOWED,
     displayPolicy = ComplicationDisplayPolicies.ALWAYS_DISPLAY
 ) {
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    override fun asWireComplicationData(): WireComplicationData = asPlainWireComplicationData(type)
+    // Always empty.
+    override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {}
 
     override fun toString(): String {
         return "NotConfiguredComplicationData()"
@@ -508,18 +515,8 @@
             )
     }
 
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    override fun asWireComplicationData(): WireComplicationData {
-        cachedWireComplicationData?.let {
-            return it
-        }
-        return createWireComplicationDataBuilder().apply {
-            fillWireComplicationDataBuilder(this)
-        }.build().also { cachedWireComplicationData = it }
-    }
-
     override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+        super.fillWireComplicationDataBuilder(builder)
         builder.setShortText(text.toWireComplicationText())
         builder.setShortTitle(title?.toWireComplicationText())
         builder.setContentDescription(
@@ -689,18 +686,8 @@
             )
     }
 
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    override fun asWireComplicationData(): WireComplicationData {
-        cachedWireComplicationData?.let {
-            return it
-        }
-        return createWireComplicationDataBuilder().apply {
-            fillWireComplicationDataBuilder(this)
-        }.build().also { cachedWireComplicationData = it }
-    }
-
     override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+        super.fillWireComplicationDataBuilder(builder)
         builder.setLongText(text.toWireComplicationText())
         builder.setLongTitle(title?.toWireComplicationText())
         monochromaticImage?.addToWireComplicationData(builder)
@@ -1055,18 +1042,8 @@
         }
     }
 
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public override fun asWireComplicationData(): WireComplicationData {
-        cachedWireComplicationData?.let {
-            return it
-        }
-        return createWireComplicationDataBuilder().apply {
-            fillWireComplicationDataBuilder(this)
-        }.build().also { cachedWireComplicationData = it }
-    }
-
     override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+        super.fillWireComplicationDataBuilder(builder)
         builder.setRangedValue(value)
         builder.setRangedValueExpression(valueExpression)
         builder.setRangedMinValue(min)
@@ -1390,18 +1367,8 @@
         }
     }
 
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public override fun asWireComplicationData(): WireComplicationData {
-        cachedWireComplicationData?.let {
-            return it
-        }
-        return createWireComplicationDataBuilder().apply {
-            fillWireComplicationDataBuilder(this)
-        }.build().also { cachedWireComplicationData = it }
-    }
-
     override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+        super.fillWireComplicationDataBuilder(builder)
         builder.setRangedValue(value)
         builder.setRangedValueExpression(valueExpression)
         builder.setTargetValue(targetValue)
@@ -1707,18 +1674,8 @@
         }
     }
 
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public override fun asWireComplicationData(): WireComplicationData {
-        cachedWireComplicationData?.let {
-            return it
-        }
-        return createWireComplicationDataBuilder().apply {
-            fillWireComplicationDataBuilder(this)
-        }.build().also { cachedWireComplicationData = it }
-    }
-
     override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+        super.fillWireComplicationDataBuilder(builder)
         builder.setElementWeights(elements.map { it.weight }.toFloatArray())
         builder.setElementColors(elements.map { it.color }.toIntArray())
         builder.setElementBackgroundColor(elementBackgroundColor)
@@ -1787,7 +1744,7 @@
          * large  number of elements we likely won't be able to render them properly because the
          * individual elements will be too small on screen. */
         @JvmStatic
-        public fun getMaxElements() = 20
+        public fun getMaxElements() = 7
     }
 }
 
@@ -1865,18 +1822,8 @@
             )
     }
 
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    override fun asWireComplicationData(): WireComplicationData {
-        cachedWireComplicationData?.let {
-            return it
-        }
-        return createWireComplicationDataBuilder().apply {
-            fillWireComplicationDataBuilder(this)
-        }.build().also { cachedWireComplicationData = it }
-    }
-
     override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+        super.fillWireComplicationDataBuilder(builder)
         monochromaticImage.addToWireComplicationData(builder)
         builder.setContentDescription(
             when (contentDescription) {
@@ -1981,18 +1928,8 @@
             )
     }
 
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    override fun asWireComplicationData(): WireComplicationData {
-        cachedWireComplicationData?.let {
-            return it
-        }
-        return createWireComplicationDataBuilder().apply {
-            fillWireComplicationDataBuilder(this)
-        }.build().also { cachedWireComplicationData = it }
-    }
-
     override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+        super.fillWireComplicationDataBuilder(builder)
         smallImage.addToWireComplicationData(builder)
         builder.setContentDescription(
             when (contentDescription) {
@@ -2103,18 +2040,8 @@
             )
     }
 
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    override fun asWireComplicationData(): WireComplicationData {
-        cachedWireComplicationData?.let {
-            return it
-        }
-        return createWireComplicationDataBuilder().apply {
-            fillWireComplicationDataBuilder(this)
-        }.build().also { cachedWireComplicationData = it }
-    }
-
     override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+        super.fillWireComplicationDataBuilder(builder)
         builder.setLargeImage(photoImage)
         builder.setContentDescription(
             when (contentDescription) {
@@ -2243,18 +2170,12 @@
             )
     }
 
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    override fun asWireComplicationData(): WireComplicationData {
-        cachedWireComplicationData?.let {
-            return it
-        }
-        return createWireComplicationDataBuilder().apply {
-            setShortText(text?.toWireComplicationText())
-            setShortTitle(title?.toWireComplicationText())
-            monochromaticImage?.addToWireComplicationData(this)
-            smallImage?.addToWireComplicationData(this)
-        }.build().also { cachedWireComplicationData = it }
+    override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+        super.fillWireComplicationDataBuilder(builder)
+        builder.setShortText(text?.toWireComplicationText())
+        builder.setShortTitle(title?.toWireComplicationText())
+        monochromaticImage?.addToWireComplicationData(builder)
+        smallImage?.addToWireComplicationData(builder)
     }
 
     override fun toString(): String {
@@ -2453,16 +2374,11 @@
 @Suppress("NewApi")
 public fun WireComplicationData.toApiComplicationData(): ComplicationData {
     try {
-        // Make sure we use the correct dataSource, persistencePolicy & displayPolicy.
-        val dataSourceCopy = dataSource
-        val persistencePolicyCopy = persistencePolicy
-        val displayPolicyCopy = displayPolicy
-        val wireComplicationData = this
         return when (type) {
             NoDataComplicationData.TYPE.toWireComplicationType() -> {
                 placeholder?.toPlaceholderComplicationData()?.let {
-                    NoDataComplicationData(it)
-                } ?: NoDataComplicationData()
+                    NoDataComplicationData(it, this@toApiComplicationData)
+                } ?: NoDataComplicationData(null, this@toApiComplicationData)
             }
 
             EmptyComplicationData.TYPE.toWireComplicationType() -> EmptyComplicationData()
@@ -2475,15 +2391,12 @@
                     shortText!!.toApiComplicationText(),
                     contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
                 ).apply {
+                    setCommon(this@toApiComplicationData)
                     setTapAction(tapAction)
                     setValidTimeRange(parseTimeRange())
                     setTitle(shortTitle?.toApiComplicationText())
                     setMonochromaticImage(parseIcon())
                     setSmallImage(parseSmallImage())
-                    setCachedWireComplicationData(wireComplicationData)
-                    setDataSource(dataSourceCopy)
-                    setPersistencePolicy(persistencePolicyCopy)
-                    setDisplayPolicy(displayPolicyCopy)
                 }.build()
 
             LongTextComplicationData.TYPE.toWireComplicationType() ->
@@ -2491,15 +2404,12 @@
                     longText!!.toApiComplicationText(),
                     contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
                 ).apply {
+                    setCommon(this@toApiComplicationData)
                     setTapAction(tapAction)
                     setValidTimeRange(parseTimeRange())
                     setTitle(longTitle?.toApiComplicationText())
                     setMonochromaticImage(parseIcon())
                     setSmallImage(parseSmallImage())
-                    setCachedWireComplicationData(wireComplicationData)
-                    setDataSource(dataSourceCopy)
-                    setPersistencePolicy(persistencePolicyCopy)
-                    setDisplayPolicy(displayPolicyCopy)
                 }.build()
 
             RangedValueComplicationData.TYPE.toWireComplicationType() ->
@@ -2511,19 +2421,16 @@
                     contentDescription = contentDescription?.toApiComplicationText()
                         ?: ComplicationText.EMPTY
                 ).apply {
+                    setCommon(this@toApiComplicationData)
                     setTapAction(tapAction)
                     setValidTimeRange(parseTimeRange())
                     setMonochromaticImage(parseIcon())
                     setSmallImage(parseSmallImage())
                     setTitle(shortTitle?.toApiComplicationText())
                     setText(shortText?.toApiComplicationText())
-                    setCachedWireComplicationData(wireComplicationData)
                     colorRamp?.let {
                         setColorRamp(ColorRamp(it, isColorRampInterpolated!!))
                     }
-                    setDataSource(dataSourceCopy)
-                    setPersistencePolicy(persistencePolicyCopy)
-                    setDisplayPolicy(displayPolicyCopy)
                     setValueType(rangedValueType)
                 }.build()
 
@@ -2532,12 +2439,9 @@
                     parseIcon()!!,
                     contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
                 ).apply {
+                    setCommon(this@toApiComplicationData)
                     setTapAction(tapAction)
                     setValidTimeRange(parseTimeRange())
-                    setCachedWireComplicationData(wireComplicationData)
-                    setDataSource(dataSourceCopy)
-                    setPersistencePolicy(persistencePolicyCopy)
-                    setDisplayPolicy(displayPolicyCopy)
                 }.build()
 
             SmallImageComplicationData.TYPE.toWireComplicationType() ->
@@ -2545,12 +2449,9 @@
                     parseSmallImage()!!,
                     contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
                 ).apply {
+                    setCommon(this@toApiComplicationData)
                     setTapAction(tapAction)
                     setValidTimeRange(parseTimeRange())
-                    setCachedWireComplicationData(wireComplicationData)
-                    setDataSource(dataSourceCopy)
-                    setPersistencePolicy(persistencePolicyCopy)
-                    setDisplayPolicy(displayPolicyCopy)
                 }.build()
 
             PhotoImageComplicationData.TYPE.toWireComplicationType() ->
@@ -2558,24 +2459,18 @@
                     largeImage!!,
                     contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
                 ).apply {
+                    setCommon(this@toApiComplicationData)
                     setTapAction(tapAction)
                     setValidTimeRange(parseTimeRange())
-                    setCachedWireComplicationData(wireComplicationData)
-                    setDataSource(dataSourceCopy)
-                    setPersistencePolicy(persistencePolicyCopy)
-                    setDisplayPolicy(displayPolicyCopy)
                 }.build()
 
             NoPermissionComplicationData.TYPE.toWireComplicationType() ->
                 NoPermissionComplicationData.Builder().apply {
+                    setCommon(this@toApiComplicationData)
                     setMonochromaticImage(parseIcon())
                     setSmallImage(parseSmallImage())
                     setTitle(shortTitle?.toApiComplicationText())
                     setText(shortText?.toApiComplicationText())
-                    setCachedWireComplicationData(wireComplicationData)
-                    setDataSource(dataSourceCopy)
-                    setPersistencePolicy(persistencePolicyCopy)
-                    setDisplayPolicy(displayPolicyCopy)
                 }.build()
 
             GoalProgressComplicationData.TYPE.toWireComplicationType() ->
@@ -2586,19 +2481,16 @@
                     contentDescription = contentDescription?.toApiComplicationText()
                         ?: ComplicationText.EMPTY
                 ).apply {
+                    setCommon(this@toApiComplicationData)
                     setTapAction(tapAction)
                     setValidTimeRange(parseTimeRange())
                     setMonochromaticImage(parseIcon())
                     setSmallImage(parseSmallImage())
                     setTitle(shortTitle?.toApiComplicationText())
                     setText(shortText?.toApiComplicationText())
-                    setCachedWireComplicationData(wireComplicationData)
                     colorRamp?.let {
                         setColorRamp(ColorRamp(it, isColorRampInterpolated!!))
                     }
-                    setDataSource(dataSourceCopy)
-                    setPersistencePolicy(persistencePolicyCopy)
-                    setDisplayPolicy(displayPolicyCopy)
                 }.build()
 
             WeightedElementsComplicationData.TYPE.toWireComplicationType() -> {
@@ -2613,6 +2505,7 @@
                     }.toList(),
                     contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
                 ).apply {
+                    setCommon(this@toApiComplicationData)
                     setElementBackgroundColor(elementBackgroundColor)
                     setTapAction(tapAction)
                     setValidTimeRange(parseTimeRange())
@@ -2620,10 +2513,6 @@
                     setSmallImage(parseSmallImage())
                     setTitle(shortTitle?.toApiComplicationText())
                     setText(shortText?.toApiComplicationText())
-                    setCachedWireComplicationData(wireComplicationData)
-                    setDataSource(dataSourceCopy)
-                    setPersistencePolicy(persistencePolicyCopy)
-                    setDisplayPolicy(displayPolicyCopy)
                 }.build()
             }
 
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Image.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Image.kt
index e965d2f..35bda85 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Image.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Image.kt
@@ -16,6 +16,7 @@
 
 package androidx.wear.watchface.complications.data
 
+import android.support.wearable.complications.ComplicationData as WireComplicationData
 import android.graphics.drawable.Icon
 import android.os.Build
 import androidx.annotation.RequiresApi
@@ -69,7 +70,7 @@
     }
 
     /** Adds a [MonochromaticImage] to a builder for [WireComplicationData]. */
-    internal fun addToWireComplicationData(builder: WireComplicationDataBuilder) = builder.apply {
+    internal fun addToWireComplicationData(builder: WireComplicationData.Builder) = builder.apply {
         setIcon(image)
         setBurnInProtectionIcon(ambientImage)
     }
@@ -173,7 +174,7 @@
     }
 
     /** Adds a [SmallImage] to a builder for [WireComplicationData]. */
-    internal fun addToWireComplicationData(builder: WireComplicationDataBuilder) = builder.apply {
+    internal fun addToWireComplicationData(builder: WireComplicationData.Builder) = builder.apply {
         setSmallImage(image)
         setSmallImageStyle(
             when (this@SmallImage.type) {
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
index f318733..5887dd6 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
@@ -16,6 +16,11 @@
 
 package androidx.wear.watchface.complications.data
 
+import android.support.wearable.complications.ComplicationData as WireComplicationData
+import android.support.wearable.complications.ComplicationText as WireComplicationText
+import android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder as WireComplicationTextTimeDifferenceBuilder
+import android.support.wearable.complications.ComplicationText.TimeFormatBuilder as WireComplicationTextTimeFormatBuilder
+import android.support.wearable.complications.TimeDependentText as WireTimeDependentText
 import android.content.res.Resources
 import android.icu.util.TimeZone
 import android.support.wearable.complications.TimeDependentText
@@ -32,17 +37,6 @@
 import java.time.Instant
 import java.util.concurrent.TimeUnit
 
-/** The wire format for [ComplicationText]. */
-internal typealias WireComplicationText = android.support.wearable.complications.ComplicationText
-
-private typealias WireComplicationTextTimeDifferenceBuilder =
-    android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder
-
-private typealias WireComplicationTextTimeFormatBuilder =
-    android.support.wearable.complications.ComplicationText.TimeFormatBuilder
-
-private typealias WireTimeDependentText = android.support.wearable.complications.TimeDependentText
-
 @JvmDefaultWithCompatibility
 /**
  * The text within a complication.
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Time.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Time.kt
index d479418..c402f16 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Time.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Time.kt
@@ -16,6 +16,7 @@
 
 package androidx.wear.watchface.complications.data
 
+import android.support.wearable.complications.ComplicationData as WireComplicationData
 import java.time.Instant
 
 /** A range of time, that may be unbounded on either side. */
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
index 49b6a23..f4dfa77 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
@@ -16,6 +16,7 @@
 
 package androidx.wear.watchface.complications.data
 
+import android.support.wearable.complications.ComplicationData as WireComplicationData
 import android.os.Build
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataEqualityTest.kt b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataEqualityTest.kt
index 196aedf..bde19c8 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataEqualityTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataEqualityTest.kt
@@ -265,19 +265,39 @@
             { setElementBackgroundColor(1) },
             { setElementBackgroundColor(2) },
         ),
+        TIMELINE_START_TIME(
+            { build().apply { timelineStartEpochSecond = 100 } },
+            { build().apply { timelineStartEpochSecond = 200 } },
+        ),
+        TIMELINE_END_TIME(
+            { build().apply { timelineEndEpochSecond = 100 } },
+            { build().apply { timelineEndEpochSecond = 200 } },
+        ),
+        TIMELINE_ENTRIES(
+            {
+                build().apply {
+                    setTimelineEntryCollection(
+                        listOf(ComplicationData.Builder(this).setRangedValue(1f).build())
+                    )
+                }
+            },
+            {
+                build().apply {
+                    setTimelineEntryCollection(
+                        listOf(ComplicationData.Builder(this).setRangedValue(2f).build())
+                    )
+                }
+            },
+        ),
         ;
 
+        private val base = ComplicationData.TYPE_NO_DATA
+
         /** Builds a [ComplicationData] with the first variation. */
-        fun buildOne() =
-            ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
-                .apply { setterOne(this) }
-                .build()
+        fun buildOne() = ComplicationData.Builder(base).apply { setterOne(this) }.build()
 
         /** Builds a [ComplicationData] with the second variation. */
-        fun buildTwo() =
-            ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
-                .apply { setterTwo(this) }
-                .build()
+        fun buildTwo() = ComplicationData.Builder(base).apply { setterTwo(this) }.build()
     }
 
     @Test
@@ -358,7 +378,6 @@
                 setShortTitle(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        /* timeDependentText = */ null,
                         StringExpression(byteArrayOf(1, 2))
                     )
                 )
@@ -367,7 +386,6 @@
                 setShortTitle(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        /* timeDependentText = */ null,
                         StringExpression(byteArrayOf(3, 4))
                     )
                 )
@@ -384,7 +402,6 @@
                 setShortText(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        /* timeDependentText = */ null,
                         StringExpression(byteArrayOf(1, 2))
                     )
                 )
@@ -393,7 +410,6 @@
                 setShortText(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        /* timeDependentText = */ null,
                         StringExpression(byteArrayOf(3, 4))
                     )
                 )
@@ -410,7 +426,6 @@
                 setLongTitle(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        /* timeDependentText = */ null,
                         StringExpression(byteArrayOf(1, 2))
                     )
                 )
@@ -419,7 +434,6 @@
                 setLongTitle(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        /* timeDependentText = */ null,
                         StringExpression(byteArrayOf(3, 4))
                     )
                 )
@@ -436,7 +450,6 @@
                 setLongText(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        /* timeDependentText = */ null,
                         StringExpression(byteArrayOf(1, 2))
                     )
                 )
@@ -445,7 +458,6 @@
                 setLongText(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        /* timeDependentText = */ null,
                         StringExpression(byteArrayOf(3, 4))
                     )
                 )
@@ -462,7 +474,6 @@
                 setContentDescription(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        /* timeDependentText = */ null,
                         StringExpression(byteArrayOf(1, 2))
                     )
                 )
@@ -471,7 +482,6 @@
                 setContentDescription(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        /* timeDependentText = */ null,
                         StringExpression(byteArrayOf(3, 4))
                     )
                 )
@@ -490,7 +500,6 @@
                         .setShortText(
                             ComplicationText(
                                 Random.nextInt().toString(), // Ignored when there's an expression.
-                                /* timeDependentText = */ null,
                                 StringExpression(byteArrayOf(1, 2))
                             )
                         )
@@ -503,7 +512,6 @@
                         .setShortText(
                             ComplicationText(
                                 Random.nextInt().toString(), // Ignored when there's an expression.
-                                /* timeDependentText = */ null,
                                 StringExpression(byteArrayOf(3, 4))
                             )
                         )
@@ -531,17 +539,13 @@
         ),
         ;
 
+        private val base = ComplicationData.TYPE_NO_DATA
+
         /** Builds a [ComplicationData] with the first variation. */
-        fun buildOne() =
-            ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
-                .apply { setterOne(this) }
-                .build()
+        fun buildOne() = ComplicationData.Builder(base).apply { setterOne(this) }.build()
 
         /** Builds a [ComplicationData] with the second variation. */
-        fun buildTwo() =
-            ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
-                .apply { setterTwo(this) }
-                .build()
+        fun buildTwo() = ComplicationData.Builder(base).apply { setterTwo(this) }.build()
     }
 
     @Test
@@ -557,4 +561,4 @@
                 .isFalse()
         }
     }
-}
\ No newline at end of file
+}
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
index 75dda2c..3b3e6b9 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
@@ -812,7 +812,6 @@
     public fun getTextAt_ignoresStringExpressionIfSurroundingStringPresent() {
         val text = ComplicationText(
             "hello" as CharSequence,
-            /* timeDependentText = */ null,
             StringExpression(byteArrayOf(1, 2, 3))
         )
 
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..a3019ad 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
@@ -16,6 +16,7 @@
 
 package androidx.wear.watchface.complications.data
 
+import android.support.wearable.complications.ComplicationData as WireComplicationData
 import android.util.Log
 import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.any
@@ -24,6 +25,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 +41,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 +75,7 @@
         evaluator.addListener(coroutineContext.asExecutor(), listener)
         advanceUntilIdle()
 
-        verify(listener, never()).invoke(any())
+        verify(listener, never()).accept(any())
     }
 
     @Test
@@ -84,7 +86,7 @@
         evaluator.addListener(coroutineContext.asExecutor(), listener)
         advanceUntilIdle()
 
-        verify(listener, times(1)).invoke(UNEVALUATED_DATA)
+        verify(listener, times(1)).accept(UNEVALUATED_DATA)
     }
 
     @Test
@@ -96,7 +98,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-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
index 7b51af9..5ca6603 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
@@ -18,6 +18,8 @@
 
 package androidx.wear.watchface.complications.data
 
+import android.support.wearable.complications.ComplicationData as WireComplicationData
+import android.support.wearable.complications.ComplicationText as WireComplicationText
 import android.annotation.SuppressLint
 import android.app.PendingIntent
 import android.content.ComponentName
@@ -66,7 +68,7 @@
         val data = NoDataComplicationData()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(null)
                     .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
                     .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
@@ -90,7 +92,7 @@
         val data = EmptyComplicationData()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_EMPTY).build()
+                WireComplicationData.Builder(WireComplicationData.TYPE_EMPTY).build()
             )
         testRoundTripConversions(data)
         assertThat(serializeAndDeserialize(data)).isInstanceOf(EmptyComplicationData::class.java)
@@ -105,7 +107,7 @@
         val data = NotConfiguredComplicationData()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NOT_CONFIGURED).build()
+                WireComplicationData.Builder(WireComplicationData.TYPE_NOT_CONFIGURED).build()
             )
         testRoundTripConversions(data)
         assertThat(serializeAndDeserialize(data))
@@ -129,7 +131,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+                WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
                     .setShortText(WireComplicationText.plainText("text"))
                     .setShortTitle(WireComplicationText.plainText("title"))
                     .setContentDescription(WireComplicationText.plainText("content description"))
@@ -174,7 +176,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+                WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
                     .setShortText(WireComplicationText.plainText("text"))
                     .setShortTitle(WireComplicationText.plainText("title"))
                     .setIcon(monochromaticImageIcon)
@@ -226,7 +228,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+                WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
                     .setLongText(WireComplicationText.plainText("text"))
                     .setLongTitle(WireComplicationText.plainText("title"))
                     .setContentDescription(WireComplicationText.plainText("content description"))
@@ -271,7 +273,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+                WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
                     .setLongText(WireComplicationText.plainText("text"))
                     .setLongTitle(WireComplicationText.plainText("title"))
                     .setIcon(monochromaticImageIcon)
@@ -324,7 +326,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+                WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
                     .setRangedValue(95f)
                     .setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
                     .setRangedMinValue(0f)
@@ -374,7 +376,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+                WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
                     .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
                     .setRangedValue(5f) // min as a sensible default
                     .setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
@@ -426,7 +428,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+                WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
                     .setRangedValue(95f)
                     .setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
                     .setRangedMinValue(0f)
@@ -478,7 +480,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+                WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
                     .setRangedValue(95f)
                     .setRangedMinValue(0f)
                     .setRangedMaxValue(100f)
@@ -536,7 +538,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+                WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
                     .setRangedValue(1200f)
                     .setTargetValue(10000f)
                     .setShortTitle(WireComplicationText.plainText("steps"))
@@ -582,7 +584,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+                WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
                     .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
                     .setRangedValue(0f) // sensible default
                     .setTargetValue(10000f)
@@ -630,7 +632,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+                WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
                     .setRangedValue(1200f)
                     .setTargetValue(10000f)
                     .setShortTitle(WireComplicationText.plainText("steps"))
@@ -680,7 +682,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+                WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
                     .setRangedValue(1200f)
                     .setTargetValue(10000f)
                     .setIcon(monochromaticImageIcon)
@@ -738,7 +740,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+                WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
                     .setRangedValue(95f)
                     .setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
                     .setRangedMinValue(0f)
@@ -807,7 +809,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
+                WireComplicationData.Builder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
                     .setElementWeights(floatArrayOf(0.5f, 1f, 2f))
                     .setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
                     .setElementBackgroundColor(Color.GRAY)
@@ -864,7 +866,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
+                WireComplicationData.Builder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
                     .setElementWeights(floatArrayOf(0.5f, 1f, 2f))
                     .setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
                     .setElementBackgroundColor(Color.TRANSPARENT)
@@ -922,7 +924,7 @@
         ).setDataSource(dataSource).build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_ICON)
+                WireComplicationData.Builder(WireComplicationData.TYPE_ICON)
                     .setIcon(icon)
                     .setContentDescription(WireComplicationText.plainText("content description"))
                     .setDataSource(dataSource)
@@ -957,7 +959,7 @@
         ).setDataSource(dataSource).build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+                WireComplicationData.Builder(WireComplicationData.TYPE_SMALL_IMAGE)
                     .setSmallImage(icon)
                     .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
                     .setContentDescription(WireComplicationText.plainText("content description"))
@@ -997,7 +999,7 @@
         ).setDataSource(dataSource).build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+                WireComplicationData.Builder(WireComplicationData.TYPE_SMALL_IMAGE)
                     .setSmallImage(bitmapIcon)
                     .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
                     .setContentDescription(WireComplicationText.plainText("content description"))
@@ -1028,7 +1030,7 @@
         ).setDataSource(dataSource).build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_LARGE_IMAGE)
+                WireComplicationData.Builder(WireComplicationData.TYPE_LARGE_IMAGE)
                     .setLargeImage(photoImage)
                     .setContentDescription(WireComplicationText.plainText("content description"))
                     .setDataSource(dataSource)
@@ -1062,7 +1064,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_PERMISSION)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_PERMISSION)
                     .setShortText(WireComplicationText.plainText("needs location"))
                     .setDataSource(dataSource)
                     .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
@@ -1095,7 +1097,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_PERMISSION)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_PERMISSION)
                     .setShortText(WireComplicationText.plainText("needs location"))
                     .setIcon(monochromaticImageIcon)
                     .setSmallImage(smallImageIcon)
@@ -1137,9 +1139,9 @@
         )
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+                        WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
                             .setShortText(ComplicationText.PLACEHOLDER.toWireComplicationText())
                             .setShortTitle(ComplicationText.PLACEHOLDER.toWireComplicationText())
                             .setIcon(createPlaceholderIcon())
@@ -1190,9 +1192,9 @@
         )
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+                        WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
                             .setLongText(WireComplicationText.plainText("text"))
                             .setContentDescription(
                                 WireComplicationText.plainText("content description")
@@ -1243,9 +1245,9 @@
         )
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+                        WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
                             .setRangedValue(RangedValueComplicationData.PLACEHOLDER)
                             .setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
                             .setRangedMinValue(0f)
@@ -1301,9 +1303,9 @@
         )
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+                        WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
                             .setRangedValue(GoalProgressComplicationData.PLACEHOLDER)
                             .setTargetValue(10000f)
                             .setShortText(ComplicationText.PLACEHOLDER.toWireComplicationText())
@@ -1364,9 +1366,9 @@
         )
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
+                        WireComplicationData.Builder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
                             .setElementWeights(floatArrayOf(0.5f, 1f, 2f))
                             .setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
                             .setElementBackgroundColor(Color.GRAY)
@@ -1424,9 +1426,9 @@
         )
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+                        WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
                             .setRangedValue(RangedValueComplicationData.PLACEHOLDER)
                             .setRangedValueType(RangedValueComplicationData.TYPE_RATING)
                             .setRangedMinValue(0f)
@@ -1480,9 +1482,9 @@
         )
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.TYPE_ICON)
+                        WireComplicationData.Builder(WireComplicationData.TYPE_ICON)
                             .setIcon(createPlaceholderIcon())
                             .setContentDescription(
                                 WireComplicationText.plainText("content description")
@@ -1527,9 +1529,9 @@
         )
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+                        WireComplicationData.Builder(WireComplicationData.TYPE_SMALL_IMAGE)
                             .setSmallImage(createPlaceholderIcon())
                             .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_ICON)
                             .setContentDescription(
@@ -1575,9 +1577,9 @@
         )
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.TYPE_LARGE_IMAGE)
+                        WireComplicationData.Builder(WireComplicationData.TYPE_LARGE_IMAGE)
                             .setLargeImage(createPlaceholderIcon())
                             .setContentDescription(
                                 WireComplicationText.plainText("content description")
@@ -1639,7 +1641,7 @@
     @Test
     public fun noDataComplicationData() {
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+            WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                 .setPlaceholder(null)
                 .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
                 .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
@@ -1651,7 +1653,7 @@
     @Test
     public fun emptyComplicationData() {
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_EMPTY).build(),
+            WireComplicationData.Builder(WireComplicationData.TYPE_EMPTY).build(),
             ComplicationType.EMPTY
         )
     }
@@ -1659,7 +1661,7 @@
     @Test
     public fun notConfiguredComplicationData() {
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_NOT_CONFIGURED).build(),
+            WireComplicationData.Builder(WireComplicationData.TYPE_NOT_CONFIGURED).build(),
             ComplicationType.NOT_CONFIGURED
         )
     }
@@ -1667,7 +1669,7 @@
     @Test
     public fun shortTextComplicationData() {
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+            WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
                 .setShortText(WireComplicationText.plainText("text"))
                 .setShortTitle(WireComplicationText.plainText("title"))
                 .setContentDescription(WireComplicationText.plainText("content description"))
@@ -1681,7 +1683,7 @@
     @Test
     public fun longTextComplicationData() {
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+            WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
                 .setLongText(WireComplicationText.plainText("text"))
                 .setLongTitle(WireComplicationText.plainText("title"))
                 .setContentDescription(WireComplicationText.plainText("content description"))
@@ -1695,7 +1697,7 @@
     @Test
     public fun rangedValueComplicationData_withFixedValue() {
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+            WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
                 .setRangedValue(95f)
                 .setRangedMinValue(0f)
                 .setRangedMaxValue(100f)
@@ -1711,7 +1713,7 @@
     @Test
     public fun rangedValueComplicationData_withValueExpression() {
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+            WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
                 .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
                 .setRangedMinValue(0f)
                 .setRangedMaxValue(100f)
@@ -1727,7 +1729,7 @@
     @Test
     public fun rangedValueComplicationData_drawSegmented() {
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+            WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
                 .setRangedValue(95f)
                 .setRangedMinValue(0f)
                 .setRangedMaxValue(100f)
@@ -1743,7 +1745,7 @@
     @Test
     public fun goalProgressComplicationData_withFixedValue() {
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+            WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
                 .setRangedValue(1200f)
                 .setTargetValue(10000f)
                 .setShortTitle(WireComplicationText.plainText("steps"))
@@ -1760,7 +1762,7 @@
     @Test
     public fun goalProgressComplicationData_withValueExpression() {
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+            WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
                 .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
                 .setTargetValue(10000f)
                 .setShortTitle(WireComplicationText.plainText("steps"))
@@ -1777,7 +1779,7 @@
     @Test
     public fun weightedElementsComplicationData() {
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
+            WireComplicationData.Builder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
                 .setElementWeights(floatArrayOf(0.5f, 1f, 2f))
                 .setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
                 .setElementBackgroundColor(Color.DKGRAY)
@@ -1794,7 +1796,7 @@
     public fun monochromaticImageComplicationData() {
         val icon = Icon.createWithContentUri("someuri")
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_ICON)
+            WireComplicationData.Builder(WireComplicationData.TYPE_ICON)
                 .setIcon(icon)
                 .setContentDescription(WireComplicationText.plainText("content description"))
                 .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
@@ -1808,7 +1810,7 @@
     public fun smallImageComplicationData() {
         val icon = Icon.createWithContentUri("someuri")
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+            WireComplicationData.Builder(WireComplicationData.TYPE_SMALL_IMAGE)
                 .setSmallImage(icon)
                 .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
                 .setContentDescription(WireComplicationText.plainText("content description"))
@@ -1823,7 +1825,7 @@
     public fun photoImageComplicationData() {
         val icon = Icon.createWithContentUri("someuri")
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_LARGE_IMAGE)
+            WireComplicationData.Builder(WireComplicationData.TYPE_LARGE_IMAGE)
                 .setLargeImage(icon)
                 .setContentDescription(WireComplicationText.plainText("content description"))
                 .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
@@ -1836,7 +1838,7 @@
     @Test
     public fun noPermissionComplicationData() {
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_NO_PERMISSION)
+            WireComplicationData.Builder(WireComplicationData.TYPE_NO_PERMISSION)
                 .setShortText(WireComplicationText.plainText("needs location"))
                 .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
                 .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
@@ -1849,9 +1851,9 @@
     public fun noDataComplicationData_shortText() {
         val icon = Icon.createWithContentUri("someuri")
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+            WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                 .setPlaceholder(
-                    WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+                    WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
                         .setContentDescription(
                             WireComplicationText.plainText("content description")
                         )
@@ -1873,9 +1875,9 @@
     public fun noDataComplicationData_longText() {
         val icon = Icon.createWithContentUri("someuri")
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+            WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                 .setPlaceholder(
-                    WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+                    WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
                         .setContentDescription(
                             WireComplicationText.plainText("content description")
                         )
@@ -1897,9 +1899,9 @@
     public fun noDataComplicationData_rangedValue() {
         val icon = Icon.createWithContentUri("someuri")
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+            WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                 .setPlaceholder(
-                    WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+                    WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
                         .setContentDescription(
                             WireComplicationText.plainText("content description")
                         )
@@ -1924,9 +1926,9 @@
     public fun noDataComplicationData_goalProgress() {
         val icon = Icon.createWithContentUri("someuri")
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+            WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                 .setPlaceholder(
-                    WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+                    WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
                         .setRangedValue(1200f)
                         .setTargetValue(10000f)
                         .setShortTitle(WireComplicationText.plainText("steps"))
@@ -1950,9 +1952,9 @@
     @Test
     public fun noDataComplicationData_weightedElementsComplicationData() {
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+            WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                 .setPlaceholder(
-                    WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
+                    WireComplicationData.Builder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
                         .setElementWeights(floatArrayOf(0.5f, 1f, 2f))
                         .setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
                         .setElementBackgroundColor(Color.DKGRAY)
@@ -1975,9 +1977,9 @@
     public fun noDataComplicationData_smallImage() {
         val icon = Icon.createWithContentUri("someuri")
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+            WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                 .setPlaceholder(
-                    WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+                    WireComplicationData.Builder(WireComplicationData.TYPE_SMALL_IMAGE)
                         .setSmallImage(icon)
                         .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
                         .setContentDescription(
@@ -1998,9 +2000,9 @@
     public fun noDataComplicationData_monochromaticImage() {
         val icon = Icon.createWithContentUri("someuri")
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+            WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                 .setPlaceholder(
-                    WireComplicationDataBuilder(WireComplicationData.TYPE_ICON)
+                    WireComplicationData.Builder(WireComplicationData.TYPE_ICON)
                         .setIcon(icon)
                         .setContentDescription(
                             WireComplicationText.plainText("content description")
@@ -2285,7 +2287,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+                WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
                     .setShortText(WireComplicationText.plainText("text"))
                     .setStartDateTimeMillis(testStartInstant.toEpochMilli())
                     .setEndDateTimeMillis(testEndDateInstant.toEpochMilli())
@@ -2302,7 +2304,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+                WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
                     .setLongText(WireComplicationText.plainText("text"))
                     .setStartDateTimeMillis(testStartInstant.toEpochMilli())
                     .setEndDateTimeMillis(testEndDateInstant.toEpochMilli())
@@ -2323,7 +2325,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+                WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
                     .setRangedValue(95f)
                     .setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
                     .setRangedMinValue(0f)
@@ -2349,7 +2351,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+                WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
                     .setRangedValue(1200f)
                     .setTargetValue(10000f)
                     .setShortTitle(WireComplicationText.plainText("steps"))
@@ -2379,7 +2381,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
+                WireComplicationData.Builder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
                     .setElementWeights(floatArrayOf(0.5f, 1f, 2f))
                     .setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
                     .setElementBackgroundColor(Color.TRANSPARENT)
@@ -2402,7 +2404,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_ICON)
+                WireComplicationData.Builder(WireComplicationData.TYPE_ICON)
                     .setIcon(icon)
                     .setStartDateTimeMillis(testStartInstant.toEpochMilli())
                     .setEndDateTimeMillis(testEndDateInstant.toEpochMilli())
@@ -2421,7 +2423,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+                WireComplicationData.Builder(WireComplicationData.TYPE_SMALL_IMAGE)
                     .setSmallImage(icon)
                     .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
                     .setStartDateTimeMillis(testStartInstant.toEpochMilli())
@@ -2440,7 +2442,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_LARGE_IMAGE)
+                WireComplicationData.Builder(WireComplicationData.TYPE_LARGE_IMAGE)
                     .setLargeImage(photoImage)
                     .setStartDateTimeMillis(testStartInstant.toEpochMilli())
                     .setEndDateTimeMillis(testEndDateInstant.toEpochMilli())
@@ -2458,9 +2460,9 @@
         )
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+                        WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
                             .setShortText(WireComplicationText.plainText("text"))
                             .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
                             .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
@@ -2480,9 +2482,9 @@
         )
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+                        WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
                             .setLongText(WireComplicationText.plainText("text"))
                             .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
                             .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
@@ -2508,9 +2510,9 @@
         )
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+                        WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
                             .setRangedValue(95f)
                             .setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
                             .setRangedMinValue(0f)
@@ -2539,9 +2541,9 @@
         )
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+                        WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
                             .setRangedValue(1200f)
                             .setTargetValue(10000f)
                             .setShortTitle(WireComplicationText.plainText("steps"))
@@ -2576,9 +2578,9 @@
         )
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
+                        WireComplicationData.Builder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
                             .setElementWeights(floatArrayOf(0.5f, 1f, 2f))
                             .setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
                             .setElementBackgroundColor(Color.TRANSPARENT)
@@ -2605,9 +2607,9 @@
         )
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.TYPE_ICON)
+                        WireComplicationData.Builder(WireComplicationData.TYPE_ICON)
                             .setIcon(icon)
                             .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
                             .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
@@ -2628,9 +2630,9 @@
         )
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+                        WireComplicationData.Builder(WireComplicationData.TYPE_SMALL_IMAGE)
                             .setSmallImage(icon)
                             .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
                             .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
@@ -2651,9 +2653,9 @@
         )
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.TYPE_LARGE_IMAGE)
+                        WireComplicationData.Builder(WireComplicationData.TYPE_LARGE_IMAGE)
                             .setLargeImage(icon)
                             .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
                             .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt
index 513af6b..a77abb9 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt
@@ -425,7 +425,7 @@
         timelineEntry.timelineStartEpochSecond = 100
         timelineEntry.timelineEndEpochSecond = 1000
 
-        val wireLongTextComplication = WireComplicationDataBuilder(
+        val wireLongTextComplication = ComplicationData.Builder(
             ComplicationType.LONG_TEXT.toWireComplicationType()
         )
             .setEndDateTimeMillis(1650988800000)
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt
index e16b877..492d983 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt
@@ -16,6 +16,9 @@
 
 package androidx.wear.watchface.complications.data
 
+import android.support.wearable.complications.ComplicationText as WireComplicationText
+import android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder as WireTimeDifferenceBuilder
+import android.support.wearable.complications.ComplicationText.TimeFormatBuilder as WireTimeFormatBuilder
 import android.content.Context
 import android.icu.util.TimeZone
 import android.support.wearable.complications.ComplicationText
@@ -28,12 +31,6 @@
 import java.time.Instant
 import java.util.concurrent.TimeUnit
 
-private typealias WireTimeDifferenceBuilder =
-    android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder
-
-private typealias WireTimeFormatBuilder =
-    android.support.wearable.complications.ComplicationText.TimeFormatBuilder
-
 @RunWith(SharedRobolectricTestRunner::class)
 public class AsWireComplicationTextTest {
     @Test
@@ -239,7 +236,7 @@
             null
         )
         val text = TimeDifferenceComplicationText(
-            ComplicationText("test", tft, /* stringExpression = */ null)
+            ComplicationText("test", tft)
         )
 
         assertNull(text.getMinimumTimeUnit())
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TypeTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TypeTest.kt
index 4c4b0a9..1e2a28c 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TypeTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TypeTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.wear.watchface.complications.data
 
+import android.support.wearable.complications.ComplicationData as WireComplicationData
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/wear/watchface/watchface-complications/src/androidTest/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetrieverTest.kt b/wear/watchface/watchface-complications/src/androidTest/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetrieverTest.kt
index da8530c..230310b 100644
--- a/wear/watchface/watchface-complications/src/androidTest/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetrieverTest.kt
+++ b/wear/watchface/watchface-complications/src/androidTest/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetrieverTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.wear.watchface.complications
 
+import android.support.wearable.complications.ComplicationProviderInfo as WireComplicationProviderInfo
 import android.app.Service
 import android.content.ComponentName
 import android.content.Context
@@ -41,9 +42,6 @@
 import org.junit.runner.RunWith
 import java.time.Instant
 
-private typealias WireComplicationProviderInfo =
-    android.support.wearable.complications.ComplicationProviderInfo
-
 private const val LEFT_COMPLICATION_ID = 101
 private const val RIGHT_COMPLICATION_ID = 102
 
diff --git a/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetriever.kt b/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetriever.kt
index 99d37bb..dbc4246 100644
--- a/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetriever.kt
+++ b/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetriever.kt
@@ -15,6 +15,7 @@
  */
 package androidx.wear.watchface.complications
 
+import android.support.wearable.complications.ComplicationProviderInfo as WireComplicationProviderInfo
 import android.annotation.SuppressLint
 import android.content.ComponentName
 import android.content.Context
@@ -53,9 +54,6 @@
 import kotlin.coroutines.resumeWithException
 import kotlinx.coroutines.CancellableContinuation
 
-private typealias WireComplicationProviderInfo =
-    android.support.wearable.complications.ComplicationProviderInfo
-
 /**
  * Retrieves [Result] for a watch face's complications.
  *
@@ -440,14 +438,11 @@
         )
 }
 
-// Ugh we need this since the linter wants the method signature all on one line...
-typealias ApiInfo = ComplicationDataSourceInfo
-
 /**
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public fun WireComplicationProviderInfo.toApiComplicationDataSourceInfo(): ApiInfo =
+public fun WireComplicationProviderInfo.toApiComplicationDataSourceInfo() =
     ComplicationDataSourceInfo(
         appName!!, providerName!!, providerIcon!!, fromWireType(complicationType),
         providerComponentName
diff --git a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java
index af75b55..d3e8b68 100644
--- a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java
+++ b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java
@@ -17,7 +17,6 @@
 package androidx.wear.watchface.control.data;
 
 import android.annotation.SuppressLint;
-import android.content.ComponentName;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -92,16 +91,15 @@
             @NonNull WatchUiState watchUiState,
             @NonNull UserStyleWireFormat userStyle,
             @Nullable List<IdAndComplicationDataWireFormat> idAndComplicationDataWireFormats,
-            @Nullable ComponentName auxiliaryComponentName) {
+            @Nullable String auxiliaryComponentPackageName,
+            @Nullable String auxiliaryComponentClassName) {
         mInstanceId = instanceId;
         mDeviceConfig = deviceConfig;
         mWatchUiState = watchUiState;
         mUserStyle = userStyle;
         mIdAndComplicationDataWireFormats = idAndComplicationDataWireFormats;
-        if (auxiliaryComponentName != null) {
-            mAuxiliaryComponentClassName = auxiliaryComponentName.getClassName();
-            mAuxiliaryComponentPackageName = auxiliaryComponentName.getPackageName();
-        }
+        mAuxiliaryComponentClassName = auxiliaryComponentClassName;
+        mAuxiliaryComponentPackageName = auxiliaryComponentPackageName;
     }
 
     @NonNull
diff --git a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
index f8818fe..fb6ed5d 100644
--- a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
+++ b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.wear.watchface.editor
 
+import android.support.wearable.complications.ComplicationProviderInfo as WireComplicationProviderInfo
 import android.annotation.SuppressLint
 import android.app.Activity
 import android.content.ComponentName
@@ -145,9 +146,6 @@
 private const val PROVIDER_CHOOSER_RESULT_EXTRA_KEY = "PROVIDER_CHOOSER_RESULT_EXTRA_KEY"
 private const val PROVIDER_CHOOSER_RESULT_EXTRA_VALUE = "PROVIDER_CHOOSER_RESULT_EXTRA_VALUE"
 
-private typealias WireComplicationProviderInfo =
-    android.support.wearable.complications.ComplicationProviderInfo
-
 internal val redStyleOption = ListOption(Option.Id("red_style"), "Red", "Red", icon = null)
 internal val greenStyleOption =
     ListOption(Option.Id("green_style"), "Green", "Green", icon = null)
diff --git a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt
index 4ece658..88edba6 100644
--- a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt
+++ b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt
@@ -16,6 +16,7 @@
 
 package androidx.wear.watchface.editor
 
+import androidx.wear.watchface.data.DeviceConfig as WireDeviceConfig
 import android.annotation.SuppressLint
 import android.app.Activity
 import android.content.ComponentName
@@ -47,8 +48,6 @@
 internal const val USER_STYLE_KEY: String = "USER_STYLE_KEY"
 internal const val USER_STYLE_VALUES: String = "USER_STYLE_VALUES"
 
-typealias WireDeviceConfig = androidx.wear.watchface.data.DeviceConfig
-
 /**
  * Parameters for an optional final screenshot taken by [EditorSession] upon exit and reported via
  * [EditorState].
diff --git a/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/WatchFaceRenderer.java b/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/WatchFaceRenderer.java
index 6bc30aa..b0a2ccf 100644
--- a/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/WatchFaceRenderer.java
+++ b/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/WatchFaceRenderer.java
@@ -102,7 +102,10 @@
         }
 
         mPaint.setColor(Color.WHITE);
-        int hour = zonedDateTime.getHour() % 12;
+        int hour = zonedDateTime.getHour();
+        if (hour != 12) {
+            hour %= 12;
+        }
         int minute = zonedDateTime.getMinute();
         int second = zonedDateTime.getSecond();
         mTime[0] = DIGITS[hour / 10];
diff --git a/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/WatchFaceRenderer.java b/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/WatchFaceRenderer.java
index 8e13885..42488a8 100644
--- a/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/WatchFaceRenderer.java
+++ b/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/WatchFaceRenderer.java
@@ -190,7 +190,10 @@
             mPaint.setColor(Color.BLACK);
             canvas.drawRect(rect, mPaint);
             mPaint.setColor(Color.WHITE);
-            int hour = zonedDateTime.getHour() % 12;
+            int hour = zonedDateTime.getHour();
+            if (hour != 12) {
+                hour %= 12;
+            }
             int minute = zonedDateTime.getMinute();
             int second = zonedDateTime.getSecond();
             mTimeText[0] = DIGITS[hour / 10];
@@ -263,7 +266,10 @@
                 @NotNull Canvas canvas, @NotNull Rect rect, @NotNull ZonedDateTime zonedDateTime,
                 RenderParameters renderParameters) {
             boolean isActive = renderParameters.getDrawMode() != DrawMode.AMBIENT;
-            int hour = zonedDateTime.getHour() % 12;
+            int hour = zonedDateTime.getHour();
+            if (hour != 12) {
+                hour %= 12;
+            }
             int minute = zonedDateTime.getMinute();
             int second = zonedDateTime.getSecond();
 
diff --git a/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/WatchFaceRenderer.java b/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/WatchFaceRenderer.java
index 5fe6e4c..e15cdbf 100644
--- a/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/WatchFaceRenderer.java
+++ b/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/WatchFaceRenderer.java
@@ -142,7 +142,10 @@
             mPaint.setColor(Color.BLACK);
             canvas.drawRect(rect, mPaint);
             mPaint.setColor(Color.WHITE);
-            int hour = zonedDateTime.getHour() % 12;
+            int hour = zonedDateTime.getHour();
+            if (hour != 12) {
+                hour %= 12;
+            }
             int minute = zonedDateTime.getMinute();
             int second = zonedDateTime.getSecond();
             mTimeText[0] = DIGITS[hour / 10];
@@ -211,7 +214,10 @@
                 @NotNull Canvas canvas, @NotNull Rect rect, @NotNull ZonedDateTime zonedDateTime,
                 RenderParameters renderParameters) {
             boolean isActive = renderParameters.getDrawMode() != DrawMode.AMBIENT;
-            int hour = zonedDateTime.getHour() % 12;
+            int hour = zonedDateTime.getHour();
+            if (hour != 12) {
+                hour %= 12;
+            }
             int minute = zonedDateTime.getMinute();
             int second = zonedDateTime.getSecond();
 
diff --git a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/OldClientAidlCompatTest.kt b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/OldClientAidlCompatTest.kt
index 38680ef..5cf9133 100644
--- a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/OldClientAidlCompatTest.kt
+++ b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/OldClientAidlCompatTest.kt
@@ -39,6 +39,7 @@
 import kotlinx.coroutines.runBlocking
 import org.junit.After
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 
@@ -191,6 +192,7 @@
     }
 
     @Test
+    @Ignore // TODO(b/265425077): This test is failing on the bots, fix it.
     fun roundTripUserStyleSchema() = runBlocking {
         val service = IStyleEchoService.Stub.asInterface(
             bindService(Intent(ACTON).apply { setPackage(PACKAGE_NAME) })
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..cf9a673 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
@@ -498,12 +497,47 @@
 
             return UserStyleSchema(userStyleSettings)
         }
+
+        internal fun UserStyleSchemaWireFormat.toApiFormat(): List<UserStyleSetting> {
+            val userStyleSettings = mSchema.map {
+                UserStyleSetting.createFromWireFormat(it)
+            }
+            val wireUserStyleSettingsIterator = mSchema.iterator()
+            for (setting in userStyleSettings) {
+                val wireUserStyleSetting = wireUserStyleSettingsIterator.next()
+                wireUserStyleSetting.mOptionChildIndices?.let {
+                    // Unfortunately due to VersionedParcelable limitations, we can not extend the
+                    // Options wire format (extending the contents of a list is not supported!!!).
+                    // This means we need to encode/decode the childSettings in a round about way.
+                    val optionsIterator = setting.options.iterator()
+                    var option: Option? = null
+                    for (childIndex in it) {
+                        if (option == null) {
+                            option = optionsIterator.next()
+                        }
+                        if (childIndex == -1) {
+                            option = null
+                        } else {
+                            val childSettings = option.childSettings as ArrayList
+                            val child = userStyleSettings[childIndex]
+                            childSettings.add(child)
+                            child.hasParent = true
+                        }
+                    }
+                }
+            }
+            return userStyleSettings
+        }
     }
 
     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++
@@ -559,7 +593,9 @@
         }
         for (setting in settings) {
             for (option in setting.options) {
-                validateComplicationSettings(option.childSettings, prevSetting)
+                if (option.childSettings.isNotEmpty()) {
+                    validateComplicationSettings(option.childSettings, prevSetting)
+                }
             }
         }
     }
@@ -567,34 +603,7 @@
     /** @hide */
     @Suppress("Deprecation") // userStyleSettings
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public constructor(wireFormat: UserStyleSchemaWireFormat) : this(
-        wireFormat.mSchema.map { UserStyleSetting.createFromWireFormat(it) }
-    ) {
-        val wireUserStyleSettingsIterator = wireFormat.mSchema.iterator()
-        for (userStyle in userStyleSettings) {
-            val wireUserStyleSetting = wireUserStyleSettingsIterator.next()
-            wireUserStyleSetting.mOptionChildIndices?.let {
-                // Unfortunately due to VersionedParcelable limitations, we can not extend the
-                // Options wire format (extending the contents of a list is not supported!!!).
-                // This means we need to encode/decode the childSettings in a round about way.
-                val optionsIterator = userStyle.options.iterator()
-                var option: Option? = null
-                for (childIndex in it) {
-                    if (option == null) {
-                        option = optionsIterator.next()
-                    }
-                    if (childIndex == -1) {
-                        option = null
-                    } else {
-                        val childSettings = option.childSettings as ArrayList
-                        val child = userStyleSettings[childIndex]
-                        childSettings.add(child)
-                        child.hasParent = true
-                    }
-                }
-            }
-        }
-    }
+    public constructor(wireFormat: UserStyleSchemaWireFormat) : this(wireFormat.toApiFormat())
 
     /** @hide */
     @Suppress("Deprecation") // userStyleSettings
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-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
index 4157013..fb68608 100644
--- a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
+++ b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
@@ -349,6 +349,9 @@
                 styleSetting2
             )
         )
+        assertThat(srcSchema.rootUserStyleSettings.map { it.id }).containsExactly(
+            UserStyleSetting.Id("clock_type")
+        )
 
         val parcel = Parcel.obtain()
         srcSchema.toWireFormat().writeToParcel(parcel, 0)
@@ -360,6 +363,9 @@
         parcel.recycle()
 
         assertThat(schema.userStyleSettings.size).isEqualTo(4)
+        assertThat(schema.rootUserStyleSettings.map { it.id }).containsExactly(
+            UserStyleSetting.Id("clock_type")
+        )
 
         val deserializedWatchFaceType = schema.userStyleSettings[0] as ListUserStyleSetting
         assertThat(deserializedWatchFaceType.id)
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/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.java b/wear/watchface/watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.java
index c9db0f7..3c8e640 100644
--- a/wear/watchface/watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.java
+++ b/wear/watchface/watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.java
@@ -66,7 +66,10 @@
         mPaint.setColor(Color.BLACK);
         canvas.drawRect(rect, mPaint);
         mPaint.setColor(Color.WHITE);
-        int hour = zonedDateTime.getHour() % 12;
+        int hour = zonedDateTime.getHour();
+        if (hour != 12) {
+            hour %= 12;
+        }
         int minute = zonedDateTime.getMinute();
         int second = zonedDateTime.getSecond();
         mTimeText[0] = DIGITS[hour / 10];
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/ComplicationHelperActivityTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/ComplicationHelperActivityTest.kt
index b2f2811..db2ad24 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/ComplicationHelperActivityTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/ComplicationHelperActivityTest.kt
@@ -22,6 +22,8 @@
 import android.os.Handler
 import android.os.Looper
 import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
 import androidx.wear.watchface.complications.data.ComplicationType
 import androidx.wear.watchface.complications.data.ComplicationType.LONG_TEXT
 import androidx.wear.watchface.complications.data.ComplicationType.MONOCHROMATIC_IMAGE
@@ -34,9 +36,12 @@
 import com.nhaarman.mockitokotlin2.verify
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
+import org.junit.runner.RunWith
 
 const val TIME_OUT_MILLIS = 500L
 
+@RunWith(AndroidJUnit4::class)
+@MediumTest
 public class ComplicationHelperActivityTest {
     private val mainThreadHandler = Handler(Looper.getMainLooper())
 
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/WatchFaceServiceAndroidTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/WatchFaceServiceAndroidTest.kt
index 7f3f0ad..3f6fe61 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/WatchFaceServiceAndroidTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/WatchFaceServiceAndroidTest.kt
@@ -19,12 +19,17 @@
 import android.content.Context
 import android.graphics.drawable.Icon
 import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
 import androidx.wear.watchface.style.UserStyleSchema
 import androidx.wear.watchface.style.UserStyleSetting
 import androidx.wear.watchface.style.WatchFaceLayer
 import androidx.wear.watchface.test.SimpleWatchFaceTestService
 import org.junit.Test
+import org.junit.runner.RunWith
 
+@RunWith(AndroidJUnit4::class)
+@MediumTest
 class WatchFaceServiceAndroidTest {
     @Test
     fun measuresWatchFaceIconsFromCustomContext() {
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/SimpleWatchFaceTestService.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/SimpleWatchFaceTestService.kt
index 77c3d9a..0738ffe 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/SimpleWatchFaceTestService.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/SimpleWatchFaceTestService.kt
@@ -16,6 +16,7 @@
 
 package androidx.wear.watchface.test
 
+import android.os.Build
 import android.view.SurfaceHolder
 import androidx.test.core.app.ApplicationProvider
 import androidx.wear.watchface.ComplicationSlotsManager
@@ -42,5 +43,5 @@
     ) = throw NotImplementedError("Should not reach this step")
 
     // Set this to `true` so that the whole setup is skipped for this test
-    override fun isPreAndroidR() = true
+    override val wearSdkVersion = Build.VERSION_CODES.O_MR1
 }
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/TestCanvasAnalogWatchFaceService.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/TestCanvasAnalogWatchFaceService.kt
index 12966a3..217a28a 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/TestCanvasAnalogWatchFaceService.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/TestCanvasAnalogWatchFaceService.kt
@@ -17,6 +17,7 @@
 package androidx.wear.watchface.test
 
 import android.content.Context
+import android.os.Build
 import android.os.Handler
 import android.view.SurfaceHolder
 import androidx.wear.watchface.ComplicationSlotsManager
@@ -87,7 +88,10 @@
 
     override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
 
-    override fun isPreAndroidR() = preRInitFlow
+    override val wearSdkVersion = when (preRInitFlow) {
+        true -> Build.VERSION_CODES.O_MR1
+        false -> Build.VERSION_CODES.R
+    }
 
     override fun readDirectBootPrefs(
         context: Context,
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
index 3160787..93526dc 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
@@ -42,16 +42,12 @@
 import androidx.test.filters.MediumTest
 import androidx.test.screenshot.AndroidXScreenshotTestRule
 import androidx.test.screenshot.assertAgainstGolden
-import androidx.wear.watchface.ComplicationSlotsManager
 import androidx.wear.watchface.DrawMode
-import androidx.wear.watchface.MutableWatchState
 import androidx.wear.watchface.RenderParameters
 import androidx.wear.watchface.SYSTEM_SUPPORTS_CONSISTENT_IDS_PREFIX
 import androidx.wear.watchface.TapEvent
 import androidx.wear.watchface.TapType
-import androidx.wear.watchface.WatchFace
 import androidx.wear.watchface.WatchFaceService
-import androidx.wear.watchface.WatchState
 import androidx.wear.watchface.complications.SystemDataSources
 import androidx.wear.watchface.complications.data.ComplicationText
 import androidx.wear.watchface.complications.data.LongTextComplicationData
@@ -75,8 +71,6 @@
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.GREEN_STYLE
-import androidx.wear.watchface.style.CurrentUserStyleRepository
-import androidx.wear.watchface.style.UserStyleSchema
 import androidx.wear.watchface.style.WatchFaceLayer
 import androidx.wear.watchface.style.data.UserStyleWireFormat
 import com.google.common.truth.Truth.assertThat
@@ -85,7 +79,6 @@
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.android.asCoroutineDispatcher
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
@@ -136,74 +129,6 @@
     }
 }
 
-internal class TestControllableWatchFaceService(
-    private val handler: Handler,
-    private var surfaceHolderOverride: SurfaceHolder,
-    private val factory: TestWatchFaceFactory,
-    private val watchState: MutableWatchState,
-    private val directBootParams: WallpaperInteractiveWatchFaceInstanceParams?
-) : WatchFaceService() {
-    init {
-        attachBaseContext(ApplicationProvider.getApplicationContext())
-    }
-
-    abstract class TestWatchFaceFactory {
-        fun createUserStyleSchema(): UserStyleSchema = UserStyleSchema(emptyList())
-
-        fun createComplicationsManager(
-            currentUserStyleRepository: CurrentUserStyleRepository
-        ): ComplicationSlotsManager =
-            ComplicationSlotsManager(emptyList(), currentUserStyleRepository)
-
-        abstract fun createWatchFaceAsync(
-            surfaceHolder: SurfaceHolder,
-            watchState: WatchState,
-            complicationSlotsManager: ComplicationSlotsManager,
-            currentUserStyleRepository: CurrentUserStyleRepository
-        ): Deferred<WatchFace>
-    }
-
-    override fun createUserStyleSchema() = factory.createUserStyleSchema()
-
-    override fun createComplicationSlotsManager(
-        currentUserStyleRepository: CurrentUserStyleRepository
-    ) = factory.createComplicationsManager(currentUserStyleRepository)
-
-    override suspend fun createWatchFace(
-        surfaceHolder: SurfaceHolder,
-        watchState: WatchState,
-        complicationSlotsManager: ComplicationSlotsManager,
-        currentUserStyleRepository: CurrentUserStyleRepository
-    ) = factory.createWatchFaceAsync(
-        surfaceHolderOverride,
-        watchState,
-        complicationSlotsManager,
-        currentUserStyleRepository
-    ).await()
-
-    override fun getUiThreadHandlerImpl() = handler
-
-    override fun getBackgroundThreadHandlerImpl() = handler
-
-    override fun getMutableWatchState() = watchState
-
-    override fun readDirectBootPrefs(
-        context: Context,
-        fileName: String
-    ) = directBootParams
-
-    override fun writeDirectBootPrefs(
-        context: Context,
-        fileName: String,
-        prefs: WallpaperInteractiveWatchFaceInstanceParams
-    ) {
-    }
-
-    override fun isPreAndroidR() = false
-
-    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
-}
-
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 @RequiresApi(Build.VERSION_CODES.O_MR1)
@@ -358,6 +283,7 @@
                         WatchUiState(false, 0),
                         UserStyleWireFormat(emptyMap()),
                         null,
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
@@ -930,6 +856,7 @@
                     mapOf(COLOR_STYLE_SETTING to GREEN_STYLE.encodeToByteArray())
                 ),
                 null,
+                null,
                 null
             ),
             null
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/XmlDefinedUserStyleSchemaAndComplicationSlotsTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/XmlDefinedUserStyleSchemaAndComplicationSlotsTest.kt
index 24d49ce..1da812c 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/XmlDefinedUserStyleSchemaAndComplicationSlotsTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/XmlDefinedUserStyleSchemaAndComplicationSlotsTest.kt
@@ -198,6 +198,7 @@
                         WatchUiState(false, 0),
                         UserStyleWireFormat(emptyMap()),
                         null,
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
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 75a2093..7d5ff70 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
@@ -16,6 +16,7 @@
 
 package androidx.wear.watchface
 
+import android.support.wearable.complications.ComplicationData as WireComplicationData
 import android.graphics.Canvas
 import android.graphics.Rect
 import android.graphics.RectF
@@ -28,32 +29,32 @@
 import androidx.annotation.RestrictTo
 import androidx.annotation.UiThread
 import androidx.annotation.WorkerThread
+import androidx.wear.watchface.RenderParameters.HighlightedElement
 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.ComplicationDisplayPolicies
+import androidx.wear.watchface.complications.data.ComplicationExperimental
 import androidx.wear.watchface.complications.data.ComplicationType
 import androidx.wear.watchface.complications.data.EmptyComplicationData
 import androidx.wear.watchface.complications.data.NoDataComplicationData
-import androidx.wear.watchface.RenderParameters.HighlightedElement
-import androidx.wear.watchface.complications.data.ComplicationDisplayPolicies
-import androidx.wear.watchface.complications.data.ComplicationExperimental
 import androidx.wear.watchface.complications.data.toApiComplicationData
 import androidx.wear.watchface.data.BoundingArcWireFormat
 import androidx.wear.watchface.style.UserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay
 import java.lang.Integer.min
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlin.math.abs
-import kotlin.math.atan2
-import kotlin.math.cos
-import kotlin.math.sin
 import java.time.Instant
 import java.time.LocalDateTime
 import java.time.ZoneId
 import java.time.ZonedDateTime
 import java.util.Objects
+import kotlin.math.abs
+import kotlin.math.atan2
+import kotlin.math.cos
+import kotlin.math.sin
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 
 /**
  * Interface for rendering complicationSlots onto a [Canvas]. These should be created by
@@ -884,8 +885,10 @@
     /**
      * The default [ComplicationType] to use alongside [defaultDataSourcePolicy].
      */
-    @Deprecated("Use DefaultComplicationDataSourcePolicy." +
-        "systemDataSourceFallbackDefaultType instead")
+    @Deprecated(
+        "Use DefaultComplicationDataSourcePolicy." +
+            "systemDataSourceFallbackDefaultType instead"
+    )
     public var defaultDataSourceType: ComplicationType = defaultDataSourceType
         @UiThread
         get
@@ -991,9 +994,7 @@
         lastComplicationUpdate = instant
         complicationHistory?.push(ComplicationDataHistoryEntry(complicationData, instant))
         timelineComplicationData = complicationData
-        timelineEntries = complicationData.asWireComplicationData().timelineEntries?.map {
-            it
-        }
+        timelineEntries = complicationData.asWireComplicationData().timelineEntries?.toList()
         selectComplicationDataForInstant(instant, loadDrawablesAsynchronous, true)
     }
 
@@ -1034,10 +1035,9 @@
             best = screenLockedFallback // This is NoDataComplicationData.
         }
 
-        if (forceUpdate || complicationData.value != best) {
-            renderer.loadData(best, loadDrawablesAsynchronous)
-            (complicationData as MutableStateFlow).value = best
-        }
+        if (!forceUpdate && complicationData.value == best) return
+        renderer.loadData(best, loadDrawablesAsynchronous)
+        (complicationData as MutableStateFlow).value = best
     }
 
     /**
@@ -1202,20 +1202,26 @@
         writer.println(
             "defaultDataSourcePolicy.primaryDataSource=${defaultDataSourcePolicy.primaryDataSource}"
         )
-        writer.println("defaultDataSourcePolicy.primaryDataSourceDefaultDataSourceType=" +
-            defaultDataSourcePolicy.primaryDataSourceDefaultType)
+        writer.println(
+            "defaultDataSourcePolicy.primaryDataSourceDefaultDataSourceType=" +
+                defaultDataSourcePolicy.primaryDataSourceDefaultType
+        )
         writer.println(
             "defaultDataSourcePolicy.secondaryDataSource=" +
                 defaultDataSourcePolicy.secondaryDataSource
         )
-        writer.println("defaultDataSourcePolicy.secondaryDataSourceDefaultDataSourceType=" +
-            defaultDataSourcePolicy.secondaryDataSourceDefaultType)
+        writer.println(
+            "defaultDataSourcePolicy.secondaryDataSourceDefaultDataSourceType=" +
+                defaultDataSourcePolicy.secondaryDataSourceDefaultType
+        )
         writer.println(
             "defaultDataSourcePolicy.systemDataSourceFallback=" +
                 defaultDataSourcePolicy.systemDataSourceFallback
         )
-        writer.println("defaultDataSourcePolicy.systemDataSourceFallbackDefaultType=" +
-            defaultDataSourcePolicy.systemDataSourceFallbackDefaultType)
+        writer.println(
+            "defaultDataSourcePolicy.systemDataSourceFallbackDefaultType=" +
+                defaultDataSourcePolicy.systemDataSourceFallbackDefaultType
+        )
         writer.println("timelineComplicationData=$timelineComplicationData")
         writer.println("timelineEntries=" + timelineEntries?.joinToString())
         writer.println("data=${renderer.getData()}")
@@ -1245,8 +1251,10 @@
         if (accessibilityTraversalIndex != other.accessibilityTraversalIndex) return false
         if (boundsType != other.boundsType) return false
         if (complicationSlotBounds != other.complicationSlotBounds) return false
-        if (supportedTypes.size != other.supportedTypes.size ||
-            !supportedTypes.containsAll(other.supportedTypes)) return false
+        if (
+            supportedTypes.size != other.supportedTypes.size ||
+            !supportedTypes.containsAll(other.supportedTypes)
+        ) return false
         if (defaultDataSourcePolicy != other.defaultDataSourcePolicy) return false
         if (initiallyEnabled != other.initiallyEnabled) return false
         if (fixedComplicationDataSource != other.fixedComplicationDataSource) return false
@@ -1260,9 +1268,11 @@
 
     override fun hashCode(): Int {
         @OptIn(ComplicationExperimental::class)
-        return Objects.hash(id, accessibilityTraversalIndex, boundsType, complicationSlotBounds,
+        return Objects.hash(
+            id, accessibilityTraversalIndex, boundsType, complicationSlotBounds,
             supportedTypes.sorted(),
             defaultDataSourcePolicy, initiallyEnabled, fixedComplicationDataSource,
-            nameResourceId, screenReaderNameResourceId, boundingArc)
+            nameResourceId, screenReaderNameResourceId, boundingArc
+        )
     }
 }
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
index 9553f47..a693dc2 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
@@ -199,11 +199,9 @@
     /** Finish initialization. */
     @WorkerThread
     internal fun init(
-        watchFaceHostApi: WatchFaceHostApi,
         renderer: Renderer,
         complicationSlotInvalidateListener: ComplicationSlot.InvalidateListener
     ) = TraceEvent("ComplicationSlotsManager.init").use {
-        this.watchFaceHostApi = watchFaceHostApi
         this.renderer = renderer
 
         for ((_, complication) in complicationSlots) {
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/main/java/androidx/wear/watchface/WatchFace.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index 541c0da..6ed0000 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -44,6 +44,7 @@
 import androidx.wear.watchface.complications.SystemDataSources
 import androidx.wear.watchface.complications.data.ComplicationData
 import androidx.wear.watchface.complications.data.toApiComplicationData
+import androidx.wear.watchface.control.HeadlessWatchFaceImpl
 import androidx.wear.watchface.control.data.ComplicationRenderParams
 import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams
 import androidx.wear.watchface.control.data.WatchFaceRenderParams
@@ -207,8 +208,8 @@
                 watchFaceService.setContext(context)
                 val engine = watchFaceService.createHeadlessEngine() as
                     WatchFaceService.EngineWrapper
-                engine.createHeadlessInstance(params)
-                return engine.deferredWatchFaceImpl.await().WFEditorDelegate()
+                val headlessWatchFaceImpl = engine.createHeadlessInstance(params)
+                return engine.deferredWatchFaceImpl.await().WFEditorDelegate(headlessWatchFaceImpl)
             }
         }
     }
@@ -727,7 +728,10 @@
         }
 
         if (!watchState.isHeadless) {
-            WatchFace.registerEditorDelegate(componentName, WFEditorDelegate())
+            WatchFace.registerEditorDelegate(
+                componentName,
+                WFEditorDelegate(headlessWatchFaceImpl = null)
+            )
             registerReceivers()
         }
 
@@ -778,7 +782,9 @@
         }
     }
 
-    internal inner class WFEditorDelegate : WatchFace.EditorDelegate {
+    internal inner class WFEditorDelegate(
+        private val headlessWatchFaceImpl: HeadlessWatchFaceImpl?
+    ) : WatchFace.EditorDelegate {
         override val userStyleSchema
             get() = currentUserStyleRepository.schema
 
@@ -849,8 +855,10 @@
             complicationSlotsManager.configExtrasChangeCallback = callback
         }
 
+        @SuppressLint("NewApi") // release
         override fun onDestroy(): Unit = TraceEvent("WFEditorDelegate.onDestroy").use {
             if (watchState.isHeadless) {
+                headlessWatchFaceImpl!!.release()
                 this@WatchFaceImpl.onDestroy()
             }
         }
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceHostApi.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceHostApi.kt
index 32d6ffa..4e70ee1 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceHostApi.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceHostApi.kt
@@ -37,6 +37,17 @@
     /** The [WatchFaceService.SystemTimeProvider]. */
     public val systemTimeProvider: WatchFaceService.SystemTimeProvider
 
+    /**
+     * Equivalent to [android.os.Build.VERSION.SDK_INT], but allows override for any
+     * platform-independent versioning.
+     *
+     * This is meant to only be used in androidTest, which only support testing on one SDK. In
+     * Robolectric tests use `@Config(sdk = [Build.VERSION_CODES.*])`.
+     *
+     * Note that this cannot override platform-dependent versioning, which means inconsistency.
+     */
+    public val wearSdkVersion: Int
+
     /** Returns the watch face's [Context]. */
     public fun getContext(): Context
 
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 86d287cc..b3ee562 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -16,6 +16,7 @@
 
 package androidx.wear.watchface
 
+import android.support.wearable.complications.ComplicationData as WireComplicationData
 import android.app.KeyguardManager
 import android.content.ComponentName
 import android.content.Context
@@ -25,7 +26,6 @@
 import android.graphics.Canvas
 import android.graphics.Rect
 import android.os.Build
-import android.os.Build.VERSION.SDK_INT
 import android.os.Bundle
 import android.os.Handler
 import android.os.HandlerThread
@@ -56,9 +56,9 @@
 import androidx.annotation.WorkerThread
 import androidx.versionedparcelable.ParcelUtils
 import androidx.wear.watchface.complications.SystemDataSources.DataSourceId
-import androidx.wear.watchface.complications.data.ComplicationPersistencePolicies
 import androidx.wear.watchface.complications.data.ComplicationData
 import androidx.wear.watchface.complications.data.ComplicationExperimental
+import androidx.wear.watchface.complications.data.ComplicationPersistencePolicies
 import androidx.wear.watchface.complications.data.ComplicationType
 import androidx.wear.watchface.complications.data.NoDataComplicationData
 import androidx.wear.watchface.complications.data.toApiComplicationData
@@ -112,9 +112,6 @@
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withTimeout
 
-/** The wire format for [ComplicationData]. */
-internal typealias WireComplicationData = android.support.wearable.complications.ComplicationData
-
 /**
  * After user code finishes, we need up to 100ms of wake lock holding for the drawing to occur. This
  * isn't the ideal approach, but the framework doesn't expose a callback that would tell us when our
@@ -364,6 +361,7 @@
             UI,
             CURRENT
         }
+
         /**
          * Waits for deferredValue using runBlocking, then executes the task on the thread
          * specified by executionThread param.
@@ -389,6 +387,7 @@
                                     task(deferredValue)
                                 }
                             }
+
                             ExecutionThread.CURRENT -> {
                                 task(deferredValue)
                             }
@@ -714,10 +713,16 @@
     internal open fun allowWatchFaceToAnimate() = true
 
     /**
-     * Whether or not the pre R style init flow (SET_BINDER wallpaper command) is expected.
-     * This is open for use by tests.
+     * Equivalent to [Build.VERSION.SDK_INT], but allows override for any platform-independent
+     * versioning.
+     *
+     * This is meant to only be used in androidTest, which only support testing on one SDK. In
+     * Robolectric tests use `@Config(sdk = [Build.VERSION_CODES.*])`.
+     *
+     * Note that this cannot override platform-dependent versioning, which means inconsistency.
      */
-    internal open fun isPreAndroidR() = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
+    @VisibleForTesting
+    internal open val wearSdkVersion = Build.VERSION.SDK_INT
 
     /** [Choreographer] isn't supposed to be mocked, so we use a thin wrapper. */
     internal interface ChoreographerWrapper {
@@ -728,6 +733,7 @@
     /** This is open to allow mocking. */
     internal open fun getChoreographer(): ChoreographerWrapper = object : ChoreographerWrapper {
         private val choreographer = Choreographer.getInstance()
+
         init {
             require(Looper.myLooper() == Looper.getMainLooper()) {
                 "Creating choreographer not on the main thread"
@@ -1229,6 +1235,7 @@
 
         private lateinit var choreographer: ChoreographerWrapper
         override val systemTimeProvider = getSystemTimeProvider()
+        override val wearSdkVersion = this@WatchFaceService.wearSdkVersion
 
         /**
          * Whether we already have a [frameCallback] posted and waiting in the [Choreographer]
@@ -1357,7 +1364,7 @@
                 InteractiveInstanceManager.takePendingWallpaperInteractiveWatchFaceInstance()
 
             // In a direct boot scenario attempt to load the previously serialized parameters.
-            if (pendingWallpaperInstance == null && !isPreAndroidR()) {
+            if (pendingWallpaperInstance == null && wearSdkVersion >= Build.VERSION_CODES.R) {
                 val params = readDirectBootPrefs(_context, DIRECT_BOOT_PREFS)
                 directBootParams = params
                 // In tests a watchface may already have been created.
@@ -1767,7 +1774,7 @@
         ): Bundle? {
             // From android R onwards the integration changes and no wallpaper commands are allowed
             // or expected and can/should be ignored.
-            if (!isPreAndroidR()) {
+            if (wearSdkVersion >= Build.VERSION_CODES.R) {
                 TraceEvent("onCommand Ignored").close()
                 return null
             }
@@ -1776,26 +1783,32 @@
                     uiThreadHandler.runOnHandlerWithTracing("onCommand COMMAND_AMBIENT_UPDATE") {
                         ambientTickUpdate()
                     }
+
                 Constants.COMMAND_BACKGROUND_ACTION ->
                     uiThreadHandler.runOnHandlerWithTracing("onCommand COMMAND_BACKGROUND_ACTION") {
                         wslFlow.onBackgroundAction(extras!!)
                     }
+
                 Constants.COMMAND_COMPLICATION_DATA ->
                     uiThreadHandler.runOnHandlerWithTracing("onCommand COMMAND_COMPLICATION_DATA") {
                         wslFlow.onComplicationSlotDataUpdate(extras!!)
                     }
+
                 Constants.COMMAND_REQUEST_STYLE ->
                     uiThreadHandler.runOnHandlerWithTracing("onCommand COMMAND_REQUEST_STYLE") {
                         wslFlow.onRequestStyle()
                     }
+
                 Constants.COMMAND_SET_BINDER ->
                     uiThreadHandler.runOnHandlerWithTracing("onCommand COMMAND_SET_BINDER") {
                         wslFlow.onSetBinder(extras!!)
                     }
+
                 Constants.COMMAND_SET_PROPERTIES ->
                     uiThreadHandler.runOnHandlerWithTracing("onCommand COMMAND_SET_PROPERTIES") {
                         wslFlow.onPropertiesChanged(extras!!)
                     }
+
                 Constants.COMMAND_TAP ->
                     uiThreadCoroutineScope.runBlockingWithTracing("onCommand COMMAND_TAP") {
                         val watchFaceImpl = deferredWatchFaceImpl.await()
@@ -1810,6 +1823,7 @@
                             )
                         )
                     }
+
                 Constants.COMMAND_TOUCH ->
                     uiThreadCoroutineScope.runBlockingWithTracing("onCommand COMMAND_TOUCH") {
                         val watchFaceImpl = deferredWatchFaceImpl.await()
@@ -1824,6 +1838,7 @@
                             )
                         )
                     }
+
                 Constants.COMMAND_TOUCH_CANCEL ->
                     uiThreadCoroutineScope.runBlockingWithTracing(
                         "onCommand COMMAND_TOUCH_CANCEL"
@@ -1840,6 +1855,7 @@
                             )
                         )
                     }
+
                 else -> {
                 }
             }
@@ -2074,6 +2090,7 @@
                     TraceEvent("WatchFaceService.createComplicationsManager").use {
                         createComplicationSlotsManager(currentUserStyleRepository)
                     }
+                complicationSlotsManager.watchFaceHostApi = this@EngineWrapper
                 complicationSlotsManager.watchState = watchState
                 complicationSlotsManager.listenForStyleChanges(uiThreadCoroutineScope)
                 listenForComplicationChanges(complicationSlotsManager)
@@ -2187,7 +2204,7 @@
                 deferredWatchFaceImpl,
                 uiThreadCoroutineScope,
                 _context.contentResolver,
-                !isPreAndroidR()
+                wearSdkVersion >= Build.VERSION_CODES.R
             )
 
             // There's no point creating BroadcastsReceiver or listening for Accessibility state
@@ -2298,7 +2315,7 @@
             // to render soon anyway.
             var initFinished = false
             complicationSlotsManager.init(
-                this, renderer,
+                renderer,
                 object : ComplicationSlot.InvalidateListener {
                     @SuppressWarnings("SyntheticAccessor")
                     override fun onInvalidate() {
@@ -2323,7 +2340,7 @@
             // watchface after requesting a change. It used [Constants.ACTION_REQUEST_STATE] as a
             // signal to trigger the old boot flow (sending the binder etc). This is no longer
             // required from android R onwards. See (b/181965946).
-            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            if (wearSdkVersion < Build.VERSION_CODES.R) {
                 // We are requesting state every time the watch face changes its visibility because
                 // wallpaper commands have a tendency to be dropped. By requesting it on every
                 // visibility change, we ensure that we don't become a victim of some race
@@ -2634,7 +2651,9 @@
          * enabled.
          */
         private fun maybeSendContentDescriptionLabelsBroadcast() {
-            if (!isPreAndroidR() && getAccessibilityManager().isEnabled) {
+            if (
+                wearSdkVersion >= Build.VERSION_CODES.R && getAccessibilityManager().isEnabled
+            ) {
                 // TODO(alexclarke): This should require a permission. See http://b/184717802
                 _context.sendBroadcast(
                     Intent(Constants.ACTION_WATCH_FACE_REFRESH_A11Y_LABELS)
@@ -2697,8 +2716,11 @@
                     writer.println("WSL style init flow")
                     writer.println("watchFaceInitStarted=${wslFlow.watchFaceInitStarted}")
                 }
+
                 this.watchFaceCreatedOrPending() -> writer.println("Androidx style init flow")
-                isPreAndroidR() -> writer.println("Expecting WSL style init")
+                wearSdkVersion < Build.VERSION_CODES.R ->
+                    writer.println("Expecting WSL style init")
+
                 else -> writer.println("Expecting androidx style style init")
             }
 
@@ -2750,7 +2772,7 @@
         indentingPrintWriter.println("AndroidX WatchFaceService $packageName")
         InteractiveInstanceManager.dump(indentingPrintWriter)
         EditorService.globalEditorService.dump(indentingPrintWriter)
-        if (SDK_INT >= 27) {
+        if (Build.VERSION.SDK_INT >= 27) {
             HeadlessWatchFaceImpl.dump(indentingPrintWriter)
         }
         indentingPrintWriter.flush()
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
index a5f3897..2c2540b 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
@@ -55,7 +55,7 @@
             indentingPrintWriter.decreaseIndent()
         }
 
-        private val headlessInstances = HashSet<HeadlessWatchFaceImpl>()
+        internal val headlessInstances = HashSet<HeadlessWatchFaceImpl>()
     }
 
     init {
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
index 283dbb4..c09f23b 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
@@ -112,12 +112,11 @@
         prefs: WallpaperInteractiveWatchFaceInstanceParams
     ) {
     }
-
-    override fun isPreAndroidR() = false
 }
 
-@Config(manifest = Config.NONE)
+@Config(manifest = Config.NONE, sdk = [Build.VERSION_CODES.R])
 @RunWith(WatchFaceTestRunner::class)
+@RequiresApi(Build.VERSION_CODES.R)
 public class AsyncWatchFaceInitTest {
     private val handler = mock<Handler>()
     private val surfaceHolder = mock<SurfaceHolder>()
@@ -134,6 +133,7 @@
         WatchUiState(false, 0),
         UserStyle(emptyMap()).toWireFormat(),
         null,
+        null,
         null
     )
 
@@ -192,10 +192,10 @@
 
     @After
     fun tearDown() {
+        InteractiveInstanceManager.releaseInstance(initParams.instanceId)
         assertThat(InteractiveInstanceManager.getInstances()).isEmpty()
     }
 
-    @RequiresApi(Build.VERSION_CODES.O_MR1)
     @Test
     public fun createInteractiveInstanceFailsIfDirectBootWatchFaceCreationIsInProgress() {
         val completableWatchFace = CompletableDeferred<WatchFace>()
@@ -236,11 +236,8 @@
         runPostedTasksFor(0)
 
         assertThat(pendingException.message).startsWith("WatchFace already exists!")
-
-        InteractiveInstanceManager.releaseInstance(initParams.instanceId)
     }
 
-    @RequiresApi(Build.VERSION_CODES.O_MR1)
     @Test
     public fun directBootAndGetExistingInstanceOrSetPendingWallpaperInteractiveWatchFaceInstance() {
         val completableDirectBootWatchFace = CompletableDeferred<WatchFace>()
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/TestCommon.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
index 67b8777..5ab6d6e 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
@@ -55,7 +55,6 @@
     private val watchState: MutableWatchState?,
     private val handler: Handler,
     private val tapListener: WatchFace.TapListener?,
-    private val preAndroidR: Boolean,
     private val directBootParams: WallpaperInteractiveWatchFaceInstanceParams?,
     private val choreographer: ChoreographerWrapper,
     var mockSystemTimeMillis: Long = 0L,
@@ -171,8 +170,6 @@
         complicationCache?.set(fileName, byteArray)
     }
 
-    override fun isPreAndroidR() = preAndroidR
-
     override fun getSystemTimeProvider() = object : SystemTimeProvider {
         override fun getSystemTimeMillis() = mockSystemTimeMillis
 
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 57d6847..84970d4 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
@@ -16,6 +16,8 @@
 
 package androidx.wear.watchface
 
+import android.support.wearable.complications.ComplicationData as WireComplicationData
+import android.support.wearable.complications.ComplicationText as WireComplicationText
 import android.annotation.SuppressLint
 import android.app.NotificationManager
 import android.app.PendingIntent
@@ -36,8 +38,6 @@
 import android.os.Looper
 import android.os.Parcel
 import android.provider.Settings
-import android.support.wearable.complications.ComplicationData
-import android.support.wearable.complications.ComplicationText
 import android.support.wearable.watchface.Constants
 import android.support.wearable.watchface.IWatchFaceService
 import android.support.wearable.watchface.WatchFaceStyle
@@ -50,13 +50,13 @@
 import androidx.annotation.Px
 import androidx.annotation.RequiresApi
 import androidx.test.core.app.ApplicationProvider
-import androidx.test.filters.SdkSuppress
 import androidx.wear.watchface.complications.ComplicationSlotBounds
 import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
 import androidx.wear.watchface.complications.SystemDataSources
-import androidx.wear.watchface.complications.data.ComplicationPersistencePolicies
 import androidx.wear.watchface.complications.data.ComplicationDisplayPolicies
 import androidx.wear.watchface.complications.data.ComplicationExperimental
+import androidx.wear.watchface.complications.data.ComplicationPersistencePolicies
+import androidx.wear.watchface.complications.data.ComplicationText
 import androidx.wear.watchface.complications.data.ComplicationType
 import androidx.wear.watchface.complications.data.CountUpTimeReference
 import androidx.wear.watchface.complications.data.EmptyComplicationData
@@ -68,6 +68,7 @@
 import androidx.wear.watchface.complications.data.TimeDifferenceStyle
 import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
 import androidx.wear.watchface.complications.rendering.ComplicationDrawable
+import androidx.wear.watchface.control.HeadlessWatchFaceImpl
 import androidx.wear.watchface.control.IInteractiveWatchFace
 import androidx.wear.watchface.control.IPendingInteractiveWatchFace
 import androidx.wear.watchface.control.IWatchfaceListener
@@ -92,9 +93,6 @@
 import androidx.wear.watchface.style.WatchFaceLayer
 import androidx.wear.watchface.style.data.UserStyleWireFormat
 import com.google.common.truth.Truth.assertThat
-import org.mockito.kotlin.eq
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.verifyNoMoreInteractions
 import java.io.StringWriter
 import java.nio.ByteBuffer
 import java.time.Instant
@@ -106,10 +104,12 @@
 import kotlin.test.assertFailsWith
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.android.asCoroutineDispatcher
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
 import org.junit.After
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
@@ -123,15 +123,18 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.any
 import org.mockito.ArgumentMatchers.anyLong
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.atLeastOnce
 import org.mockito.Mockito.doAnswer
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.times
 import org.mockito.Mockito.validateMockitoUsage
 import org.mockito.Mockito.verify
-import org.robolectric.annotation.Config
+import org.mockito.Mockito.`when`
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verifyNoMoreInteractions
 import org.robolectric.Shadows.shadowOf
+import org.robolectric.annotation.Config
 
 private const val INTERACTIVE_UPDATE_RATE_MS = 16L
 private const val LEFT_COMPLICATION_ID = 1000
@@ -547,7 +550,6 @@
             watchState,
             handler,
             tapListener,
-            true,
             null,
             choreographer
         )
@@ -561,8 +563,8 @@
                     complication.id,
                     when (complication.defaultDataSourceType) {
                         ComplicationType.SHORT_TEXT ->
-                            ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                                .setShortText(ComplicationText.plainText("Initial Short"))
+                            WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                                .setShortText(WireComplicationText.plainText("Initial Short"))
                                 .setTapAction(
                                     PendingIntent.getActivity(
                                         context, 0, Intent("ShortText"),
@@ -572,8 +574,8 @@
                                 .build()
 
                         ComplicationType.LONG_TEXT ->
-                            ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                                .setShortText(ComplicationText.plainText("Initial Long"))
+                            WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                                .setShortText(WireComplicationText.plainText("Initial Long"))
                                 .setTapAction(
                                     PendingIntent.getActivity(
                                         context, 0, Intent("LongText"),
@@ -583,7 +585,7 @@
                                 .build()
 
                         ComplicationType.PHOTO_IMAGE ->
-                            ComplicationData.Builder(ComplicationData.TYPE_LARGE_IMAGE)
+                            WireComplicationData.Builder(WireComplicationData.TYPE_LARGE_IMAGE)
                                 .setLargeImage(Icon.createWithContentUri("someuri"))
                                 .setTapAction(
                                     PendingIntent.getActivity(
@@ -608,12 +610,25 @@
     }
 
     private fun initWallpaperInteractiveWatchFaceInstance(
-        @WatchFaceType watchFaceType: Int,
-        complicationSlots: List<ComplicationSlot>,
-        userStyleSchema: UserStyleSchema,
-        wallpaperInteractiveWatchFaceInstanceParams: WallpaperInteractiveWatchFaceInstanceParams,
+        @WatchFaceType watchFaceType: Int = WatchFaceType.ANALOG,
+        complicationSlots: List<ComplicationSlot> = emptyList(),
+        userStyleSchema: UserStyleSchema = UserStyleSchema(emptyList()),
+        wallpaperInteractiveWatchFaceInstanceParams: WallpaperInteractiveWatchFaceInstanceParams =
+            WallpaperInteractiveWatchFaceInstanceParams(
+                INTERACTIVE_INSTANCE_ID,
+                DeviceConfig(
+                    false,
+                    false,
+                    0,
+                    0
+                ),
+                WatchUiState(false, 0),
+                UserStyle(emptyMap()).toWireFormat(),
+                null,
+                null,
+                null
+            ),
         complicationCache: MutableMap<String, ByteArray>? = null,
-        preAndroidR: Boolean = false
     ) {
         testWatchFaceService = TestWatchFaceService(
             watchFaceType,
@@ -631,7 +646,6 @@
             watchState,
             handler,
             null,
-            preAndroidR,
             null,
             choreographer,
             mockSystemTimeMillis = looperTimeMillis,
@@ -666,8 +680,8 @@
         // [WatchFaceService.createWatchFace] Will have run by now because we're using an immediate
         // coroutine dispatcher.
         runBlocking {
-            watchFaceImpl = engineWrapper.deferredWatchFaceImpl.await()
-            engineWrapper.deferredValidation.await()
+            watchFaceImpl = engineWrapper.deferredWatchFaceImpl.awaitWithTimeout()
+            engineWrapper.deferredValidation.awaitWithTimeout()
         }
 
         currentUserStyleRepository = watchFaceImpl.currentUserStyleRepository
@@ -711,7 +725,7 @@
 
     private fun setComplicationViaWallpaperCommand(
         complicationSlotId: Int,
-        complicationData: ComplicationData
+        complicationData: WireComplicationData
     ) {
         engineWrapper.onCommand(
             Constants.COMMAND_COMPLICATION_DATA,
@@ -759,6 +773,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun maybeUpdateDrawMode_setsCorrectDrawMode() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -814,6 +829,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun onDraw_zonedDateTime_setFromSystemTime() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -828,6 +844,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun onDraw_zonedDateTime_affectedCorrectly_with2xMockTime() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -856,6 +873,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun onDraw_zonedDateTime_affectedCorrectly_withMockTimeWrapping() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -927,6 +945,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun singleTaps_correctlyDetected() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -974,6 +993,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun computeBounds_does_not_mutate_it() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1001,6 +1021,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun singleTaps_inMargins_correctlyDetected() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1066,6 +1087,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     @Suppress("DEPRECATION") // setDefaultDataSourceType
     public fun lowestIdComplicationSelectedWhenMarginsOverlap() {
         val complication100 =
@@ -1121,6 +1143,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun singleTaps_onDifferentComplications() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1147,6 +1170,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun tapCancel_after_tapDown_CancelsTap() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1161,6 +1185,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun edgeComplication_tap() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1191,6 +1216,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun edgeComplicationWithBoundingArc_tap() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1226,6 +1252,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun tapListener_tap() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1250,6 +1277,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun tapListener_tap_viaWallpaperCommand() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1300,6 +1328,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun tapListener_tapComplication() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1324,6 +1353,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun interactiveFrameRate_reducedWhenBatteryLow() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1355,6 +1385,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun interactiveFrameRate_restoreWhenPowerConnectedAfterBatteryLow() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1386,6 +1417,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun computeDelayTillNextFrame_accountsForSlowDraw() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1404,6 +1436,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun computeDelayTillNextFrame_verySlowDraw() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1422,6 +1455,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun computeDelayTillNextFrame_beginFrameTimeInTheFuture() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1442,6 +1476,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun computeDelayTillNextFrame_1000ms_update_atTopOfSecond() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1463,6 +1498,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun computeDelayTillNextFrame_frame_scheduled_at_near_perfect_time() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1484,6 +1520,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun computeDelayTillNextFrame_60000ms_update_atTopOfMinute() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1505,6 +1542,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun computeDelayTillNextFrame_60000ms_update_with_stopwatchComplication() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1521,7 +1559,7 @@
                             TimeDifferenceStyle.STOPWATCH,
                             CountUpTimeReference(Instant.parse("2022-10-30T10:15:30.001Z"))
                         ).setMinimumTimeUnit(TimeUnit.MINUTES).build(),
-                        androidx.wear.watchface.complications.data.ComplicationText.EMPTY
+                        ComplicationText.EMPTY
                     ).build().asWireComplicationData()
                 )
             )
@@ -1542,6 +1580,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun complicationSlotsManager_getNextChangeInstant() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1564,7 +1603,7 @@
                             TimeDifferenceStyle.STOPWATCH,
                             CountUpTimeReference(referenceInstant)
                         ).setMinimumTimeUnit(TimeUnit.HOURS).build(),
-                        androidx.wear.watchface.complications.data.ComplicationText.EMPTY
+                        ComplicationText.EMPTY
                     ).build().asWireComplicationData()
                 )
             )
@@ -1585,7 +1624,7 @@
                             TimeDifferenceStyle.STOPWATCH,
                             CountUpTimeReference(referenceInstant)
                         ).setMinimumTimeUnit(TimeUnit.SECONDS).build(),
-                        androidx.wear.watchface.complications.data.ComplicationText.EMPTY
+                        ComplicationText.EMPTY
                     ).build().asWireComplicationData()
                 )
             )
@@ -1596,6 +1635,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun getComplicationSlotIdAt_returnsCorrectComplications() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1614,6 +1654,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun getBackgroundComplicationSlotId_returnsNull() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1627,6 +1668,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun getBackgroundComplicationSlotId_returnsCorrectId() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1639,6 +1681,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun getStoredUserStyleNotSupported_userStyle_isPersisted() {
         // The style should get persisted in a file because this test is set up using the legacy
         // Wear 2.0 APIs.
@@ -1678,7 +1721,6 @@
             watchState,
             handler,
             null,
-            true,
             null,
             choreographer
         )
@@ -1701,62 +1743,39 @@
             )
     }
 
-    @SdkSuppress(maxSdkVersion = 29)
     @Test
-    public fun onApplyWindowInsetsBeforeR_setsChinHeight() {
-        initEngine(
-            WatchFaceType.ANALOG,
-            emptyList(),
-            UserStyleSchema(emptyList())
-        )
-        // Initially the chin size is set to zero.
-        assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(0)
-        // When window insets are delivered to the watch face.
-        engineWrapper.onApplyWindowInsets(getChinWindowInsetsApi25(chinHeight = 12))
-        // Then the chin size is updated.
-        assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(12)
-    }
-
-    @SdkSuppress(minSdkVersion = 30)
-    @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
+    @RequiresApi(Build.VERSION_CODES.R)
     public fun onApplyWindowInsetsRAndAbove_setsChinHeight() {
-        initEngine(
+        initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
             emptyList(),
-            UserStyleSchema(emptyList())
+            UserStyleSchema(emptyList()),
+            WallpaperInteractiveWatchFaceInstanceParams(
+                INTERACTIVE_INSTANCE_ID,
+                DeviceConfig(
+                    false,
+                    false,
+                    0,
+                    0
+                ),
+                WatchUiState(false, 0),
+                UserStyle(emptyMap()).toWireFormat(),
+                null,
+                null,
+                null
+            )
         )
         // Initially the chin size is set to zero.
         assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(0)
         // When window insets are delivered to the watch face.
-        engineWrapper.onApplyWindowInsets(getChinWindowInsetsApi30(chinHeight = 12))
+        engineWrapper.onApplyWindowInsets(getChinWindowInsets(chinHeight = 12))
         // Then the chin size is updated.
         assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(12)
     }
 
     @Test
-    public fun onApplyWindowInsetsBeforeR_multipleCallsIgnored() {
-        initEngine(
-            WatchFaceType.ANALOG,
-            emptyList(),
-            UserStyleSchema(emptyList())
-        )
-        // Initially the chin size is set to zero.
-        assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(0)
-        // When window insets are delivered to the watch face.
-        engineWrapper.onApplyWindowInsets(getChinWindowInsetsApi25(chinHeight = 12))
-        // Then the chin size is updated.
-        assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(12)
-        // When the same window insets are delivered to the watch face again.
-        engineWrapper.onApplyWindowInsets(getChinWindowInsetsApi25(chinHeight = 12))
-        // Nothing happens.
-        assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(12)
-        // When different window insets are delivered to the watch face again.
-        engineWrapper.onApplyWindowInsets(getChinWindowInsetsApi25(chinHeight = 24))
-        // Nothing happens and the size is unchanged.
-        assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(12)
-    }
-
-    @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun initWallpaperInteractiveWatchFaceInstanceWithUserStyle() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -1778,6 +1797,7 @@
                     )
                 ).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
@@ -1794,6 +1814,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun initWallpaperInteractiveWatchFaceInstanceWithUserStyleThatDoesntMatchSchema() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -1810,6 +1831,7 @@
                 WatchUiState(false, 0),
                 UserStyle(mapOf(watchHandStyleSetting to badStyleOption)).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
@@ -1819,6 +1841,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun wear2ImmutablePropertiesSetCorrectly() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1834,6 +1857,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun wear2ImmutablePropertiesSetCorrectly2() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1849,6 +1873,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun wallpaperInteractiveWatchFaceImmutablePropertiesSetCorrectly() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -1865,6 +1890,7 @@
                 WatchUiState(false, 0),
                 UserStyle(mapOf(watchHandStyleSetting to badStyleOption)).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
@@ -1875,6 +1901,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun invalidOldStyleIdReplacedWithDefault() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -1886,6 +1913,7 @@
                 WatchUiState(false, 0),
                 UserStyle(mapOf(watchHandStyleSetting to badStyleOption)).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
@@ -1894,6 +1922,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun onCreate_calls_setActiveComplications_withCorrectIDs() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1908,6 +1937,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun onCreate_calls_setContentDescriptionLabels_withCorrectArgs() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1930,6 +1960,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun onCreate_calls_setContentDescriptionLabels_withCorrectArgs_noComplications() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1948,6 +1979,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun ContentDescriptionLabels_notMadeForEmptyComplication() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -1959,8 +1991,8 @@
         // We're only sending one complication, the others should default to empty.
         setComplicationViaWallpaperCommand(
             rightComplication.id,
-            ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                .setShortText(ComplicationText.plainText("Initial Short"))
+            WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                .setShortText(WireComplicationText.plainText("Initial Short"))
                 .build()
         )
 
@@ -1976,6 +2008,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun moveComplications() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -2039,6 +2072,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun styleChangesAccessibilityTraversalIndex() {
         val rightAndSelectComplicationsOption = ComplicationSlotsOption(
             Option.Id(RIGHT_AND_LEFT_COMPLICATIONS),
@@ -2153,6 +2187,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun centerX_and_centerY_containUpToDateValues() {
         initEngine(WatchFaceType.ANALOG, emptyList(), UserStyleSchema(emptyList()))
 
@@ -2175,6 +2210,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun requestStyleBeforeSetBinder() {
         testWatchFaceService = TestWatchFaceService(
             WatchFaceType.ANALOG,
@@ -2191,7 +2227,6 @@
             watchState,
             handler,
             null,
-            true,
             null,
             choreographer
         )
@@ -2214,6 +2249,7 @@
 
     @Suppress("DEPRECATION") // setDefaultDataSourceType
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun defaultComplicationDataSourcesWithFallbacks_newApi() {
         val dataSource1 = ComponentName("com.app1", "com.app1.App1")
         val dataSource2 = ComponentName("com.app2", "com.app2.App2")
@@ -2239,12 +2275,13 @@
             LEFT_COMPLICATION_ID,
             listOf(dataSource1, dataSource2),
             SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET,
-            ComplicationData.TYPE_SHORT_TEXT
+            WireComplicationData.TYPE_SHORT_TEXT
         )
     }
 
     @Suppress("DEPRECATION") // setDefaultDataSourceType
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun defaultComplicationDataSourcesWithFallbacks_oldApi() {
         val dataSource1 = ComponentName("com.app1", "com.app1.App1")
         val dataSource2 = ComponentName("com.app2", "com.app2.App2")
@@ -2272,19 +2309,20 @@
         runPostedTasksFor(0)
 
         verify(iWatchFaceService).setDefaultComplicationProvider(
-            LEFT_COMPLICATION_ID, dataSource2, ComplicationData.TYPE_SHORT_TEXT
+            LEFT_COMPLICATION_ID, dataSource2, WireComplicationData.TYPE_SHORT_TEXT
         )
         verify(iWatchFaceService).setDefaultComplicationProvider(
-            LEFT_COMPLICATION_ID, dataSource1, ComplicationData.TYPE_SHORT_TEXT
+            LEFT_COMPLICATION_ID, dataSource1, WireComplicationData.TYPE_SHORT_TEXT
         )
         verify(iWatchFaceService).setDefaultSystemComplicationProvider(
             LEFT_COMPLICATION_ID,
             SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET,
-            ComplicationData.TYPE_SHORT_TEXT
+            WireComplicationData.TYPE_SHORT_TEXT
         )
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun previewReferenceTimeMillisAnalog() {
         val instanceParams = WallpaperInteractiveWatchFaceInstanceParams(
             INTERACTIVE_INSTANCE_ID,
@@ -2302,6 +2340,7 @@
                 )
             ).toWireFormat(),
             null,
+            null,
             null
         )
 
@@ -2316,6 +2355,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun previewReferenceTimeMillisDigital() {
         val instanceParams = WallpaperInteractiveWatchFaceInstanceParams(
             INTERACTIVE_INSTANCE_ID,
@@ -2333,6 +2373,7 @@
                 )
             ).toWireFormat(),
             null,
+            null,
             null
         )
 
@@ -2347,6 +2388,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun getComplicationDetails() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -2366,11 +2408,11 @@
         )
         assertThat(complicationDetails[0].complicationState.supportedTypes).isEqualTo(
             intArrayOf(
-                ComplicationData.TYPE_RANGED_VALUE,
-                ComplicationData.TYPE_LONG_TEXT,
-                ComplicationData.TYPE_SHORT_TEXT,
-                ComplicationData.TYPE_ICON,
-                ComplicationData.TYPE_SMALL_IMAGE
+                WireComplicationData.TYPE_RANGED_VALUE,
+                WireComplicationData.TYPE_LONG_TEXT,
+                WireComplicationData.TYPE_SHORT_TEXT,
+                WireComplicationData.TYPE_ICON,
+                WireComplicationData.TYPE_SMALL_IMAGE
             )
         )
 
@@ -2383,11 +2425,11 @@
         )
         assertThat(complicationDetails[1].complicationState.supportedTypes).isEqualTo(
             intArrayOf(
-                ComplicationData.TYPE_RANGED_VALUE,
-                ComplicationData.TYPE_LONG_TEXT,
-                ComplicationData.TYPE_SHORT_TEXT,
-                ComplicationData.TYPE_ICON,
-                ComplicationData.TYPE_SMALL_IMAGE
+                WireComplicationData.TYPE_RANGED_VALUE,
+                WireComplicationData.TYPE_LONG_TEXT,
+                WireComplicationData.TYPE_SHORT_TEXT,
+                WireComplicationData.TYPE_ICON,
+                WireComplicationData.TYPE_SMALL_IMAGE
             )
         )
 
@@ -2399,11 +2441,12 @@
             Rect(0, 0, 100, 100)
         )
         assertThat(complicationDetails[2].complicationState.supportedTypes).isEqualTo(
-            intArrayOf(ComplicationData.TYPE_LARGE_IMAGE)
+            intArrayOf(WireComplicationData.TYPE_LARGE_IMAGE)
         )
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun getComplicationDetails_early_init_with_styleOverrides() {
         val complicationsStyleSetting = ComplicationSlotsUserStyleSetting(
             UserStyleSetting.Id("complications_style_setting"),
@@ -2424,7 +2467,7 @@
             listOf(leftComplication, rightComplication),
             { _, currentUserStyleRepository, watchState ->
                 // Prevent initialization until initDeferred completes.
-                initDeferred.await()
+                initDeferred.awaitWithTimeout()
                 renderer = TestRenderer(
                     surfaceHolder,
                     currentUserStyleRepository,
@@ -2437,7 +2480,6 @@
             watchState,
             handler,
             null,
-            false,
             null,
             choreographer
         )
@@ -2458,6 +2500,7 @@
                             set(complicationsStyleSetting, leftOnlyComplicationsOption)
                         }.toUserStyle().toWireFormat(),
                         emptyList(),
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
@@ -2493,6 +2536,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun shouldAnimateOverrideControlsEnteringAmbientMode() {
         lateinit var testRenderer: TestRendererWithShouldAnimate
         testWatchFaceService = TestWatchFaceService(
@@ -2511,7 +2555,6 @@
             watchState,
             handler,
             null,
-            true,
             null,
             choreographer
         )
@@ -2538,6 +2581,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun complicationsUserStyleSettingSelectionAppliesChanges() {
         initEngine(
             WatchFaceType.DIGITAL,
@@ -2581,6 +2625,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun partialComplicationOverrides() {
         initEngine(
             WatchFaceType.DIGITAL,
@@ -2612,8 +2657,10 @@
         assertFalse(leftComplication.enabled)
         assertTrue(rightComplication.enabled)
         assertEquals(rightComplication.nameResourceId, NAME_RESOURCE_ID)
-        assertEquals(rightComplication.screenReaderNameResourceId,
-            SCREEN_READER_NAME_RESOURCE_ID)
+        assertEquals(
+            rightComplication.screenReaderNameResourceId,
+            SCREEN_READER_NAME_RESOURCE_ID
+        )
 
         // Select both complicationSlots.
         val mutableUserStyleC = currentUserStyleRepository.userStyle.value.toMutableUserStyle()
@@ -2627,6 +2674,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun partialComplicationOverrideAppliedToInitialStyle() {
         val complicationsStyleSetting = ComplicationSlotsUserStyleSetting(
             UserStyleSetting.Id("complications_style_setting"),
@@ -2731,11 +2779,8 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.TIRAMISU])
     fun hierarchical_complicationsStyleSetting() {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
-            return
-        }
-
         val option1 = ListUserStyleSetting.ListOption(
             Option.Id("1"),
             displayName = "1",
@@ -2765,11 +2810,24 @@
             WatchFaceLayer.ALL_WATCH_FACE_LAYERS
         )
 
-        initEngine(
+        initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.DIGITAL,
             listOf(leftComplication, rightComplication),
             UserStyleSchema(listOf(choice, complicationsStyleSetting, complicationsStyleSetting2)),
-            apiVersion = 4
+            WallpaperInteractiveWatchFaceInstanceParams(
+                INTERACTIVE_INSTANCE_ID,
+                DeviceConfig(
+                    false,
+                    false,
+                    0,
+                    0
+                ),
+                WatchUiState(false, 0),
+                UserStyle(emptyMap()).toWireFormat(),
+                null,
+                null,
+                null
+            )
         )
 
         currentUserStyleRepository.updateUserStyle(
@@ -2811,6 +2869,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun observeComplicationData() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -2827,12 +2886,13 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
 
-        lateinit var leftComplicationData: ComplicationData
-        lateinit var rightComplicationData: ComplicationData
+        lateinit var leftComplicationData: WireComplicationData
+        lateinit var rightComplicationData: WireComplicationData
 
         val scope = CoroutineScope(Dispatchers.Main.immediate)
 
@@ -2852,26 +2912,27 @@
             listOf(
                 IdAndComplicationDataWireFormat(
                     LEFT_COMPLICATION_ID,
-                    ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT)
-                        .setLongText(ComplicationText.plainText("TYPE_LONG_TEXT")).build()
+                    WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+                        .setLongText(WireComplicationText.plainText("TYPE_LONG_TEXT")).build()
                 ),
                 IdAndComplicationDataWireFormat(
                     RIGHT_COMPLICATION_ID,
-                    ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                        .setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT")).build()
+                    WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                        .setShortText(WireComplicationText.plainText("TYPE_SHORT_TEXT")).build()
                 )
             )
         )
 
-        assertThat(leftComplicationData.type).isEqualTo(ComplicationData.TYPE_LONG_TEXT)
+        assertThat(leftComplicationData.type).isEqualTo(WireComplicationData.TYPE_LONG_TEXT)
         assertThat(leftComplicationData.longText?.getTextAt(context.resources, 0))
             .isEqualTo("TYPE_LONG_TEXT")
-        assertThat(rightComplicationData.type).isEqualTo(ComplicationData.TYPE_SHORT_TEXT)
+        assertThat(rightComplicationData.type).isEqualTo(WireComplicationData.TYPE_SHORT_TEXT)
         assertThat(rightComplicationData.shortText?.getTextAt(context.resources, 0))
             .isEqualTo("TYPE_SHORT_TEXT")
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun complicationCache() {
         val complicationCache = HashMap<String, ByteArray>()
         val instanceParams = WallpaperInteractiveWatchFaceInstanceParams(
@@ -2880,6 +2941,7 @@
             WatchUiState(false, 0),
             UserStyle(emptyMap()).toWireFormat(),
             null,
+            null,
             null
         )
         initWallpaperInteractiveWatchFaceInstance(
@@ -2901,8 +2963,8 @@
             listOf(
                 IdAndComplicationDataWireFormat(
                     LEFT_COMPLICATION_ID,
-                    ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT)
-                        .setLongText(ComplicationText.plainText("TYPE_LONG_TEXT"))
+                    WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+                        .setLongText(WireComplicationText.plainText("TYPE_LONG_TEXT"))
                         .setTapAction(
                             PendingIntent.getActivity(
                                 context, 0, Intent("LongText"),
@@ -2913,8 +2975,8 @@
                 ),
                 IdAndComplicationDataWireFormat(
                     RIGHT_COMPLICATION_ID,
-                    ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                        .setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT")).build()
+                    WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                        .setShortText(WireComplicationText.plainText("TYPE_SHORT_TEXT")).build()
                 )
             )
         )
@@ -2942,7 +3004,6 @@
             watchState,
             handler,
             null,
-            true,
             null,
             choreographer,
             complicationCache = complicationCache
@@ -2977,7 +3038,7 @@
         // [WatchFaceService.createWatchFace] Will have run by now because we're using an immediate
         // coroutine dispatcher.
         runBlocking {
-            val watchFaceImpl2 = engineWrapper2.deferredWatchFaceImpl.await()
+            val watchFaceImpl2 = engineWrapper2.deferredWatchFaceImpl.awaitWithTimeout()
 
             // Check the ComplicationData was cached.
             val leftComplicationData =
@@ -2989,11 +3050,11 @@
                     RIGHT_COMPLICATION_ID
                 ]!!.complicationData.value.asWireComplicationData()
 
-            assertThat(leftComplicationData.type).isEqualTo(ComplicationData.TYPE_LONG_TEXT)
+            assertThat(leftComplicationData.type).isEqualTo(WireComplicationData.TYPE_LONG_TEXT)
             assertThat(leftComplicationData.longText?.getTextAt(context.resources, 0))
                 .isEqualTo("TYPE_LONG_TEXT")
             assertThat(leftComplicationData.tapActionLostDueToSerialization).isTrue()
-            assertThat(rightComplicationData.type).isEqualTo(ComplicationData.TYPE_SHORT_TEXT)
+            assertThat(rightComplicationData.type).isEqualTo(WireComplicationData.TYPE_SHORT_TEXT)
             assertThat(rightComplicationData.shortText?.getTextAt(context.resources, 0))
                 .isEqualTo("TYPE_SHORT_TEXT")
             assertThat(rightComplicationData.tapActionLostDueToSerialization).isFalse()
@@ -3003,7 +3064,7 @@
     }
 
     @Test
-    @Suppress("NewApi")
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun complicationCachePolicy() {
         val complicationCache = HashMap<String, ByteArray>()
         val instanceParams = WallpaperInteractiveWatchFaceInstanceParams(
@@ -3012,6 +3073,7 @@
             WatchUiState(false, 0),
             UserStyle(emptyMap()).toWireFormat(),
             null,
+            null,
             null
         )
         initWallpaperInteractiveWatchFaceInstance(
@@ -3027,8 +3089,8 @@
             listOf(
                 IdAndComplicationDataWireFormat(
                     LEFT_COMPLICATION_ID,
-                    ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT)
-                        .setLongText(ComplicationText.plainText("TYPE_LONG_TEXT"))
+                    WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+                        .setLongText(WireComplicationText.plainText("TYPE_LONG_TEXT"))
                         .setTapAction(
                             PendingIntent.getActivity(
                                 context, 0, Intent("LongText"),
@@ -3040,8 +3102,8 @@
                 ),
                 IdAndComplicationDataWireFormat(
                     RIGHT_COMPLICATION_ID,
-                    ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                        .setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT")).build()
+                    WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                        .setShortText(WireComplicationText.plainText("TYPE_SHORT_TEXT")).build()
                 )
             )
         )
@@ -3069,7 +3131,6 @@
             watchState,
             handler,
             null,
-            true,
             null,
             choreographer,
             complicationCache = complicationCache
@@ -3104,7 +3165,7 @@
         // [WatchFaceService.createWatchFace] Will have run by now because we're using an immediate
         // coroutine dispatcher.
         runBlocking {
-            val watchFaceImpl2 = engineWrapper2.deferredWatchFaceImpl.await()
+            val watchFaceImpl2 = engineWrapper2.deferredWatchFaceImpl.awaitWithTimeout()
 
             // Check only the right ComplicationData was cached.
             val leftComplicationData =
@@ -3116,8 +3177,8 @@
                     RIGHT_COMPLICATION_ID
                 ]!!.complicationData.value.asWireComplicationData()
 
-            assertThat(leftComplicationData.type).isEqualTo(ComplicationData.TYPE_NO_DATA)
-            assertThat(rightComplicationData.type).isEqualTo(ComplicationData.TYPE_SHORT_TEXT)
+            assertThat(leftComplicationData.type).isEqualTo(WireComplicationData.TYPE_NO_DATA)
+            assertThat(rightComplicationData.type).isEqualTo(WireComplicationData.TYPE_SHORT_TEXT)
             assertThat(rightComplicationData.shortText?.getTextAt(context.resources, 0))
                 .isEqualTo("TYPE_SHORT_TEXT")
             assertThat(rightComplicationData.tapActionLostDueToSerialization).isFalse()
@@ -3127,6 +3188,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun complicationCache_timeline() {
         val complicationCache = HashMap<String, ByteArray>()
         val instanceParams = WallpaperInteractiveWatchFaceInstanceParams(
@@ -3135,6 +3197,7 @@
             WatchUiState(false, 0),
             UserStyle(emptyMap()).toWireFormat(),
             null,
+            null,
             null
         )
         initWallpaperInteractiveWatchFaceInstance(
@@ -3149,11 +3212,11 @@
         assertThat(complicationSlotsManager[LEFT_COMPLICATION_ID]!!.complicationData.value.type)
             .isEqualTo(ComplicationType.NO_DATA)
 
-        val a = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-            .setShortText(ComplicationText.plainText("A"))
+        val a = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+            .setShortText(WireComplicationText.plainText("A"))
             .build()
-        val b = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-            .setShortText(ComplicationText.plainText("B"))
+        val b = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+            .setShortText(WireComplicationText.plainText("B"))
             .build()
         b.timelineStartEpochSecond = 1000
         b.timelineEndEpochSecond = Long.MAX_VALUE
@@ -3186,7 +3249,6 @@
             watchState,
             handler,
             null,
-            true,
             null,
             choreographer,
             complicationCache = complicationCache
@@ -3221,7 +3283,7 @@
         // [WatchFaceService.createWatchFace] Will have run by now because we're using an immediate
         // coroutine dispatcher.
         runBlocking {
-            val watchFaceImpl2 = engineWrapper2.deferredWatchFaceImpl.await()
+            val watchFaceImpl2 = engineWrapper2.deferredWatchFaceImpl.awaitWithTimeout()
 
             watchFaceImpl2.complicationSlotsManager.selectComplicationDataForInstant(
                 Instant.ofEpochSecond(999)
@@ -3233,7 +3295,7 @@
                     LEFT_COMPLICATION_ID
                 ]!!.complicationData.value.asWireComplicationData()
 
-            assertThat(leftComplicationData.type).isEqualTo(ComplicationData.TYPE_SHORT_TEXT)
+            assertThat(leftComplicationData.type).isEqualTo(WireComplicationData.TYPE_SHORT_TEXT)
             assertThat(leftComplicationData.shortText?.getTextAt(context.resources, 0))
                 .isEqualTo("A")
 
@@ -3246,7 +3308,7 @@
                     LEFT_COMPLICATION_ID
                 ]!!.complicationData.value.asWireComplicationData()
 
-            assertThat(leftComplicationData.type).isEqualTo(ComplicationData.TYPE_SHORT_TEXT)
+            assertThat(leftComplicationData.type).isEqualTo(WireComplicationData.TYPE_SHORT_TEXT)
             assertThat(leftComplicationData.shortText?.getTextAt(context.resources, 0))
                 .isEqualTo("B")
         }
@@ -3255,6 +3317,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun complicationsInitialized_with_NoComplicationComplicationData() {
         initEngine(
             WatchFaceType.DIGITAL,
@@ -3271,8 +3334,9 @@
         ).isInstanceOf(NoDataComplicationData::class.java)
     }
 
-    @RequiresApi(Build.VERSION_CODES.O_MR1)
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
+    @RequiresApi(Build.VERSION_CODES.O_MR1)
     public fun headless_complicationsInitialized_with_EmptyComplicationData() {
         testWatchFaceService = TestWatchFaceService(
             WatchFaceType.ANALOG,
@@ -3289,7 +3353,6 @@
             watchState,
             handler,
             null,
-            false, // Allows DirectBoot
             WallpaperInteractiveWatchFaceInstanceParams(
                 "Headless",
                 DeviceConfig(
@@ -3306,6 +3369,7 @@
                     )
                 ).toWireFormat(),
                 null,
+                null,
                 null
             ),
             choreographer
@@ -3327,7 +3391,7 @@
         // [WatchFaceService.createWatchFace] Will have run by now because we're using an immediate
         // coroutine dispatcher.
         runBlocking {
-            watchFaceImpl = engineWrapper.deferredWatchFaceImpl.await()
+            watchFaceImpl = engineWrapper.deferredWatchFaceImpl.awaitWithTimeout()
         }
 
         assertThat(
@@ -3339,6 +3403,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun complication_isActiveAt() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -3355,6 +3420,7 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
@@ -3363,8 +3429,8 @@
             listOf(
                 IdAndComplicationDataWireFormat(
                     LEFT_COMPLICATION_ID,
-                    ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                        .setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT"))
+                    WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                        .setShortText(WireComplicationText.plainText("TYPE_SHORT_TEXT"))
                         .build()
                 )
             )
@@ -3378,7 +3444,7 @@
             listOf(
                 IdAndComplicationDataWireFormat(
                     LEFT_COMPLICATION_ID,
-                    ComplicationData.Builder(ComplicationData.TYPE_EMPTY).build()
+                    WireComplicationData.Builder(WireComplicationData.TYPE_EMPTY).build()
                 )
             )
         )
@@ -3390,8 +3456,8 @@
             listOf(
                 IdAndComplicationDataWireFormat(
                     LEFT_COMPLICATION_ID,
-                    ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                        .setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT"))
+                    WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                        .setShortText(WireComplicationText.plainText("TYPE_SHORT_TEXT"))
                         .setStartDateTimeMillis(1000000)
                         .setEndDateTimeMillis(2000000)
                         .build()
@@ -3406,6 +3472,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun updateInvalidCompliationIdDoesNotCrash() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -3422,6 +3489,7 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
@@ -3431,8 +3499,8 @@
             listOf(
                 IdAndComplicationDataWireFormat(
                     RIGHT_COMPLICATION_ID,
-                    ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                        .setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT"))
+                    WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                        .setShortText(WireComplicationText.plainText("TYPE_SHORT_TEXT"))
                         .build()
                 )
             )
@@ -3453,6 +3521,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun watchStateStateFlowDataMembersHaveValues() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -3469,6 +3538,7 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
@@ -3480,6 +3550,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun isBatteryLowAndNotCharging_modified_by_broadcasts() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -3496,6 +3567,7 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
@@ -3515,6 +3587,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun processBatteryStatus() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -3531,6 +3604,7 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
@@ -3570,6 +3644,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun isAmbientInitalisedEvenWithoutPropertiesSent() {
         testWatchFaceService = TestWatchFaceService(
             WatchFaceType.ANALOG,
@@ -3586,7 +3661,6 @@
             watchState,
             handler,
             null,
-            true,
             null,
             choreographer
         )
@@ -3602,6 +3676,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun ambientToInteractiveTransition() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -3618,6 +3693,7 @@
                 WatchUiState(true, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
@@ -3647,6 +3723,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun interactiveToAmbientTransition() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -3663,6 +3740,7 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
@@ -3685,6 +3763,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun onDestroy_clearsInstanceRecord() {
         val instanceId = INTERACTIVE_INSTANCE_ID
         initWallpaperInteractiveWatchFaceInstance(
@@ -3702,6 +3781,7 @@
                 WatchUiState(false, 0),
                 UserStyle(hashMapOf(colorStyleSetting to blueStyleOption)).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
@@ -3711,6 +3791,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun sendComplicationWallpaperCommandPreRFlow() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -3720,8 +3801,8 @@
 
         setComplicationViaWallpaperCommand(
             LEFT_COMPLICATION_ID,
-            ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                .setShortText(ComplicationText.plainText("Override"))
+            WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                .setShortText(WireComplicationText.plainText("Override"))
                 .build()
         )
 
@@ -3737,6 +3818,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun sendComplicationWallpaperCommandIgnoredPostRFlow() {
         val instanceId = INTERACTIVE_INSTANCE_ID
         initWallpaperInteractiveWatchFaceInstance(
@@ -3756,11 +3838,12 @@
                 listOf(
                     IdAndComplicationDataWireFormat(
                         LEFT_COMPLICATION_ID,
-                        ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                            .setShortText(ComplicationText.plainText("INITIAL_VALUE"))
+                        WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                            .setShortText(WireComplicationText.plainText("INITIAL_VALUE"))
                             .build()
                     )
                 ),
+                null,
                 null
             )
         )
@@ -3768,8 +3851,8 @@
         // This should be ignored because we're on the R flow.
         setComplicationViaWallpaperCommand(
             LEFT_COMPLICATION_ID,
-            ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                .setShortText(ComplicationText.plainText("Override"))
+            WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                .setShortText(WireComplicationText.plainText("Override"))
                 .build()
         )
 
@@ -3785,6 +3868,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun directBoot() {
         val instanceId = "DirectBootInstance"
         testWatchFaceService = TestWatchFaceService(
@@ -3802,7 +3886,6 @@
             watchState,
             handler,
             null,
-            false, // Allows DirectBoot
             WallpaperInteractiveWatchFaceInstanceParams(
                 instanceId,
                 DeviceConfig(
@@ -3819,6 +3902,7 @@
                     )
                 ).toWireFormat(),
                 null,
+                null,
                 null
             ),
             choreographer
@@ -3840,6 +3924,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun headlessFlagPreventsDirectBoot() {
         val instanceId = "DirectBootInstance"
         testWatchFaceService = TestWatchFaceService(
@@ -3857,7 +3942,6 @@
             watchState,
             handler,
             null,
-            false, // Allows DirectBoot
             WallpaperInteractiveWatchFaceInstanceParams(
                 instanceId,
                 DeviceConfig(
@@ -3874,6 +3958,7 @@
                     )
                 ).toWireFormat(),
                 null,
+                null,
                 null
             ),
             choreographer
@@ -3888,6 +3973,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun firstOnVisibilityChangedIgnoredPostRFlow() {
         val instanceId = INTERACTIVE_INSTANCE_ID
         initWallpaperInteractiveWatchFaceInstance(
@@ -3907,11 +3993,12 @@
                 listOf(
                     IdAndComplicationDataWireFormat(
                         LEFT_COMPLICATION_ID,
-                        ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                            .setShortText(ComplicationText.plainText("INITIAL_VALUE"))
+                        WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                            .setShortText(WireComplicationText.plainText("INITIAL_VALUE"))
                             .build()
                     )
                 ),
+                null,
                 null
             )
         )
@@ -3941,6 +4028,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun complicationsUserStyleSetting_with_setComplicationBounds() {
         val rightComplicationBoundsOption = ComplicationSlotsOption(
             Option.Id(RIGHT_COMPLICATION),
@@ -3989,11 +4077,12 @@
                 listOf(
                     IdAndComplicationDataWireFormat(
                         LEFT_COMPLICATION_ID,
-                        ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                            .setShortText(ComplicationText.plainText("INITIAL_VALUE"))
+                        WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                            .setShortText(WireComplicationText.plainText("INITIAL_VALUE"))
                             .build()
                     )
                 ),
+                null,
                 null
             )
         )
@@ -4019,6 +4108,7 @@
 
     @Suppress("DEPRECATION") // DefaultComplicationDataSourcePolicyAndType
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun canvasComplication_onRendererCreated() {
         val leftCanvasComplication = mock<CanvasComplication>()
         val leftComplication =
@@ -4058,6 +4148,7 @@
 
     @Suppress("DEPRECATION") // DefaultComplicationDataSourcePolicyAndType
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun complicationSlotsWithTheSameRenderer() {
         val sameCanvasComplication = mock<CanvasComplication>()
         val leftComplication =
@@ -4091,12 +4182,13 @@
         assertTrue(engineWrapper.deferredValidation.isCancelled)
         runBlocking {
             assertFailsWith<IllegalArgumentException> {
-                engineWrapper.deferredValidation.await()
+                engineWrapper.deferredValidation.awaitWithTimeout()
             }
         }
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun additionalContentDescriptionLabelsSetBeforeWatchFaceInitComplete() {
         val pendingIntent = PendingIntent.getActivity(
             context, 0, Intent("Example"),
@@ -4131,7 +4223,6 @@
             watchState,
             handler,
             null,
-            false,
             null,
             choreographer
         )
@@ -4150,6 +4241,7 @@
                         WatchUiState(false, 0),
                         UserStyle(emptyMap()).toWireFormat(),
                         emptyList(),
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
@@ -4187,6 +4279,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun onAccessibilityStateChanged_preAndroidR() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -4203,9 +4296,9 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 emptyList(),
+                null,
                 null
             ),
-            preAndroidR = true
         )
 
         engineWrapper.systemViewOfContentDescriptionLabelsIsStale = true
@@ -4218,6 +4311,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun onAccessibilityStateChanged_androidR_or_above() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -4234,9 +4328,9 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 emptyList(),
+                null,
                 null
             ),
-            preAndroidR = false
         )
 
         engineWrapper.systemViewOfContentDescriptionLabelsIsStale = true
@@ -4250,6 +4344,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun contentDescriptionLabels_contains_ComplicationData() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -4266,6 +4361,7 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 emptyList(),
+                null,
                 null
             )
         )
@@ -4283,15 +4379,15 @@
             mutableListOf(
                 IdAndComplicationDataWireFormat(
                     LEFT_COMPLICATION_ID,
-                    ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                        .setShortText(ComplicationText.plainText("LEFT!"))
+                    WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                        .setShortText(WireComplicationText.plainText("LEFT!"))
                         .setTapAction(leftPendingIntent)
                         .build()
                 ),
                 IdAndComplicationDataWireFormat(
                     RIGHT_COMPLICATION_ID,
-                    ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                        .setShortText(ComplicationText.plainText("RIGHT!"))
+                    WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                        .setShortText(WireComplicationText.plainText("RIGHT!"))
                         .setTapAction(rightPendingIntent)
                         .build()
                 )
@@ -4318,6 +4414,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun schemaWithTooLargeIcon() {
         val tooLargeIcon = Icon.createWithBitmap(
             Bitmap.createBitmap(
@@ -4357,6 +4454,7 @@
                         )
                     ).toWireFormat(),
                     null,
+                    null,
                     null
                 )
             )
@@ -4371,6 +4469,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun schemaWithTooLargeWireFormat() {
         val longOptionsList = ArrayList<ListUserStyleSetting.ListOption>()
         for (i in 0..10000) {
@@ -4403,6 +4502,7 @@
                     WatchUiState(false, 0),
                     UserStyle(hashMapOf(watchHandStyleSetting to gothicStyleOption)).toWireFormat(),
                     null,
+                    null,
                     null
                 )
             )
@@ -4417,6 +4517,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun getComplicationSlotMetadataWireFormats_parcelTest() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -4433,6 +4534,7 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 emptyList(),
+                null,
                 null
             )
         )
@@ -4463,6 +4565,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     @RequiresApi(Build.VERSION_CODES.O_MR1)
     public fun glesRendererLifecycle() {
         val eventLog = ArrayList<String>()
@@ -4515,7 +4618,6 @@
             null,
             handler,
             null,
-            false,
             null,
             choreographer,
             forceIsVisible = true
@@ -4535,6 +4637,7 @@
                         WatchUiState(false, 0),
                         UserStyle(emptyMap()).toWireFormat(),
                         emptyList(),
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
@@ -4591,6 +4694,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     @RequiresApi(Build.VERSION_CODES.O_MR1)
     public fun assetLifeCycle_CanvasRenderer() {
         val eventLog = ArrayList<String>()
@@ -4654,7 +4758,6 @@
             null,
             handler,
             null,
-            false,
             null,
             choreographer,
             forceIsVisible = true
@@ -4674,6 +4777,7 @@
                         WatchUiState(false, 0),
                         UserStyle(emptyMap()).toWireFormat(),
                         emptyList(),
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
@@ -4728,6 +4832,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     @RequiresApi(Build.VERSION_CODES.O_MR1)
     public fun assetLifeCycle_GlesRenderer() {
         val eventLog = ArrayList<String>()
@@ -4785,7 +4890,6 @@
             null,
             handler,
             null,
-            false,
             null,
             choreographer,
             forceIsVisible = true
@@ -4805,6 +4909,7 @@
                         WatchUiState(false, 0),
                         UserStyle(emptyMap()).toWireFormat(),
                         emptyList(),
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
@@ -4859,6 +4964,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun renderer_onDestroy_called_even_if_init_not_complete() {
         val initDeferred = CompletableDeferred<Unit>()
         var onDestroyCalled = false
@@ -4875,7 +4981,7 @@
                     // Prevent initialization until initDeferred completes.
                     override suspend fun init() {
                         super.init()
-                        initDeferred.await()
+                        initDeferred.awaitWithTimeout()
                     }
 
                     override fun onDestroy() {
@@ -4889,7 +4995,6 @@
             watchState,
             handler,
             null,
-            false,
             null,
             choreographer
         )
@@ -4908,6 +5013,7 @@
                         WatchUiState(false, 0),
                         UserStyle(emptyMap()).toWireFormat(),
                         emptyList(),
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
@@ -4939,6 +5045,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun dump_androidXFlow() {
         // Advance time a little so timestamps are not zero
         looperTimeMillis = 1000
@@ -4958,6 +5065,7 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 emptyList(),
+                null,
                 null
             )
         )
@@ -4995,6 +5103,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun dump_wslFlow() {
         initEngine(
             WatchFaceType.DIGITAL,
@@ -5012,6 +5121,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun uiThreadPriority_interactive() {
         testWatchFaceService = TestWatchFaceService(
             WatchFaceType.DIGITAL,
@@ -5028,7 +5138,6 @@
             watchState,
             handler,
             null,
-            false,
             null,
             choreographer,
             mainThreadPriorityDelegate = mainThreadPriorityDelegate
@@ -5048,6 +5157,7 @@
                         WatchUiState(false, 0),
                         UserStyle(emptyMap()).toWireFormat(),
                         emptyList(),
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
@@ -5090,8 +5200,9 @@
         assertThat(mainThreadPriorityDelegate.priority).isEqualTo(Priority.Normal)
     }
 
-    @RequiresApi(Build.VERSION_CODES.O_MR1)
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
+    @RequiresApi(Build.VERSION_CODES.O_MR1)
     public fun uiThreadPriority_headless() {
         testWatchFaceService = TestWatchFaceService(
             WatchFaceType.ANALOG,
@@ -5108,7 +5219,6 @@
             watchState,
             handler,
             null,
-            false, // Allows DirectBoot
             WallpaperInteractiveWatchFaceInstanceParams(
                 "Headless",
                 DeviceConfig(
@@ -5125,6 +5235,7 @@
                     )
                 ).toWireFormat(),
                 null,
+                null,
                 null
             ),
             choreographer
@@ -5147,6 +5258,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     fun onVisibilityChanged_true_always_renders_a_frame() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -5165,6 +5277,8 @@
         assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(2000L)
     }
 
+    @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     @RequiresApi(Build.VERSION_CODES.O_MR1)
     public fun headlessId() {
         testWatchFaceService = TestWatchFaceService(
@@ -5182,9 +5296,8 @@
             watchState,
             handler,
             null,
-            false, // Allows DirectBoot
             WallpaperInteractiveWatchFaceInstanceParams(
-                "Headless",
+                "wfId-Headless",
                 DeviceConfig(
                     false,
                     false,
@@ -5194,6 +5307,7 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null,
+                null,
                 null
             ),
             choreographer
@@ -5208,25 +5322,26 @@
                 DeviceConfig(false, false, 100, 200),
                 100,
                 100,
-                "Headless-instance"
+                "wfId-Headless-instance"
             )
         )
-        assertThat(watchState.watchFaceInstanceId.value).isEqualTo("Headless-instance")
+        assertThat(watchState.watchFaceInstanceId.value).isEqualTo("wfId-Headless-instance")
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun selectComplicationDataForInstant_overlapping() {
-        val a = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-            .setShortText(ComplicationText.plainText("A"))
+        val a = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+            .setShortText(WireComplicationText.plainText("A"))
             .build()
-        val b = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-            .setShortText(ComplicationText.plainText("B"))
+        val b = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+            .setShortText(WireComplicationText.plainText("B"))
             .build()
         b.timelineStartEpochSecond = 1000
         b.timelineEndEpochSecond = 4000
 
-        val c = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-            .setShortText(ComplicationText.plainText("C"))
+        val c = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+            .setShortText(WireComplicationText.plainText("C"))
             .build()
         c.timelineStartEpochSecond = 2000
         c.timelineEndEpochSecond = 3000
@@ -5269,18 +5384,19 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun selectComplicationDataForInstant_disjoint() {
-        val a = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-            .setShortText(ComplicationText.plainText("A"))
+        val a = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+            .setShortText(WireComplicationText.plainText("A"))
             .build()
-        val b = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-            .setShortText(ComplicationText.plainText("B"))
+        val b = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+            .setShortText(WireComplicationText.plainText("B"))
             .build()
         b.timelineStartEpochSecond = 1000
         b.timelineEndEpochSecond = 2000
 
-        val c = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-            .setShortText(ComplicationText.plainText("C"))
+        val c = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+            .setShortText(WireComplicationText.plainText("C"))
             .build()
         c.timelineStartEpochSecond = 3000
         c.timelineEndEpochSecond = 4000
@@ -5323,28 +5439,27 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun selectComplicationDataForInstant_timeLineWithPlaceholder() {
-        val placeholderText =
-            androidx.wear.watchface.complications.data.ComplicationText.PLACEHOLDER
         val timelineEntry =
-            ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
+            WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                 .setPlaceholder(
-                    ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT)
-                        .setLongText(placeholderText.toWireComplicationText())
+                    WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+                        .setLongText(ComplicationText.PLACEHOLDER.toWireComplicationText())
                         .build()
                 )
                 .build()
         timelineEntry.timelineStartEpochSecond = 100
         timelineEntry.timelineEndEpochSecond = 1000
 
-        val wireLongTextComplication = ComplicationData.Builder(
+        val wireLongTextComplication = WireComplicationData.Builder(
             ComplicationType.LONG_TEXT.toWireComplicationType()
         )
             .setEndDateTimeMillis(1650988800000)
             .setDataSource(ComponentName("a", "b"))
-            .setLongText(ComplicationText.plainText("longText"))
-            .setSmallImageStyle(ComplicationData.IMAGE_STYLE_ICON)
-            .setContentDescription(ComplicationText.plainText("test"))
+            .setLongText(WireComplicationText.plainText("longText"))
+            .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_ICON)
+            .setContentDescription(WireComplicationText.plainText("test"))
             .build()
         wireLongTextComplication.setTimelineEntryCollection(listOf(timelineEntry))
 
@@ -5373,6 +5488,50 @@
     }
 
     @Test
+    public fun updateComplicationTimelineOnly_updatesComplication() {
+        // Arrange
+        initWallpaperInteractiveWatchFaceInstance(complicationSlots = listOf(leftComplication))
+        val defaultBase = WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+            .setLongText(WireComplicationText("default"))
+            .build()
+        val timelineEntryBase = WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+            .setLongText(WireComplicationText("timeline"))
+            .build()
+        val oldTimelineEntry = WireComplicationData.Builder(defaultBase).build().apply {
+            setTimelineEntryCollection(
+                listOf(
+                    WireComplicationData.Builder(timelineEntryBase).build().apply {
+                        timelineStartEpochSecond = 100
+                        timelineEndEpochSecond = 200
+                    }
+                )
+            )
+        }
+        val newTimelineEntry = WireComplicationData.Builder(defaultBase).build().apply {
+            setTimelineEntryCollection(
+                listOf(
+                    WireComplicationData.Builder(timelineEntryBase).build().apply {
+                        timelineStartEpochSecond = 200
+                        timelineEndEpochSecond = 300
+                    }
+                )
+            )
+        }
+        engineWrapper.setComplicationDataList(
+            listOf(IdAndComplicationDataWireFormat(LEFT_COMPLICATION_ID, oldTimelineEntry))
+        )
+        complicationSlotsManager.selectComplicationDataForInstant(Instant.ofEpochSecond(150))
+        // Act
+        engineWrapper.setComplicationDataList(
+            listOf(IdAndComplicationDataWireFormat(LEFT_COMPLICATION_ID, newTimelineEntry))
+        )
+        complicationSlotsManager.selectComplicationDataForInstant(Instant.ofEpochSecond(250))
+        // Assert
+        assertThat(getLeftLongTextComplicationDataText()).isEqualTo("timeline")
+    }
+
+    @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun renderParameters_isScreenshot() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -5389,6 +5548,7 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
@@ -5413,6 +5573,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     @Suppress("Deprecation")
     public fun applyComplicationSlotsStyleCategoryOption() {
         initWallpaperInteractiveWatchFaceInstance(
@@ -5430,6 +5591,7 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
@@ -5526,6 +5688,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun onActionScreenOff_preR() {
         Settings.Global.putInt(
             context.contentResolver,
@@ -5548,7 +5711,6 @@
             watchState,
             handler,
             null,
-            true,
             null,
             choreographer
         )
@@ -5567,6 +5729,7 @@
                         WatchUiState(false, 0),
                         UserStyle(emptyMap()).toWireFormat(),
                         emptyList(),
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
@@ -5597,6 +5760,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun onActionScreenOff_ambientNotEnabled() {
         Settings.Global.putInt(
             context.contentResolver,
@@ -5619,7 +5783,6 @@
             watchState,
             handler,
             null,
-            false,
             null,
             choreographer
         )
@@ -5638,6 +5801,7 @@
                         WatchUiState(false, 0),
                         UserStyle(emptyMap()).toWireFormat(),
                         emptyList(),
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
@@ -5668,6 +5832,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun onActionScreenOff_onActionScreenOn_ambientEnabled() {
         Settings.Global.putInt(
             context.contentResolver,
@@ -5690,7 +5855,6 @@
             watchState,
             handler,
             null,
-            false,
             null,
             choreographer
         )
@@ -5709,6 +5873,7 @@
                         WatchUiState(false, 0),
                         UserStyle(emptyMap()).toWireFormat(),
                         emptyList(),
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
@@ -5752,6 +5917,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun onActionTimeTick() {
         testWatchFaceService = TestWatchFaceService(
             WatchFaceType.DIGITAL,
@@ -5768,7 +5934,6 @@
             watchState,
             handler,
             null,
-            false,
             null,
             choreographer
         )
@@ -5787,6 +5952,7 @@
                         WatchUiState(false, 0),
                         UserStyle(emptyMap()).toWireFormat(),
                         emptyList(),
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
@@ -5830,6 +5996,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun setComplicationDataListMergesCorrectly() {
         initEngine(
             WatchFaceType.ANALOG,
@@ -5839,22 +6006,22 @@
 
         val left1 = IdAndComplicationDataWireFormat(
             LEFT_COMPLICATION_ID,
-            ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                .setShortText(ComplicationText.plainText("Left1"))
+            WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                .setShortText(WireComplicationText.plainText("Left1"))
                 .build()
         )
 
         val left2 = IdAndComplicationDataWireFormat(
             LEFT_COMPLICATION_ID,
-            ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                .setShortText(ComplicationText.plainText("Left2"))
+            WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                .setShortText(WireComplicationText.plainText("Left2"))
                 .build()
         )
 
         val right = IdAndComplicationDataWireFormat(
             RIGHT_COMPLICATION_ID,
-            ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                .setShortText(ComplicationText.plainText("Right"))
+            WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                .setShortText(WireComplicationText.plainText("Right"))
                 .build()
         )
 
@@ -5872,6 +6039,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun updateInstance() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -5888,6 +6056,7 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
@@ -5916,17 +6085,18 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun updateComplications_after_updateInstance() {
         val complicationList = listOf(
             IdAndComplicationDataWireFormat(
                 LEFT_COMPLICATION_ID,
-                ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT)
-                    .setLongText(ComplicationText.plainText("TYPE_LONG_TEXT")).build()
+                WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+                    .setLongText(WireComplicationText.plainText("TYPE_LONG_TEXT")).build()
             ),
             IdAndComplicationDataWireFormat(
                 RIGHT_COMPLICATION_ID,
-                ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
-                    .setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT")).build()
+                WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                    .setShortText(WireComplicationText.plainText("TYPE_SHORT_TEXT")).build()
             )
         )
 
@@ -5945,6 +6115,7 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
@@ -5960,21 +6131,25 @@
         }
 
         assertThat(leftComplication.complicationData.value).isInstanceOf(
-            NoDataComplicationData::class.java)
+            NoDataComplicationData::class.java
+        )
         assertThat(rightComplication.complicationData.value).isInstanceOf(
-            NoDataComplicationData::class.java)
+            NoDataComplicationData::class.java
+        )
 
         interactiveWatchFaceInstance.updateComplicationData(complicationList)
 
         assertThat(leftComplication.complicationData.value).isInstanceOf(
-            LongTextComplicationData::class.java)
+            LongTextComplicationData::class.java
+        )
         assertThat(rightComplication.complicationData.value).isInstanceOf(
-            ShortTextComplicationData::class.java)
+            ShortTextComplicationData::class.java
+        )
     }
 
     @OptIn(WatchFaceExperimental::class)
     @Test
-    @RequiresApi(27)
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun onComputeColors() {
         @Suppress("DEPRECATION")
         lateinit var renderer: Renderer.CanvasRenderer
@@ -5999,7 +6174,8 @@
                         canvas: Canvas,
                         bounds: Rect,
                         zonedDateTime: ZonedDateTime
-                    ) { }
+                    ) {
+                    }
 
                     override fun renderHighlightLayer(
                         canvas: Canvas,
@@ -6014,7 +6190,6 @@
             null,
             handler,
             null,
-            false,
             null,
             choreographer,
             forceIsVisible = true
@@ -6034,6 +6209,7 @@
                         WatchUiState(false, 0),
                         UserStyle(emptyMap()).toWireFormat(),
                         emptyList(),
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
@@ -6057,7 +6233,7 @@
         val listener = object : IWatchfaceListener.Stub() {
             override fun getApiVersion() = 1
 
-            override fun onWatchfaceReady() { }
+            override fun onWatchfaceReady() {}
 
             override fun onWatchfaceColorsChanged(watchFaceColors: WatchFaceColorsWireFormat?) {
                 lastWatchFaceColors = watchFaceColors?.toApiFormat()
@@ -6098,6 +6274,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun onPreviewImageUpdateRequested() {
         @Suppress("DEPRECATION")
         lateinit var renderer: Renderer.CanvasRenderer
@@ -6117,7 +6294,8 @@
                         canvas: Canvas,
                         bounds: Rect,
                         zonedDateTime: ZonedDateTime
-                    ) { }
+                    ) {
+                    }
 
                     override fun renderHighlightLayer(
                         canvas: Canvas,
@@ -6132,7 +6310,6 @@
             null,
             handler,
             null,
-            false,
             null,
             choreographer,
             forceIsVisible = true
@@ -6152,6 +6329,7 @@
                         WatchUiState(false, 0),
                         UserStyle(emptyMap()).toWireFormat(),
                         emptyList(),
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
@@ -6175,7 +6353,7 @@
         val listener = object : IWatchfaceListener.Stub() {
             override fun getApiVersion() = 1
 
-            override fun onWatchfaceReady() { }
+            override fun onWatchfaceReady() {}
 
             override fun onWatchfaceColorsChanged(watchFaceColors: WatchFaceColorsWireFormat?) {}
 
@@ -6217,6 +6395,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun onPreviewImageUpdateRequested_earlyCall() {
         @Suppress("DEPRECATION")
         lateinit var renderer: Renderer.CanvasRenderer
@@ -6256,7 +6435,6 @@
             null,
             handler,
             null,
-            false,
             null,
             choreographer,
             forceIsVisible = true
@@ -6276,6 +6454,7 @@
                         WatchUiState(false, 0),
                         UserStyle(emptyMap()).toWireFormat(),
                         emptyList(),
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
@@ -6322,6 +6501,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     @RequiresApi(Build.VERSION_CODES.O_MR1)
     public fun sendPreviewImageNeedsUpdateRequest_headlessInstance() {
         @Suppress("DEPRECATION")
@@ -6341,11 +6521,13 @@
                     init {
                         sendPreviewImageNeedsUpdateRequest()
                     }
+
                     override fun render(
                         canvas: Canvas,
                         bounds: Rect,
                         zonedDateTime: ZonedDateTime
-                    ) { }
+                    ) {
+                    }
 
                     override fun renderHighlightLayer(
                         canvas: Canvas,
@@ -6360,7 +6542,6 @@
             null,
             handler,
             null,
-            false,
             null,
             choreographer,
             forceIsVisible = true
@@ -6381,14 +6562,15 @@
 
         // This shouldn't crash.
         runBlocking {
-            watchFaceImpl = engineWrapper.deferredWatchFaceImpl.await()
+            watchFaceImpl = engineWrapper.deferredWatchFaceImpl.awaitWithTimeout()
         }
 
         engineWrapper.onDestroy()
     }
 
     @Test
-    @Suppress("NewApi")
+    @Config(sdk = [Build.VERSION_CODES.R])
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public fun doNotDisplayComplicationWhenScreenLocked() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
@@ -6405,6 +6587,7 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
@@ -6418,7 +6601,7 @@
                             TimeDifferenceStyle.STOPWATCH,
                             CountUpTimeReference(Instant.parse("2022-10-30T10:15:30.001Z"))
                         ).setMinimumTimeUnit(TimeUnit.MINUTES).build(),
-                        androidx.wear.watchface.complications.data.ComplicationText.EMPTY
+                        ComplicationText.EMPTY
                     )
                         .setDisplayPolicy(
                             ComplicationDisplayPolicies.DO_NOT_SHOW_WHEN_DEVICE_LOCKED
@@ -6441,6 +6624,42 @@
             .isInstanceOf(ShortTextComplicationData::class.java)
     }
 
+    @SuppressLint("NewApi")
+    @Test
+    public fun createHeadlessSessionDelegate_onDestroy() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        val componentName = ComponentName(context, TestNopCanvasWatchFaceService::class.java)
+        lateinit var delegate: WatchFace.EditorDelegate
+
+        // Allows us to programmatically control tasks.
+        TestNopCanvasWatchFaceService.handler = this.handler
+
+        CoroutineScope(handler.asCoroutineDispatcher().immediate).launch {
+            delegate = WatchFace.createHeadlessSessionDelegate(
+                componentName,
+                HeadlessWatchFaceInstanceParams(
+                    componentName,
+                    DeviceConfig(false, false, 100, 200),
+                    100,
+                    100,
+                    null
+                ),
+                context
+            )
+        }
+
+        // Run all pending tasks.
+        while (pendingTasks.isNotEmpty()) {
+            pendingTasks.remove().runnable.run()
+        }
+
+        assertThat(HeadlessWatchFaceImpl.headlessInstances).isNotEmpty()
+        delegate.onDestroy()
+
+        // The headlessInstances should become empty, otherwise there's a leak.
+        assertThat(HeadlessWatchFaceImpl.headlessInstances).isEmpty()
+    }
+
     private fun getLeftShortTextComplicationDataText(): CharSequence {
         val complication = complicationSlotsManager[
             LEFT_COMPLICATION_ID
@@ -6463,17 +6682,59 @@
         )
     }
 
-    @SuppressLint("NewApi")
-    @Suppress("DEPRECATION")
-    private fun getChinWindowInsetsApi25(@Px chinHeight: Int): WindowInsets =
-        WindowInsets.Builder().setSystemWindowInsets(
-            Insets.of(0, 0, 0, chinHeight)
-        ).build()
-
-    @SuppressLint("NewApi")
-    private fun getChinWindowInsetsApi30(@Px chinHeight: Int): WindowInsets =
+    @RequiresApi(Build.VERSION_CODES.R)
+    private fun getChinWindowInsets(@Px chinHeight: Int): WindowInsets =
         WindowInsets.Builder().setInsets(
             WindowInsets.Type.systemBars(),
             Insets.of(Rect().apply { bottom = chinHeight })
         ).build()
+
+    private suspend fun <T> Deferred<T>.awaitWithTimeout(): T = withTimeout(1000) { await() }
 }
+
+class TestNopCanvasWatchFaceService : WatchFaceService() {
+    companion object {
+        lateinit var handler: Handler
+    }
+
+    override fun getUiThreadHandlerImpl() = handler
+
+    // To make unit tests simpler and non-flaky we run background tasks and ui tasks on the same
+    // handler.
+    override fun getBackgroundThreadHandlerImpl() = handler
+
+    override suspend fun createWatchFace(
+        surfaceHolder: SurfaceHolder,
+        watchState: WatchState,
+        complicationSlotsManager: ComplicationSlotsManager,
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ) = WatchFace(
+        WatchFaceType.DIGITAL,
+        @Suppress("deprecation")
+        object : Renderer.CanvasRenderer(
+            surfaceHolder,
+            currentUserStyleRepository,
+            watchState,
+            CanvasType.HARDWARE,
+            16
+        ) {
+            override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
+                // Intentionally empty.
+            }
+
+            override fun renderHighlightLayer(
+                canvas: Canvas,
+                bounds: Rect,
+                zonedDateTime: ZonedDateTime
+            ) {
+                // Intentionally empty.
+            }
+        }
+    )
+
+    override fun getSystemTimeProvider() = object : SystemTimeProvider {
+        override fun getSystemTimeMillis() = 123456789L
+
+        override fun getSystemTimeZoneId() = ZoneId.of("UTC")
+    }
+}
\ No newline at end of file
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParamsTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParamsTest.kt
index 19549fb..783b044 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParamsTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParamsTest.kt
@@ -15,7 +15,6 @@
  */
 package androidx.wear.watchface.control.data
 
-import android.content.ComponentName
 import androidx.versionedparcelable.ParcelUtils
 import androidx.wear.watchface.data.DeviceConfig
 import androidx.wear.watchface.data.WatchUiState
@@ -40,7 +39,8 @@
             ),
             UserStyle(emptyMap()).toWireFormat(),
             null,
-            ComponentName("some.package", "SomeClass")
+            null,
+            null
         )
 
         val dummyOutputStream = ByteArrayOutputStream()
diff --git a/wear/wear-samples-ambient/build.gradle b/wear/wear-samples-ambient/build.gradle
index 1cf4e6a..b73c613 100644
--- a/wear/wear-samples-ambient/build.gradle
+++ b/wear/wear-samples-ambient/build.gradle
@@ -24,8 +24,8 @@
 
 dependencies {
     api(project(":wear:wear"))
+    implementation("androidx.core:core:1.6.0")
     implementation(libs.kotlinStdlib)
-
 }
 
 androidx {
diff --git a/wear/wear-samples-ambient/src/main/java/androidx/wear/samples/ambient/MainActivity.kt b/wear/wear-samples-ambient/src/main/java/androidx/wear/samples/ambient/MainActivity.kt
index 0b9ead7..1c8e558 100644
--- a/wear/wear-samples-ambient/src/main/java/androidx/wear/samples/ambient/MainActivity.kt
+++ b/wear/wear-samples-ambient/src/main/java/androidx/wear/samples/ambient/MainActivity.kt
@@ -5,15 +5,16 @@
 import android.os.Looper
 import android.util.Log
 import android.widget.TextView
-import androidx.fragment.app.FragmentActivity
-import androidx.wear.ambient.AmbientModeSupport
+import androidx.core.app.ComponentActivity
+import androidx.wear.ambient.AmbientLifecycleObserver
+import androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientDetails
+import androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientLifecycleCallback
 import java.text.SimpleDateFormat
 import java.util.Date
 
 /** Sample activity that provides an ambient experience. */
 class MainActivity :
-    FragmentActivity(R.layout.activity_main),
-    AmbientModeSupport.AmbientCallbackProvider {
+    ComponentActivity() {
 
     /** Used to dispatch periodic updates when the activity is in active mode. */
     private val activeUpdatesHandler = Handler(Looper.getMainLooper())
@@ -22,7 +23,7 @@
     private val model = MainViewModel()
 
     /** The controller for ambient mode, initialized when the activity is created. */
-    private lateinit var ambientController: AmbientModeSupport.AmbientController
+    private lateinit var ambientObserver: AmbientLifecycleObserver
 
     // The views that are part of the activity.
     private val timerTextView by lazy { findViewById<TextView>(R.id.timer) }
@@ -30,11 +31,31 @@
     private val timestampTextView by lazy { findViewById<TextView>(R.id.timestamp) }
     private val updatesTextView by lazy { findViewById<TextView>(R.id.updates) }
 
+    private val ambientCallback = object : AmbientLifecycleCallback {
+        override fun onEnterAmbient(ambientDetails: AmbientDetails) {
+            Log.d(TAG, "onEnterAmbient()")
+            model.setStatus(Status.AMBIENT)
+            model.publishUpdate()
+        }
+
+        override fun onUpdateAmbient() {
+            Log.d(TAG, "onUpdateAmbient()")
+            model.publishUpdate()
+        }
+
+        override fun onExitAmbient() {
+            Log.d(TAG, "onExitAmbient()")
+            model.setStatus(Status.ACTIVE)
+            model.publishUpdate()
+            schedule()
+        }
+    }
+
     /** Invoked on [activeUpdatesHandler], posts an update when the activity is in active mode. */
     private val mActiveUpdatesRunnable: Runnable =
         Runnable {
             // If invoked in ambient mode, do nothing.
-            if (ambientController.isAmbient) {
+            if (ambientObserver.isAmbient()) {
                 return@Runnable
             }
             model.publishUpdate()
@@ -45,8 +66,10 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         Log.d(TAG, "onCreate")
+        setContentView(R.layout.activity_main)
         observeModel()
-        ambientController = AmbientModeSupport.attach(this)
+        ambientObserver = AmbientLifecycleObserver(this, ambientCallback)
+        this.lifecycle.addObserver(ambientObserver)
     }
 
     override fun onStart() {
@@ -78,29 +101,6 @@
         super.onDestroy()
     }
 
-    override fun getAmbientCallback() = object : AmbientModeSupport.AmbientCallback() {
-        override fun onEnterAmbient(ambientDetails: Bundle) {
-            super.onEnterAmbient(ambientDetails)
-            Log.d(TAG, "onEnterAmbient()")
-            model.setStatus(Status.AMBIENT)
-            model.publishUpdate()
-        }
-
-        override fun onUpdateAmbient() {
-            super.onUpdateAmbient()
-            Log.d(TAG, "onUpdateAmbient()")
-            model.publishUpdate()
-        }
-
-        override fun onExitAmbient() {
-            super.onExitAmbient()
-            Log.d(TAG, "onExitAmbient()")
-            model.setStatus(Status.ACTIVE)
-            model.publishUpdate()
-            schedule()
-        }
-    }
-
     private fun observeModel() {
         model.observeStartTime(this) {
             timerTextView.text = formatTimer(model.getTimer())
diff --git a/wear/wear/api/current.txt b/wear/wear/api/current.txt
index 30a2fbd..b1e72e0 100644
--- a/wear/wear/api/current.txt
+++ b/wear/wear/api/current.txt
@@ -17,6 +17,30 @@
 
 package androidx.wear.ambient {
 
+  public final class AmbientLifecycleObserver implements androidx.wear.ambient.AmbientLifecycleObserverInterface {
+    ctor public AmbientLifecycleObserver(android.app.Activity activity, java.util.concurrent.Executor callbackExecutor, androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientLifecycleCallback callbacks);
+    ctor public AmbientLifecycleObserver(android.app.Activity activity, androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientLifecycleCallback callbacks);
+    method public boolean isAmbient();
+  }
+
+  public interface AmbientLifecycleObserverInterface extends androidx.lifecycle.DefaultLifecycleObserver {
+    method public boolean isAmbient();
+  }
+
+  public static final class AmbientLifecycleObserverInterface.AmbientDetails {
+    ctor public AmbientLifecycleObserverInterface.AmbientDetails(boolean burnInProtectionRequired, boolean deviceHasLowBitAmbient);
+    method public boolean getBurnInProtectionRequired();
+    method public boolean getDeviceHasLowBitAmbient();
+    property public final boolean burnInProtectionRequired;
+    property public final boolean deviceHasLowBitAmbient;
+  }
+
+  public static interface AmbientLifecycleObserverInterface.AmbientLifecycleCallback {
+    method public default void onEnterAmbient(androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientDetails ambientDetails);
+    method public default void onExitAmbient();
+    method public default void onUpdateAmbient();
+  }
+
   @Deprecated public final class AmbientMode extends android.app.Fragment {
     ctor @Deprecated public AmbientMode();
     method @Deprecated public static <T extends android.app.Activity> androidx.wear.ambient.AmbientMode.AmbientController! attachAmbientSupport(T!);
@@ -50,30 +74,30 @@
     method @Deprecated public void setAmbientOffloadEnabled(boolean);
   }
 
-  public final class AmbientModeSupport extends androidx.fragment.app.Fragment {
-    ctor public AmbientModeSupport();
-    method public static <T extends androidx.fragment.app.FragmentActivity> androidx.wear.ambient.AmbientModeSupport.AmbientController! attach(T!);
-    field public static final String EXTRA_BURN_IN_PROTECTION = "com.google.android.wearable.compat.extra.BURN_IN_PROTECTION";
-    field public static final String EXTRA_LOWBIT_AMBIENT = "com.google.android.wearable.compat.extra.LOWBIT_AMBIENT";
-    field public static final String FRAGMENT_TAG = "android.support.wearable.ambient.AmbientMode";
+  @Deprecated public final class AmbientModeSupport extends androidx.fragment.app.Fragment {
+    ctor @Deprecated public AmbientModeSupport();
+    method @Deprecated public static <T extends androidx.fragment.app.FragmentActivity> androidx.wear.ambient.AmbientModeSupport.AmbientController! attach(T!);
+    field @Deprecated public static final String EXTRA_BURN_IN_PROTECTION = "com.google.android.wearable.compat.extra.BURN_IN_PROTECTION";
+    field @Deprecated public static final String EXTRA_LOWBIT_AMBIENT = "com.google.android.wearable.compat.extra.LOWBIT_AMBIENT";
+    field @Deprecated public static final String FRAGMENT_TAG = "android.support.wearable.ambient.AmbientMode";
   }
 
-  public abstract static class AmbientModeSupport.AmbientCallback {
-    ctor public AmbientModeSupport.AmbientCallback();
-    method public void onAmbientOffloadInvalidated();
-    method public void onEnterAmbient(android.os.Bundle!);
-    method public void onExitAmbient();
-    method public void onUpdateAmbient();
+  @Deprecated public abstract static class AmbientModeSupport.AmbientCallback {
+    ctor @Deprecated public AmbientModeSupport.AmbientCallback();
+    method @Deprecated public void onAmbientOffloadInvalidated();
+    method @Deprecated public void onEnterAmbient(android.os.Bundle!);
+    method @Deprecated public void onExitAmbient();
+    method @Deprecated public void onUpdateAmbient();
   }
 
-  public static interface AmbientModeSupport.AmbientCallbackProvider {
-    method public androidx.wear.ambient.AmbientModeSupport.AmbientCallback! getAmbientCallback();
+  @Deprecated public static interface AmbientModeSupport.AmbientCallbackProvider {
+    method @Deprecated public androidx.wear.ambient.AmbientModeSupport.AmbientCallback! getAmbientCallback();
   }
 
-  public final class AmbientModeSupport.AmbientController {
-    method public boolean isAmbient();
-    method public void setAmbientOffloadEnabled(boolean);
-    method public void setAutoResumeEnabled(boolean);
+  @Deprecated public final class AmbientModeSupport.AmbientController {
+    method @Deprecated public boolean isAmbient();
+    method @Deprecated public void setAmbientOffloadEnabled(boolean);
+    method @Deprecated public void setAutoResumeEnabled(boolean);
   }
 
 }
diff --git a/wear/wear/api/public_plus_experimental_current.txt b/wear/wear/api/public_plus_experimental_current.txt
index 30a2fbd..b1e72e0 100644
--- a/wear/wear/api/public_plus_experimental_current.txt
+++ b/wear/wear/api/public_plus_experimental_current.txt
@@ -17,6 +17,30 @@
 
 package androidx.wear.ambient {
 
+  public final class AmbientLifecycleObserver implements androidx.wear.ambient.AmbientLifecycleObserverInterface {
+    ctor public AmbientLifecycleObserver(android.app.Activity activity, java.util.concurrent.Executor callbackExecutor, androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientLifecycleCallback callbacks);
+    ctor public AmbientLifecycleObserver(android.app.Activity activity, androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientLifecycleCallback callbacks);
+    method public boolean isAmbient();
+  }
+
+  public interface AmbientLifecycleObserverInterface extends androidx.lifecycle.DefaultLifecycleObserver {
+    method public boolean isAmbient();
+  }
+
+  public static final class AmbientLifecycleObserverInterface.AmbientDetails {
+    ctor public AmbientLifecycleObserverInterface.AmbientDetails(boolean burnInProtectionRequired, boolean deviceHasLowBitAmbient);
+    method public boolean getBurnInProtectionRequired();
+    method public boolean getDeviceHasLowBitAmbient();
+    property public final boolean burnInProtectionRequired;
+    property public final boolean deviceHasLowBitAmbient;
+  }
+
+  public static interface AmbientLifecycleObserverInterface.AmbientLifecycleCallback {
+    method public default void onEnterAmbient(androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientDetails ambientDetails);
+    method public default void onExitAmbient();
+    method public default void onUpdateAmbient();
+  }
+
   @Deprecated public final class AmbientMode extends android.app.Fragment {
     ctor @Deprecated public AmbientMode();
     method @Deprecated public static <T extends android.app.Activity> androidx.wear.ambient.AmbientMode.AmbientController! attachAmbientSupport(T!);
@@ -50,30 +74,30 @@
     method @Deprecated public void setAmbientOffloadEnabled(boolean);
   }
 
-  public final class AmbientModeSupport extends androidx.fragment.app.Fragment {
-    ctor public AmbientModeSupport();
-    method public static <T extends androidx.fragment.app.FragmentActivity> androidx.wear.ambient.AmbientModeSupport.AmbientController! attach(T!);
-    field public static final String EXTRA_BURN_IN_PROTECTION = "com.google.android.wearable.compat.extra.BURN_IN_PROTECTION";
-    field public static final String EXTRA_LOWBIT_AMBIENT = "com.google.android.wearable.compat.extra.LOWBIT_AMBIENT";
-    field public static final String FRAGMENT_TAG = "android.support.wearable.ambient.AmbientMode";
+  @Deprecated public final class AmbientModeSupport extends androidx.fragment.app.Fragment {
+    ctor @Deprecated public AmbientModeSupport();
+    method @Deprecated public static <T extends androidx.fragment.app.FragmentActivity> androidx.wear.ambient.AmbientModeSupport.AmbientController! attach(T!);
+    field @Deprecated public static final String EXTRA_BURN_IN_PROTECTION = "com.google.android.wearable.compat.extra.BURN_IN_PROTECTION";
+    field @Deprecated public static final String EXTRA_LOWBIT_AMBIENT = "com.google.android.wearable.compat.extra.LOWBIT_AMBIENT";
+    field @Deprecated public static final String FRAGMENT_TAG = "android.support.wearable.ambient.AmbientMode";
   }
 
-  public abstract static class AmbientModeSupport.AmbientCallback {
-    ctor public AmbientModeSupport.AmbientCallback();
-    method public void onAmbientOffloadInvalidated();
-    method public void onEnterAmbient(android.os.Bundle!);
-    method public void onExitAmbient();
-    method public void onUpdateAmbient();
+  @Deprecated public abstract static class AmbientModeSupport.AmbientCallback {
+    ctor @Deprecated public AmbientModeSupport.AmbientCallback();
+    method @Deprecated public void onAmbientOffloadInvalidated();
+    method @Deprecated public void onEnterAmbient(android.os.Bundle!);
+    method @Deprecated public void onExitAmbient();
+    method @Deprecated public void onUpdateAmbient();
   }
 
-  public static interface AmbientModeSupport.AmbientCallbackProvider {
-    method public androidx.wear.ambient.AmbientModeSupport.AmbientCallback! getAmbientCallback();
+  @Deprecated public static interface AmbientModeSupport.AmbientCallbackProvider {
+    method @Deprecated public androidx.wear.ambient.AmbientModeSupport.AmbientCallback! getAmbientCallback();
   }
 
-  public final class AmbientModeSupport.AmbientController {
-    method public boolean isAmbient();
-    method public void setAmbientOffloadEnabled(boolean);
-    method public void setAutoResumeEnabled(boolean);
+  @Deprecated public final class AmbientModeSupport.AmbientController {
+    method @Deprecated public boolean isAmbient();
+    method @Deprecated public void setAmbientOffloadEnabled(boolean);
+    method @Deprecated public void setAutoResumeEnabled(boolean);
   }
 
 }
diff --git a/wear/wear/api/restricted_current.txt b/wear/wear/api/restricted_current.txt
index 38baf3e1..023be62 100644
--- a/wear/wear/api/restricted_current.txt
+++ b/wear/wear/api/restricted_current.txt
@@ -17,6 +17,30 @@
 
 package androidx.wear.ambient {
 
+  public final class AmbientLifecycleObserver implements androidx.wear.ambient.AmbientLifecycleObserverInterface {
+    ctor public AmbientLifecycleObserver(android.app.Activity activity, java.util.concurrent.Executor callbackExecutor, androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientLifecycleCallback callbacks);
+    ctor public AmbientLifecycleObserver(android.app.Activity activity, androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientLifecycleCallback callbacks);
+    method public boolean isAmbient();
+  }
+
+  public interface AmbientLifecycleObserverInterface extends androidx.lifecycle.DefaultLifecycleObserver {
+    method public boolean isAmbient();
+  }
+
+  public static final class AmbientLifecycleObserverInterface.AmbientDetails {
+    ctor public AmbientLifecycleObserverInterface.AmbientDetails(boolean burnInProtectionRequired, boolean deviceHasLowBitAmbient);
+    method public boolean getBurnInProtectionRequired();
+    method public boolean getDeviceHasLowBitAmbient();
+    property public final boolean burnInProtectionRequired;
+    property public final boolean deviceHasLowBitAmbient;
+  }
+
+  public static interface AmbientLifecycleObserverInterface.AmbientLifecycleCallback {
+    method public default void onEnterAmbient(androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientDetails ambientDetails);
+    method public default void onExitAmbient();
+    method public default void onUpdateAmbient();
+  }
+
   @Deprecated public final class AmbientMode extends android.app.Fragment {
     ctor @Deprecated public AmbientMode();
     method @Deprecated public static <T extends android.app.Activity> androidx.wear.ambient.AmbientMode.AmbientController! attachAmbientSupport(T!);
@@ -50,30 +74,30 @@
     method @Deprecated public void setAmbientOffloadEnabled(boolean);
   }
 
-  public final class AmbientModeSupport extends androidx.fragment.app.Fragment {
-    ctor public AmbientModeSupport();
-    method public static <T extends androidx.fragment.app.FragmentActivity> androidx.wear.ambient.AmbientModeSupport.AmbientController! attach(T!);
-    field public static final String EXTRA_BURN_IN_PROTECTION = "com.google.android.wearable.compat.extra.BURN_IN_PROTECTION";
-    field public static final String EXTRA_LOWBIT_AMBIENT = "com.google.android.wearable.compat.extra.LOWBIT_AMBIENT";
-    field public static final String FRAGMENT_TAG = "android.support.wearable.ambient.AmbientMode";
+  @Deprecated public final class AmbientModeSupport extends androidx.fragment.app.Fragment {
+    ctor @Deprecated public AmbientModeSupport();
+    method @Deprecated public static <T extends androidx.fragment.app.FragmentActivity> androidx.wear.ambient.AmbientModeSupport.AmbientController! attach(T!);
+    field @Deprecated public static final String EXTRA_BURN_IN_PROTECTION = "com.google.android.wearable.compat.extra.BURN_IN_PROTECTION";
+    field @Deprecated public static final String EXTRA_LOWBIT_AMBIENT = "com.google.android.wearable.compat.extra.LOWBIT_AMBIENT";
+    field @Deprecated public static final String FRAGMENT_TAG = "android.support.wearable.ambient.AmbientMode";
   }
 
-  public abstract static class AmbientModeSupport.AmbientCallback {
-    ctor public AmbientModeSupport.AmbientCallback();
-    method public void onAmbientOffloadInvalidated();
-    method public void onEnterAmbient(android.os.Bundle!);
-    method public void onExitAmbient();
-    method public void onUpdateAmbient();
+  @Deprecated public abstract static class AmbientModeSupport.AmbientCallback {
+    ctor @Deprecated public AmbientModeSupport.AmbientCallback();
+    method @Deprecated public void onAmbientOffloadInvalidated();
+    method @Deprecated public void onEnterAmbient(android.os.Bundle!);
+    method @Deprecated public void onExitAmbient();
+    method @Deprecated public void onUpdateAmbient();
   }
 
-  public static interface AmbientModeSupport.AmbientCallbackProvider {
-    method public androidx.wear.ambient.AmbientModeSupport.AmbientCallback! getAmbientCallback();
+  @Deprecated public static interface AmbientModeSupport.AmbientCallbackProvider {
+    method @Deprecated public androidx.wear.ambient.AmbientModeSupport.AmbientCallback! getAmbientCallback();
   }
 
-  public final class AmbientModeSupport.AmbientController {
-    method public boolean isAmbient();
-    method public void setAmbientOffloadEnabled(boolean);
-    method public void setAutoResumeEnabled(boolean);
+  @Deprecated public final class AmbientModeSupport.AmbientController {
+    method @Deprecated public boolean isAmbient();
+    method @Deprecated public void setAmbientOffloadEnabled(boolean);
+    method @Deprecated public void setAutoResumeEnabled(boolean);
   }
 
 }
diff --git a/wear/wear/build.gradle b/wear/wear/build.gradle
index a85136e..83bcb50 100644
--- a/wear/wear/build.gradle
+++ b/wear/wear/build.gradle
@@ -14,6 +14,7 @@
     api("androidx.core:core:1.6.0")
     api("androidx.versionedparcelable:versionedparcelable:1.1.1")
     api('androidx.dynamicanimation:dynamicanimation:1.0.0')
+    api('androidx.lifecycle:lifecycle-runtime:2.5.1')
 
     androidTestImplementation(project(":test:screenshot:screenshot"))
     androidTestImplementation(libs.kotlinStdlib)
diff --git a/wear/wear/src/main/java/androidx/wear/ambient/AmbientLifecycleObserver.kt b/wear/wear/src/main/java/androidx/wear/ambient/AmbientLifecycleObserver.kt
new file mode 100644
index 0000000..7bafa39
--- /dev/null
+++ b/wear/wear/src/main/java/androidx/wear/ambient/AmbientLifecycleObserver.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.wear.ambient
+
+import android.app.Activity
+import android.os.Bundle
+import androidx.lifecycle.LifecycleOwner
+import com.google.android.wearable.compat.WearableActivityController
+import java.util.concurrent.Executor
+
+/**
+ * Lifecycle Observer which can be used to add ambient support to an activity on Wearable devices.
+ *
+ * Applications which wish to show layouts in ambient mode should attach this observer to their
+ * activities or fragments, passing in a set of callback to be notified about ambient state. In
+ * addition, the app needs to declare that it uses the [android.Manifest.permission.WAKE_LOCK]
+ * permission in its manifest.
+ *
+ * The created [AmbientLifecycleObserver] can also be used to query whether the device is in
+ * ambient mode.
+ *
+ * As an example of how to use this class, see the following example:
+ *
+ * ```
+ * class MyActivity : ComponentActivity() {
+ *     private val callbacks = object : AmbientLifecycleObserver.AmbientLifecycleCallback {
+ *         // ...
+ *     }
+ *
+ *     private val ambientObserver = DefaultAmbientLifecycleObserver(this, callbacks)
+ *
+ *     override fun onCreate(savedInstanceState: Bundle) {
+ *         lifecycle.addObserver(ambientObserver)
+ *     }
+ * }
+ * ```
+ *
+ * @param activity The activity that this observer is being attached to.
+ * @param callbackExecutor The executor to run the provided callbacks on.
+ * @param callbacks An instance of [AmbientLifecycleObserverInterface.AmbientLifecycleCallback], used to
+ *                  notify the observer about changes to the ambient state.
+ */
+@Suppress("CallbackName")
+class AmbientLifecycleObserver(
+    activity: Activity,
+    callbackExecutor: Executor,
+    callbacks: AmbientLifecycleObserverInterface.AmbientLifecycleCallback,
+) : AmbientLifecycleObserverInterface {
+    private val delegate: AmbientDelegate
+    private val callbackTranslator = object : AmbientDelegate.AmbientCallback {
+        override fun onEnterAmbient(ambientDetails: Bundle?) {
+            val burnInProtection = ambientDetails?.getBoolean(
+                WearableActivityController.EXTRA_BURN_IN_PROTECTION) ?: false
+            val lowBitAmbient = ambientDetails?.getBoolean(
+                WearableActivityController.EXTRA_LOWBIT_AMBIENT) ?: false
+            callbackExecutor.run {
+                callbacks.onEnterAmbient(AmbientLifecycleObserverInterface.AmbientDetails(
+                    burnInProtectionRequired = burnInProtection,
+                    deviceHasLowBitAmbient = lowBitAmbient
+                ))
+            }
+        }
+
+        override fun onUpdateAmbient() {
+            callbackExecutor.run { callbacks.onUpdateAmbient() }
+        }
+
+        override fun onExitAmbient() {
+            callbackExecutor.run { callbacks.onExitAmbient() }
+        }
+
+        override fun onAmbientOffloadInvalidated() {
+        }
+    }
+
+    /**
+     * Construct a [AmbientLifecycleObserver], using the UI thread to dispatch ambient
+     * callbacks.
+     *
+     * @param activity The activity that this observer is being attached to.
+     * @param callbacks An instance of [AmbientLifecycleObserverInterface.AmbientLifecycleCallback], used to
+     *                  notify the observer about changes to the ambient state.
+     */
+    constructor(
+        activity: Activity,
+        callbacks: AmbientLifecycleObserverInterface.AmbientLifecycleCallback
+    ) : this(activity, { r -> r.run() }, callbacks)
+
+    init {
+        delegate = AmbientDelegate(activity, WearableControllerProvider(), callbackTranslator)
+    }
+
+    override fun isAmbient(): Boolean = delegate.isAmbient
+
+    override fun onCreate(owner: LifecycleOwner) {
+        super.onCreate(owner)
+        delegate.onCreate()
+        delegate.setAmbientEnabled()
+    }
+
+    override fun onResume(owner: LifecycleOwner) {
+        super.onResume(owner)
+        delegate.onResume()
+    }
+
+    override fun onPause(owner: LifecycleOwner) {
+        super.onPause(owner)
+        delegate.onPause()
+    }
+
+    override fun onStop(owner: LifecycleOwner) {
+        super.onStop(owner)
+        delegate.onStop()
+    }
+
+    override fun onDestroy(owner: LifecycleOwner) {
+        super.onDestroy(owner)
+        delegate.onDestroy()
+    }
+}
\ No newline at end of file
diff --git a/wear/wear/src/main/java/androidx/wear/ambient/AmbientLifecycleObserverInterface.kt b/wear/wear/src/main/java/androidx/wear/ambient/AmbientLifecycleObserverInterface.kt
new file mode 100644
index 0000000..353a695
--- /dev/null
+++ b/wear/wear/src/main/java/androidx/wear/ambient/AmbientLifecycleObserverInterface.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.wear.ambient
+
+import androidx.lifecycle.DefaultLifecycleObserver
+
+/**
+ * Interface for LifecycleObservers which are used to add ambient mode support to activities on
+ * Wearable devices.
+ *
+ * This interface can be implemented, or faked out, to allow for testing activities which use
+ * ambient support. Applications should use [AmbientLifecycleObserver] to implement ambient support
+ * on real devices.
+ */
+@Suppress("CallbackName")
+interface AmbientLifecycleObserverInterface : DefaultLifecycleObserver {
+    /**
+     * Details about ambient mode support on the current device, passed to
+     * [AmbientLifecycleCallback.onEnterAmbient].
+     *
+     * @param burnInProtectionRequired whether the ambient layout must implement burn-in protection.
+     *     When this property is set to true, views must be shifted around periodically in ambient
+     *     mode. To ensure that content isn't shifted off the screen, avoid placing content within
+     *     10 pixels of the edge of the screen. Activities should also avoid solid white areas to
+     *     prevent pixel burn-in. Both of these requirements  only apply in ambient mode, and only
+     *     when this property is set to true.
+     * @param deviceHasLowBitAmbient whether this device has low-bit ambient mode. When this
+     *     property is set to true, the screen supports fewer bits for each color in ambient mode.
+     *     In this case, activities should disable anti-aliasing in ambient mode.
+     */
+    class AmbientDetails(
+        val burnInProtectionRequired: Boolean,
+        val deviceHasLowBitAmbient: Boolean
+    ) {
+        override fun toString(): String =
+            "AmbientDetails - burnInProtectionRequired: $burnInProtectionRequired, " +
+                "deviceHasLowBitAmbient: $deviceHasLowBitAmbient"
+    }
+
+    /** Callback to receive ambient mode state changes. */
+    interface AmbientLifecycleCallback {
+        /**
+         * Called when an activity is entering ambient mode. This event is sent while an activity is
+         * running (after onResume, before onPause). All drawing should complete by the conclusion
+         * of this method. Note that {@code invalidate()} calls will be executed before resuming
+         * lower-power mode.
+         *
+         * @param ambientDetails instance of [AmbientDetails] containing information about the
+         *     display being used.
+         */
+        fun onEnterAmbient(ambientDetails: AmbientDetails) {}
+
+        /**
+         * Called when the system is updating the display for ambient mode. Activities may use this
+         * opportunity to update or invalidate views.
+         */
+        fun onUpdateAmbient() {}
+
+        /**
+         * Called when an activity should exit ambient mode. This event is sent while an activity is
+         * running (after onResume, before onPause).
+         */
+        fun onExitAmbient() {}
+    }
+
+    /**
+     * @return {@code true} if the activity is currently in ambient.
+     */
+    fun isAmbient(): Boolean
+}
diff --git a/wear/wear/src/main/java/androidx/wear/ambient/AmbientModeSupport.java b/wear/wear/src/main/java/androidx/wear/ambient/AmbientModeSupport.java
index 0b4b539..80dcd2f 100644
--- a/wear/wear/src/main/java/androidx/wear/ambient/AmbientModeSupport.java
+++ b/wear/wear/src/main/java/androidx/wear/ambient/AmbientModeSupport.java
@@ -77,7 +77,12 @@
  *         }
  *     }
  * }</pre>
+ *
+ * @deprecated Use {@link AmbientLifecycleObserverInterface} and {@link AmbientLifecycleObserver}
+ *     instead. These classes use lifecycle components instead, preventing the need to hook these
+ *     events using fragments.
  */
+@Deprecated
 public final class AmbientModeSupport extends Fragment {
     private static final String TAG = "AmbientModeSupport";
 
diff --git a/wear/wear/src/test/java/androidx/wear/ambient/AmbientLifecycleObserverTest.kt b/wear/wear/src/test/java/androidx/wear/ambient/AmbientLifecycleObserverTest.kt
new file mode 100644
index 0000000..d0fee22
--- /dev/null
+++ b/wear/wear/src/test/java/androidx/wear/ambient/AmbientLifecycleObserverTest.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.wear.ambient
+
+import android.os.Bundle
+import androidx.lifecycle.Lifecycle
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.wearable.compat.WearableActivityController
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AmbientLifecycleObserverTest {
+    private lateinit var scenario: ActivityScenario<AmbientLifecycleObserverTestActivity>
+
+    @Before
+    fun setUp() {
+        scenario = AmbientTestActivityUtil.launchActivity(
+            AmbientLifecycleObserverTestActivity::class.java)
+    }
+
+    private fun resetState(controller: WearableActivityController) {
+        controller.mCreateCalled = false
+        controller.mDestroyCalled = false
+        controller.mPauseCalled = false
+        controller.mResumeCalled = false
+        controller.mStopCalled = false
+    }
+
+    @Test
+    fun testEnterAmbientCallback() {
+        scenario.onActivity { activity ->
+            WearableActivityController.getLastInstance().enterAmbient()
+            assertTrue(activity.enterAmbientCalled)
+            assertFalse(activity.exitAmbientCalled)
+            assertFalse(activity.updateAmbientCalled)
+
+            assertNotNull(activity.enterAmbientArgs)
+
+            // Nothing in the bundle, both should be false.
+            assertFalse(activity.enterAmbientArgs!!.burnInProtectionRequired)
+            assertFalse(activity.enterAmbientArgs!!.deviceHasLowBitAmbient)
+        }
+    }
+
+    @Test
+    fun testEnterAmbientCallbackWithArgs() {
+        scenario.onActivity { activity ->
+            val bundle = Bundle()
+            bundle.putBoolean(WearableActivityController.EXTRA_LOWBIT_AMBIENT, true)
+            bundle.putBoolean(WearableActivityController.EXTRA_BURN_IN_PROTECTION, true)
+
+            WearableActivityController.getLastInstance().enterAmbient(bundle)
+
+            assertTrue(activity.enterAmbientArgs!!.burnInProtectionRequired)
+            assertTrue(activity.enterAmbientArgs!!.deviceHasLowBitAmbient)
+        }
+    }
+
+    @Test
+    fun testExitAmbientCallback() {
+        scenario.onActivity { activity ->
+            WearableActivityController.getLastInstance().exitAmbient()
+            assertFalse(activity.enterAmbientCalled)
+            assertTrue(activity.exitAmbientCalled)
+            assertFalse(activity.updateAmbientCalled)
+        }
+    }
+
+    @Test
+    fun testUpdateAmbientCallback() {
+        scenario.onActivity { activity ->
+            WearableActivityController.getLastInstance().updateAmbient()
+            assertFalse(activity.enterAmbientCalled)
+            assertFalse(activity.exitAmbientCalled)
+            assertTrue(activity.updateAmbientCalled)
+        }
+    }
+
+    @Test
+    fun onCreateCanPassThrough() {
+        // Default after launch is that the activity is running.
+        val controller = WearableActivityController.getLastInstance()
+        assertTrue(controller.mCreateCalled)
+        assertFalse(controller.mDestroyCalled)
+        assertFalse(controller.mPauseCalled)
+        assertTrue(controller.mResumeCalled)
+        assertFalse(controller.mStopCalled)
+    }
+
+    @Test
+    fun onPauseCanPassThrough() {
+        val controller = WearableActivityController.getLastInstance()
+        resetState(controller)
+
+        // Note: STARTED is when the app is paused; RUNNING is when it's actually running.
+        scenario.moveToState(Lifecycle.State.STARTED)
+
+        assertFalse(controller.mCreateCalled)
+        assertFalse(controller.mDestroyCalled)
+        assertTrue(controller.mPauseCalled)
+        assertFalse(controller.mResumeCalled)
+        assertFalse(controller.mStopCalled)
+    }
+
+    @Test
+    fun onStopCanPassThrough() {
+        val controller = WearableActivityController.getLastInstance()
+        resetState(controller)
+
+        scenario.moveToState(Lifecycle.State.CREATED)
+
+        assertFalse(controller.mCreateCalled)
+        assertFalse(controller.mDestroyCalled)
+        assertTrue(controller.mPauseCalled)
+        assertFalse(controller.mResumeCalled)
+        assertTrue(controller.mStopCalled)
+    }
+
+    @Test
+    fun onDestroyCanPassThrough() {
+        val controller = WearableActivityController.getLastInstance()
+        resetState(controller)
+
+        scenario.moveToState(Lifecycle.State.DESTROYED)
+
+        assertFalse(controller.mCreateCalled)
+        assertTrue(controller.mDestroyCalled)
+        assertTrue(controller.mPauseCalled)
+        assertFalse(controller.mResumeCalled)
+        assertTrue(controller.mStopCalled)
+    }
+
+    @Test
+    fun canQueryInAmbient() {
+        scenario.onActivity { activity ->
+            val controller = WearableActivityController.getLastInstance()
+            assertFalse(activity.observer.isAmbient())
+            controller.isAmbient = true
+            assertTrue(activity.observer.isAmbient())
+        }
+    }
+}
diff --git a/wear/wear/src/test/java/androidx/wear/ambient/AmbientLifecycleObserverTestActivity.kt b/wear/wear/src/test/java/androidx/wear/ambient/AmbientLifecycleObserverTestActivity.kt
new file mode 100644
index 0000000..508c401
--- /dev/null
+++ b/wear/wear/src/test/java/androidx/wear/ambient/AmbientLifecycleObserverTestActivity.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.wear.ambient
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+
+class AmbientLifecycleObserverTestActivity : ComponentActivity() {
+    private val callback = object : AmbientLifecycleObserverInterface.AmbientLifecycleCallback {
+        override fun onEnterAmbient(
+            ambientDetails: AmbientLifecycleObserverInterface.AmbientDetails
+        ) {
+            enterAmbientCalled = true
+            enterAmbientArgs = ambientDetails
+        }
+
+        override fun onUpdateAmbient() {
+            updateAmbientCalled = true
+        }
+
+        override fun onExitAmbient() {
+            exitAmbientCalled = true
+        }
+    }
+
+    val observer = AmbientLifecycleObserver(this, { r -> r.run() }, callback)
+
+    var enterAmbientCalled = false
+    var enterAmbientArgs: AmbientLifecycleObserverInterface.AmbientDetails? = null
+    var updateAmbientCalled = false
+    var exitAmbientCalled = false
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        lifecycle.addObserver(observer)
+    }
+}
\ No newline at end of file
diff --git a/wear/wear/src/test/java/com/google/android/wearable/compat/WearableActivityController.java b/wear/wear/src/test/java/com/google/android/wearable/compat/WearableActivityController.java
index eb4d559..79f2291 100644
--- a/wear/wear/src/test/java/com/google/android/wearable/compat/WearableActivityController.java
+++ b/wear/wear/src/test/java/com/google/android/wearable/compat/WearableActivityController.java
@@ -19,11 +19,17 @@
 import android.app.Activity;
 import android.os.Bundle;
 
+import androidx.annotation.Nullable;
+
 /**
  * Mock version of {@link WearableActivityController}. During instrumentation testing, the tests
  * would end up using this instead of the version implemented on device.
  */
 public class WearableActivityController {
+    public static final java.lang.String EXTRA_BURN_IN_PROTECTION =
+            "com.google.android.wearable.compat.extra.BURN_IN_PROTECTION";
+    public static final java.lang.String EXTRA_LOWBIT_AMBIENT =
+            "com.google.android.wearable.compat.extra.LOWBIT_AMBIENT";
 
     private static WearableActivityController sLastInstance;
 
@@ -37,20 +43,44 @@
     private boolean mAmbient = false;
     private boolean mAmbientOffloadEnabled = false;
 
+    public boolean mCreateCalled = false;
+    public boolean mResumeCalled = false;
+    public boolean mPauseCalled = false;
+    public boolean mStopCalled = false;
+    public boolean mDestroyCalled = false;
+
     public WearableActivityController(String tag, Activity activity, AmbientCallback callback) {
         sLastInstance = this;
         mCallback = callback;
     }
 
     // Methods required by the stub but not currently used in tests.
-    public void onCreate() {}
-    public void onResume() {}
-    public void onPause() {}
-    public void onStop() {}
-    public void onDestroy() {}
+    public void onCreate() {
+        mCreateCalled = true;
+    }
+
+    public void onResume() {
+        mResumeCalled = true;
+    }
+
+    public void onPause() {
+        mPauseCalled = true;
+    }
+
+    public void onStop() {
+        mStopCalled = true;
+    }
+
+    public void onDestroy() {
+        mDestroyCalled = true;
+    }
 
     public void enterAmbient() {
-        mCallback.onEnterAmbient(null);
+        enterAmbient(null);
+    }
+
+    public void enterAmbient(@Nullable Bundle enterAmbientArgs) {
+        mCallback.onEnterAmbient(enterAmbientArgs);
     }
 
     public void exitAmbient() {
diff --git a/webkit/integration-tests/testapp/build.gradle b/webkit/integration-tests/testapp/build.gradle
index 2d2e298..c7f39ec 100644
--- a/webkit/integration-tests/testapp/build.gradle
+++ b/webkit/integration-tests/testapp/build.gradle
@@ -31,6 +31,7 @@
     implementation(libs.espressoIdlingNet)
     implementation(libs.espressoIdlingResource)
 
+    androidTestImplementation(libs.espressoRemote)
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testRunner)
diff --git a/webkit/integration-tests/testapp/src/androidTest/AndroidManifest.xml b/webkit/integration-tests/testapp/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..5ec74fb
--- /dev/null
+++ b/webkit/integration-tests/testapp/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<!--
+  Copyright 2023 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.example.androidx.webkit"
+        android:targetProcesses="*">
+        <!-- This enables Multiprocess Espresso. -->
+        <meta-data
+            android:name="remoteMethod"
+            android:value="androidx.test.espresso.remote.EspressoRemote#remoteInit" />
+    </instrumentation>
+</manifest>
\ No newline at end of file
diff --git a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java
index 1fdb830..43e3a0d 100644
--- a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java
+++ b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java
@@ -16,6 +16,11 @@
 
 package com.example.androidx.webkit;
 
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
 import static org.junit.Assert.assertTrue;
 
 import androidx.core.content.ContextCompat;
@@ -52,15 +57,32 @@
     @Test
     public void testSetDataDirectorySuffix() throws Throwable {
         final String dataDirPrefix = "app_webview_";
-        final String dataDirSuffix = "per_process_webview_data_0";
-
-        WebkitTestHelpers.clickMenuListItemWithString(
-                R.string.process_global_config_activity_title);
-        Thread.sleep(1000);
-
+        final String dataDirSuffix = "per_process_webview_data_test";
         File file = new File(ContextCompat.getDataDir(ApplicationProvider.getApplicationContext()),
                 dataDirPrefix + dataDirSuffix);
 
+        // Ensures WebView directory created during earlier tests runs are purged.
+        deleteDirectory(file);
+        // This should ideally be an assumption, but we want a stronger signal to ensure the test
+        // does not silently stop working.
+        if (file.exists()) {
+            throw new RuntimeException("WebView Directory exists before test despite attempt to "
+                    + "delete it");
+        }
+        WebkitTestHelpers.clickMenuListItemWithString(
+                R.string.process_global_config_activity_title);
+        onView(withId(R.id.process_global_textview)).check(matches(withText("WebView Loaded!")));
+
         assertTrue(file.exists());
     }
+
+    private static boolean deleteDirectory(File directoryToBeDeleted) {
+        File[] allContents = directoryToBeDeleted.listFiles();
+        if (allContents != null) {
+            for (File file : allContents) {
+                deleteDirectory(file);
+            }
+        }
+        return directoryToBeDeleted.delete();
+    }
 }
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ImageDragActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ImageDragActivity.java
index 1ae07c2..1569ff6 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ImageDragActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ImageDragActivity.java
@@ -18,6 +18,7 @@
 
 import android.os.Bundle;
 import android.webkit.WebView;
+import android.webkit.WebViewClient;
 
 import androidx.annotation.Nullable;
 import androidx.appcompat.app.AppCompatActivity;
@@ -33,6 +34,7 @@
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_image_drag);
         WebView demoWebview = findViewById(R.id.image_webview);
+        demoWebview.setWebViewClient(new WebViewClient()); // Open links in this WebView.
 
         demoWebview.loadUrl("www.google.com");
     }
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
index acad3fa..b6fb511 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
@@ -20,6 +20,7 @@
 import android.os.Bundle;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
+import android.widget.TextView;
 
 import androidx.annotation.Nullable;
 import androidx.appcompat.app.AppCompatActivity;
@@ -45,11 +46,14 @@
         }
         ProcessGlobalConfig config = new ProcessGlobalConfig();
         config.setDataDirectorySuffix(this,
-                "per_process_webview_data_0");
+                "per_process_webview_data_test");
         ProcessGlobalConfig.apply(config);
         setContentView(R.layout.activity_process_global_config);
         WebView wv = findViewById(R.id.process_global_config_webview);
+        wv.getSettings().setJavaScriptEnabled(true);
         wv.setWebViewClient(new WebViewClient());
         wv.loadUrl("www.google.com");
+        TextView tv = findViewById(R.id.process_global_textview);
+        tv.setText("WebView Loaded!");
     }
 }
diff --git a/webkit/integration-tests/testapp/src/main/res/layout/activity_process_global_config.xml b/webkit/integration-tests/testapp/src/main/res/layout/activity_process_global_config.xml
index c2717b2..e898b1d 100644
--- a/webkit/integration-tests/testapp/src/main/res/layout/activity_process_global_config.xml
+++ b/webkit/integration-tests/testapp/src/main/res/layout/activity_process_global_config.xml
@@ -14,8 +14,25 @@
      limitations under the License.
 -->
 
-<WebView
-xmlns:android="http://schemas.android.com/apk/res/android"
-android:id="@+id/process_global_config_webview"
-android:layout_width="match_parent"
-android:layout_height="match_parent" />
\ No newline at end of file
+<!--Orientation is decided at runtime-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/activity_medium_interstitial"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    tools:ignore="Orientation">
+
+    <TextView
+        android:id="@+id/process_global_textview"
+        android:layout_width="wrap_content"
+        android:layout_height="105dp"
+        android:text="TextView" />
+
+    <WebView
+        android:id="@+id/process_global_config_webview"
+        android:layout_width="match_parent"
+        android:layout_height="604dp"></WebView>
+</LinearLayout>
+
+
diff --git a/webkit/webkit/api/current.txt b/webkit/webkit/api/current.txt
index faf13cb..0c9d5ee 100644
--- a/webkit/webkit/api/current.txt
+++ b/webkit/webkit/api/current.txt
@@ -5,6 +5,16 @@
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_COOKIE_INFO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.List<java.lang.String!> getCookieInfo(android.webkit.CookieManager, String);
   }
 
+  public final class DropDataContentProvider extends android.content.ContentProvider {
+    ctor public DropDataContentProvider();
+    method public int delete(android.net.Uri, String?, String![]?);
+    method public String? getType(android.net.Uri);
+    method public android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+    method public boolean onCreate();
+    method public android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+    method public int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+  }
+
   public abstract class JavaScriptReplyProxy {
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(String);
   }
diff --git a/webkit/webkit/api/public_plus_experimental_current.txt b/webkit/webkit/api/public_plus_experimental_current.txt
index faf13cb..0c9d5ee 100644
--- a/webkit/webkit/api/public_plus_experimental_current.txt
+++ b/webkit/webkit/api/public_plus_experimental_current.txt
@@ -5,6 +5,16 @@
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_COOKIE_INFO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.List<java.lang.String!> getCookieInfo(android.webkit.CookieManager, String);
   }
 
+  public final class DropDataContentProvider extends android.content.ContentProvider {
+    ctor public DropDataContentProvider();
+    method public int delete(android.net.Uri, String?, String![]?);
+    method public String? getType(android.net.Uri);
+    method public android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+    method public boolean onCreate();
+    method public android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+    method public int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+  }
+
   public abstract class JavaScriptReplyProxy {
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(String);
   }
diff --git a/webkit/webkit/api/restricted_current.txt b/webkit/webkit/api/restricted_current.txt
index faf13cb..0c9d5ee 100644
--- a/webkit/webkit/api/restricted_current.txt
+++ b/webkit/webkit/api/restricted_current.txt
@@ -5,6 +5,16 @@
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_COOKIE_INFO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.List<java.lang.String!> getCookieInfo(android.webkit.CookieManager, String);
   }
 
+  public final class DropDataContentProvider extends android.content.ContentProvider {
+    ctor public DropDataContentProvider();
+    method public int delete(android.net.Uri, String?, String![]?);
+    method public String? getType(android.net.Uri);
+    method public android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+    method public boolean onCreate();
+    method public android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+    method public int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+  }
+
   public abstract class JavaScriptReplyProxy {
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(String);
   }
diff --git a/webkit/webkit/src/main/java/androidx/webkit/DropDataContentProvider.java b/webkit/webkit/src/main/java/androidx/webkit/DropDataContentProvider.java
index 6c38760..07acee1 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/DropDataContentProvider.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/DropDataContentProvider.java
@@ -25,7 +25,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
 import androidx.webkit.internal.WebViewGlueCommunicator;
 
 import org.chromium.support_lib_boundary.DropDataContentProviderBoundaryInterface;
@@ -33,13 +32,23 @@
 import java.io.FileNotFoundException;
 
 /**
- * TODO(1353048): Un-hide this after finishing the feature.
+ * WebView provides partial support for Android
+ * <a href="https://developer.android.com/develop/ui/views/touch-and-input/drag-drop">
+ * Drag and Drop</a> allowing images, text and links to be dragged out of a WebView.
  *
- * @hide
- * This should be added to the manifest in order to enable dragging images out.
+ * The content provider is required to make the images drag work, to enable, you should add this
+ * class to your manifest, for example:
+ *
+ * <pre class="prettyprint">
+ *  &lt;provider
+ *             android:authorities="{{your-package}}.DropDataProvider"
+ *             android:name="androidx.webkit.DropDataContentProvider"
+ *             android:exported="false"
+ *             android:grantUriPermissions="true"/&gt;
+ * </pre>
+ *
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class DropDataContentProvider extends ContentProvider {
+public final class DropDataContentProvider extends ContentProvider {
     DropDataContentProviderBoundaryInterface mImpl;
 
     @Override
diff --git a/work/OWNERS b/work/OWNERS
index e994ca6..d390fd5 100644
--- a/work/OWNERS
+++ b/work/OWNERS
@@ -1,6 +1,7 @@
-# Bug component: 409906
+# Bug component: 324783
+sergeyv@google.com
 sumir@google.com
 rahulrav@google.com
 ilake@google.com
 
-per-file settings.gradle = dustinlam@google.com, rahulrav@google.com
+per-file settings.gradle = dustinlam@google.com, rahulrav@google.com, sergeyv@google.com
diff --git a/work/work-inspection/build.gradle b/work/work-inspection/build.gradle
index 5083c43..6b3612c 100644
--- a/work/work-inspection/build.gradle
+++ b/work/work-inspection/build.gradle
@@ -28,7 +28,7 @@
     api("androidx.annotation:annotation:1.1.0")
     api(libs.kotlinStdlib)
     compileOnly("androidx.inspection:inspection:1.0.0")
-    compileOnly("androidx.lifecycle:lifecycle-runtime:2.2.0")
+    compileOnly(project(":lifecycle:lifecycle-runtime"))
     compileOnly(project(":work:work-runtime"))
     compileOnly("androidx.room:room-runtime:2.5.0-beta01")
     androidTestImplementation(project(":inspection:inspection-testing"))
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..4487440 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
                             }
                         }
                     }
@@ -281,7 +281,6 @@
         latch.await()
     }
 
-    override fun getLifecycle(): Lifecycle {
-        return lifecycleRegistry
-    }
+    override val lifecycle: Lifecycle
+        get() = lifecycleRegistry
 }
diff --git a/work/work-runtime/src/androidTest/AndroidManifest.xml b/work/work-runtime/src/androidTest/AndroidManifest.xml
index 3f54449..e636ee9 100644
--- a/work/work-runtime/src/androidTest/AndroidManifest.xml
+++ b/work/work-runtime/src/androidTest/AndroidManifest.xml
@@ -17,10 +17,12 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
 
     <!--
-      ~ Adding this permission to the test-app's AndroidManifest.xml. This is because
+      ~ Adding these permissions to the test-app's AndroidManifest.xml. This is because
       ~ we don't want applications to implicitly add this permission on API 31 and above. We only
       ~ need this permission for tests.
     -->
-    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
+    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
+        android:maxSdkVersion="32"/>
+    <uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
     <application android:name="androidx.multidex.MultiDexApplication"/>
 </manifest>
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java b/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
index 21a89ed..83b8023 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
@@ -619,10 +619,16 @@
         database.insert("workspec", CONFLICT_FAIL, valuesTwo);
         mMigrationTestHelper.runMigrationsAndValidate(TEST_DATABASE, VERSION_17, true,
                 Migration_16_17.INSTANCE);
-        Cursor workSpecs = database.query("SELECT id FROM WorkSpec");
-        assertThat(workSpecs.getCount(), is(1));
-        assertThat(workSpecs.moveToFirst(), is(true));
+        Cursor workSpecs = database.query("SELECT id, input_merger_class_name FROM WorkSpec");
+        assertThat(workSpecs.getCount(), is(2));
+        assertThat(workSpecs.moveToNext(), is(true));
+        assertThat(workSpecs.getString(workSpecs.getColumnIndex("id")), is(idOne));
+        assertThat(workSpecs.getString(workSpecs.getColumnIndex("input_merger_class_name")),
+                is(OverwritingInputMerger.class.getName()));
+        assertThat(workSpecs.moveToNext(), is(true));
         assertThat(workSpecs.getString(workSpecs.getColumnIndex("id")), is(idTwo));
+        assertThat(workSpecs.getString(workSpecs.getColumnIndex("input_merger_class_name")),
+                is(OverwritingInputMerger.class.getName()));
         database.close();
     }
 
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
index 0354b64..2fcecaa 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
@@ -22,6 +22,7 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import androidx.test.filters.RequiresDevice
 import androidx.test.filters.SdkSuppress
 import androidx.work.WorkInfo.State
 import androidx.work.WorkManager.UpdateResult.APPLIED_FOR_NEXT_RUN
@@ -215,6 +216,7 @@
         workManager.awaitSuccess(requestId)
     }
 
+    @RequiresDevice // b/266498479
     @Test
     @MediumTest
     fun progressReset() {
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
index 4d9b724c..ec2f13a 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
@@ -1600,6 +1600,10 @@
     @SuppressWarnings("unchecked")
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void testCancelAllWork_updatesLastCancelAllTimeLiveData() throws InterruptedException {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         PreferenceUtils preferenceUtils = new PreferenceUtils(mWorkManagerImpl.getWorkDatabase());
         preferenceUtils.setLastCancelAllTimeMillis(0L);
 
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/AlarmsTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/AlarmsTest.java
index b9442eef..164a3ef 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/AlarmsTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/AlarmsTest.java
@@ -28,7 +28,6 @@
 
 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.work.DatabaseTest;
 import androidx.work.OneTimeWorkRequest;
@@ -50,7 +49,6 @@
     private final long mTriggerAt = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1);
 
     @Test
-    @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void testSetAlarm_noPreExistingAlarms() {
         OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class).build();
         insertWork(work);
@@ -62,7 +60,6 @@
     }
 
     @Test
-    @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void testSetAlarm_withPreExistingAlarms() {
         OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class).build();
         insertWork(work);
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
index e1edd07..6419d8d 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
@@ -37,6 +37,7 @@
 
 import android.content.Context;
 import android.content.Intent;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.Log;
@@ -205,6 +206,10 @@
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     public void testSchedule() throws InterruptedException {
+        if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
+            return; // b/262909049: Do not run this test on pre-release Android U.
+        }
+
         OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setLastEnqueueTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
                 .setInitialDelay(TimeUnit.HOURS.toMillis(1), TimeUnit.MILLISECONDS)
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;
         }
     }
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.kt
index dc5093b..e36def2 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.kt
@@ -23,6 +23,7 @@
 import androidx.room.migration.AutoMigrationSpec
 import androidx.room.migration.Migration
 import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.work.OverwritingInputMerger
 import androidx.work.impl.WorkDatabaseVersions.VERSION_1
 import androidx.work.impl.WorkDatabaseVersions.VERSION_10
 import androidx.work.impl.WorkDatabaseVersions.VERSION_11
@@ -299,8 +300,14 @@
 object Migration_16_17 : Migration(VERSION_16, VERSION_17) {
     override fun migrate(db: SupportSQLiteDatabase) {
         // b/261721822: unclear how the content of input_merger_class_name could have been,
-        // null such that it fails to migrate to a table with a NOT NULL constrain.
-        db.execSQL("DELETE FROM WorkSpec WHERE input_merger_class_name IS NULL")
+        // null such that it fails to migrate to a table with a NOT NULL constrain, therefore
+        // set the current default value to avoid dropping the worker.
+        db.execSQL(
+            """UPDATE WorkSpec
+                SET input_merger_class_name = '${OverwritingInputMerger::class.java.name}'
+                WHERE input_merger_class_name IS NULL
+                """.trimIndent()
+        )
         db.execSQL(
             """CREATE TABLE IF NOT EXISTS `_new_WorkSpec` (
                 `id` TEXT NOT NULL,
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
index bd85def..d0f484a 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
@@ -788,6 +788,10 @@
     public void setReschedulePendingResult(
             @NonNull BroadcastReceiver.PendingResult rescheduleReceiverResult) {
         synchronized (sLock) {
+            // if we have two broadcast in the row, finish old one and use new one
+            if (mRescheduleReceiverResult != null) {
+                mRescheduleReceiverResult.finish();
+            }
             mRescheduleReceiverResult = rescheduleReceiverResult;
             if (mForceStopRunnableCompleted) {
                 mRescheduleReceiverResult.finish();